├── .copywrite.hcl ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── .go-version ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── action.go ├── catalog-info.yaml ├── checks.go ├── cmd └── round-trip-dumper │ ├── README.md │ └── main.go ├── config.go ├── config_test.go ├── expression.go ├── expression_test.go ├── go.mod ├── go.sum ├── metadata.go ├── metadata_test.go ├── parse_test.go ├── plan.go ├── plan_test.go ├── sanitize ├── copy.go ├── copy_test.go ├── sanitize_change.go ├── sanitize_change_test.go ├── sanitize_plan.go ├── sanitize_plan_test.go ├── sanitize_plan_variables.go ├── sanitize_plan_variables_test.go ├── sanitize_state.go ├── sanitize_state_test.go └── testdata │ ├── basic.golden │ └── basic.json ├── schemas.go ├── schemas_test.go ├── state.go ├── state_test.go ├── testdata ├── 013_module_depends_on │ ├── main.tf │ ├── plan.json │ └── schemas.json ├── 110_basic │ ├── .terraform.lock.hcl │ ├── foo │ │ └── main.tf │ ├── module.tf │ ├── outputs.tf │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── schemas.json │ └── variables.tf ├── 110_sensitive_values │ ├── foo │ │ └── main.tf │ ├── module.tf │ ├── outputs.tf │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── state.json │ ├── terraform.tfstate │ └── variables.tf ├── 120_basic │ ├── .terraform.lock.hcl │ ├── foo │ │ └── main.tf │ ├── module.tf │ ├── outputs.tf │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── schemas.json │ └── variables.tf ├── Makefile ├── basic │ ├── foo │ │ └── main.tf │ ├── functions.json │ ├── module.tf │ ├── outputs.tf │ ├── plan-0.15.json │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── schemas.json │ └── variables.tf ├── config_resource_depends_on │ ├── main.tf │ ├── plan.json │ ├── schemas.json │ └── terraform.tfstate ├── deep_module │ ├── foo │ │ ├── bar │ │ │ └── main.tf │ │ └── main.tf │ ├── main.tf │ ├── plan.json │ └── schemas.json ├── ephemeral_resources │ └── schemas.json ├── explicit_null │ ├── main.tf │ ├── plan.json │ ├── schemas.json │ └── terraform.tfvars ├── functions │ └── schemas.json ├── has_changes │ ├── foo │ │ └── main.tf │ ├── module.tf │ ├── outputs.tf │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── schemas.json │ ├── terraform.tfstate │ └── variables.tf ├── has_checks │ ├── child_moodule │ │ └── main.tf │ ├── main.tf │ ├── plan.json │ ├── schemas.json │ └── state.json ├── identity │ ├── plan.json │ ├── schemas.json │ └── state.json ├── moved_block │ ├── main.tf │ ├── plan.json │ ├── schemas.json │ └── show.json ├── nested_attributes │ └── schemas.json ├── nested_config_keys │ ├── main.tf │ ├── plan.json │ └── schemas.json ├── no_changes │ ├── foo │ │ └── main.tf │ ├── module.tf │ ├── outputs.tf │ ├── plan.json │ ├── providers.tf │ ├── resources.tf │ ├── schemas.json │ ├── state.json │ ├── terraform.tfstate │ └── variables.tf ├── numerics │ └── plan.json ├── output_depends_on │ ├── main.tf │ ├── plan.json │ └── schemas.json ├── provider_version │ ├── main.tf │ ├── plan.json │ └── schemas.json ├── registry_module │ ├── main.tf │ ├── plan.json │ └── schemas.json └── write_only_attribute_on_resource │ └── schemas.json ├── tfjson.go ├── validate.go ├── validate_test.go ├── version.go └── version_test.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2019 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | "testdata/**", 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: ["dependencies"] 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | labels: 13 | - dependencies 14 | groups: 15 | github-actions-breaking: 16 | update-types: 17 | - major 18 | github-actions-backward-compatible: 19 | update-types: 20 | - minor 21 | - patch 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | 3 | Fixes # 4 | 5 | ## Description 6 | 7 | In plain English, describe your approach to addressing the issue linked above. For example, if you made a particular design decision, let us know why you chose this path instead of another solution. 8 | 9 | 10 | ## Rollback Plan 11 | 12 | - [ ] If a change needs to be reverted, we will roll out an update to the code within 7 days. 13 | 14 | ## Changes to Security Controls 15 | 16 | Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | GOPROXY: https://proxy.golang.org/ 16 | 17 | jobs: 18 | copywrite: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 3 21 | steps: 22 | - name: Checkout Repo 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: Install copywrite 25 | uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 26 | - name: Validate Header Compliance 27 | run: copywrite headers --plan 28 | 29 | test: 30 | runs-on: ${{ matrix.os }} 31 | timeout-minutes: 10 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: 36 | - ubuntu-latest 37 | - macos-latest 38 | go: 39 | - "1.18" 40 | - "1.19" 41 | - "1.20" 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | - name: Set up Go 46 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 47 | with: 48 | go-version: ${{ matrix.go }} 49 | - name: Go mod download 50 | run: go mod download -x 51 | - name: Go mod verify 52 | run: go mod verify 53 | - name: Run tests 54 | run: go test -cover -v ./... 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | plan.tfplan 3 | terraform.tfstate.backup 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.20 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This codebase has shared ownership and responsibility. 2 | * @hashicorp/terraform-core @hashicorp/terraform-core-plugins @hashicorp/tf-editor-experience-engineers 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to terraform-json 2 | 3 | ## Versioning 4 | 5 | The `github.com/hashicorp/terraform-json` Go module in its entirety is versioned according to [Go module versioning](https://golang.org/ref/mod#versions) with Git tags. 6 | 7 | There is currently no firm plan for releasing v1. 8 | 9 | ## Releases 10 | 11 | Releases are made on a reasonably regular basis by the Terraform team, using our custom CI workflows. There is currently no set release schedule and no requirement for _contributors_ to write changelog entries. 12 | 13 | The following notes are only relevant to maintainers. 14 | 15 | [Create new release](https://github.com/hashicorp/terraform-json/releases/new) via GitHub UI to point to the new tag and use GitHub to generate the changelog (`Generate release notes` button). 16 | 17 | You can format the generated changelog before publishing - e.g. ensure entries are grouped into categories such as `ENHANCEMENTS`, `BUG FIXES` and `INTERNAL`. 18 | 19 | ## Security vulnerabilities 20 | 21 | Please disclose security vulnerabilities by following the procedure 22 | described at https://www.hashicorp.com/security#vulnerability-reporting. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOTOOLS = \ 2 | gotest.tools/gotestsum@latest 3 | 4 | test: tools 5 | gotestsum --format=short-verbose $(TEST) $(TESTARGS) 6 | 7 | generate: 8 | cd testdata && make generate 9 | 10 | modules: 11 | go mod download && go mod verify 12 | 13 | test-circle: 14 | mkdir -p test-results/terraform-json 15 | gotestsum --format=short-verbose --junitfile test-results/terraform-json/results.xml 16 | 17 | tools: 18 | @echo $(GOTOOLS) | xargs -t -n1 go install 19 | go mod tidy 20 | 21 | .PHONY: test generate modules test-circle tools 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-json 2 | 3 | [![GoDoc](https://godoc.org/github.com/hashicorp/terraform-json?status.svg)](https://godoc.org/github.com/hashicorp/terraform-json) 4 | 5 | This repository houses data types designed to help parse the data produced by 6 | two [Terraform](https://www.terraform.io/) commands: 7 | 8 | * [`terraform show -json`](https://www.terraform.io/docs/commands/show.html#json-output) 9 | * [`terraform providers schema -json`](https://www.terraform.io/docs/commands/providers/schema.html#json) 10 | 11 | While containing mostly data types, there are also a few helpers to assist with 12 | working with the data. 13 | 14 | This repository also serves as de facto documentation for the formats produced 15 | by these commands. For more details, see the 16 | [GoDoc](https://godoc.org/github.com/hashicorp/terraform-json). 17 | 18 | ## Should I use this library? 19 | 20 | This library was built for a few specific applications, and is not intended for 21 | general purpose use. 22 | 23 | The Terraform core team **recommends against** using `terraform-json` if your 24 | application has any of the following requirements: 25 | 26 | * **Forward-compatibility**: each version of this library represents a specific 27 | snapshot of the [Terraform JSON output format](https://developer.hashicorp.com/terraform/internals/json-format), 28 | and it often slightly lags behind Terraform itself. The library supports 29 | [the 1.x compatibility promises](https://developer.hashicorp.com/terraform/language/v1-compatibility-promises) 30 | but you will need to upgrade the version promptly to use new additions. If you 31 | require full compatibility with future Terraform versions, we recommend 32 | implementing your own custom decoders for the parts of the JSON format you need. 33 | * **Writing JSON output**: the structures in this library are not guaranteed to emit 34 | JSON data which is semantically equivalent to Terraform itself. If your application 35 | must robustly write JSON data to be consumed by systems which expect Terraform's 36 | format to be supported, you should implement your own custom encoders. 37 | * **Filtering or round-tripping**: the Terraform JSON formats are designed to be 38 | forwards compatible, and permit new attributes to be added which may safely be 39 | ignored by earlier versions of consumers. This library **drops unknown attributes**, 40 | which means it is unsuitable for any application which intends to filter data 41 | or read-modify-write data which will be consumed downstream. Any application doing 42 | this will silently drop new data from new versions. For this application, you should 43 | implement a custom decoder and encoder which preserves any unknown attributes 44 | through a round-trip. 45 | 46 | When is `terraform-json` suitable? We recommend using it for applications which 47 | decode the core stable data types and use it directly, and don't attempt to emit 48 | JSON to be consumed by applications which expect the Terraform format. 49 | 50 | ## Why a separate repository? 51 | 52 | To reduce dependencies on any of Terraform core's internals, we've made a design 53 | decision to make any helpers or libraries that work with the external JSON data 54 | external and not a part of the Terraform GitHub repository itself. 55 | 56 | While Terraform core will change often and be relatively unstable, this library 57 | will see a smaller amount of change. Most of the major changes have already 58 | happened leading up to 0.12, so you can expect this library to only see minor 59 | incremental changes going forward. 60 | 61 | For this reason, `terraform show -json` and `terraform providers schema -json` 62 | is the recommended format for working with Terraform data externally, and as 63 | such, if you require any help working with the data in these formats, or even a 64 | reference of how the JSON is formatted, use this repository. 65 | -------------------------------------------------------------------------------- /action.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | // Action is a valid action type for a resource change. 7 | // 8 | // Note that a singular Action is not telling of a full resource 9 | // change operation. Certain resource actions, such as replacement, 10 | // are a composite of more than one type. See the Actions type and 11 | // its helpers for more information. 12 | type Action string 13 | 14 | const ( 15 | // ActionNoop denotes a no-op operation. 16 | ActionNoop Action = "no-op" 17 | 18 | // ActionCreate denotes a create operation. 19 | ActionCreate Action = "create" 20 | 21 | // ActionRead denotes a read operation. 22 | ActionRead Action = "read" 23 | 24 | // ActionUpdate denotes an update operation. 25 | ActionUpdate Action = "update" 26 | 27 | // ActionDelete denotes a delete operation. 28 | ActionDelete Action = "delete" 29 | 30 | // ActionForget denotes a forget operation. 31 | ActionForget Action = "forget" 32 | ) 33 | 34 | // Actions denotes a valid change type. 35 | type Actions []Action 36 | 37 | // NoOp is true if this set of Actions denotes a no-op. 38 | func (a Actions) NoOp() bool { 39 | if len(a) != 1 { 40 | return false 41 | } 42 | 43 | return a[0] == ActionNoop 44 | } 45 | 46 | // Create is true if this set of Actions denotes creation of a new 47 | // resource. 48 | func (a Actions) Create() bool { 49 | if len(a) != 1 { 50 | return false 51 | } 52 | 53 | return a[0] == ActionCreate 54 | } 55 | 56 | // Read is true if this set of Actions denotes a read operation only. 57 | func (a Actions) Read() bool { 58 | if len(a) != 1 { 59 | return false 60 | } 61 | 62 | return a[0] == ActionRead 63 | } 64 | 65 | // Update is true if this set of Actions denotes an update operation. 66 | func (a Actions) Update() bool { 67 | if len(a) != 1 { 68 | return false 69 | } 70 | 71 | return a[0] == ActionUpdate 72 | } 73 | 74 | // Delete is true if this set of Actions denotes resource removal. 75 | func (a Actions) Delete() bool { 76 | if len(a) != 1 { 77 | return false 78 | } 79 | 80 | return a[0] == ActionDelete 81 | } 82 | 83 | // DestroyBeforeCreate is true if this set of Actions denotes a 84 | // destroy-before-create operation. This is the standard resource 85 | // replacement method. 86 | func (a Actions) DestroyBeforeCreate() bool { 87 | if len(a) != 2 { 88 | return false 89 | } 90 | 91 | return a[0] == ActionDelete && a[1] == ActionCreate 92 | } 93 | 94 | // CreateBeforeDestroy is true if this set of Actions denotes a 95 | // create-before-destroy operation, usually the result of replacement 96 | // to a resource that has the create_before_destroy lifecycle option 97 | // set. 98 | func (a Actions) CreateBeforeDestroy() bool { 99 | if len(a) != 2 { 100 | return false 101 | } 102 | 103 | return a[0] == ActionCreate && a[1] == ActionDelete 104 | } 105 | 106 | // Replace is true if this set of Actions denotes a valid replacement 107 | // operation. 108 | func (a Actions) Replace() bool { 109 | return a.DestroyBeforeCreate() || a.CreateBeforeDestroy() 110 | } 111 | 112 | // Forget is true if this set of Actions denotes a forget operation. 113 | func (a Actions) Forget() bool { 114 | if len(a) != 1 { 115 | return false 116 | } 117 | 118 | return a[0] == ActionForget 119 | } 120 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | # 4 | # Intended for internal HashiCorp use only 5 | apiVersion: backstage.io/v1alpha1 6 | kind: Component 7 | metadata: 8 | name: terraform-json 9 | description: Helper types for the Terraform external data representation 10 | annotations: 11 | github.com/project-slug: hashicorp/terraform-json 12 | jira/project-key: TF 13 | jira/label: terraform-json 14 | spec: 15 | type: library 16 | owner: team-tf-core 17 | lifecycle: production 18 | -------------------------------------------------------------------------------- /checks.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | // CheckKind is a string representation of the type of conditional check 7 | // referenced in a check result. 8 | type CheckKind string 9 | 10 | const ( 11 | // CheckKindResource indicates the check result is from a pre- or 12 | // post-condition on a resource or data source. 13 | CheckKindResource CheckKind = "resource" 14 | 15 | // CheckKindOutputValue indicates the check result is from an output 16 | // post-condition. 17 | CheckKindOutputValue CheckKind = "output_value" 18 | 19 | // CheckKindCheckBlock indicates the check result is from a check block. 20 | CheckKindCheckBlock CheckKind = "check" 21 | ) 22 | 23 | // CheckStatus is a string representation of the status of a given conditional 24 | // check. 25 | type CheckStatus string 26 | 27 | const ( 28 | // CheckStatusPass indicates the check passed. 29 | CheckStatusPass CheckStatus = "pass" 30 | 31 | // CheckStatusFail indicates the check failed. 32 | CheckStatusFail CheckStatus = "fail" 33 | 34 | // CheckStatusError indicates the check errored. This is distinct from 35 | // CheckStatusFail in that it represents a logical or configuration error 36 | // within the check block that prevented the check from executing, as 37 | // opposed to the check was attempted and evaluated to false. 38 | CheckStatusError CheckStatus = "error" 39 | 40 | // CheckStatusUnknown indicates the result of the check was not known. This 41 | // could be because a value within the check could not be known at plan 42 | // time, or because the overall plan failed for an unrelated reason before 43 | // this check could be executed. 44 | CheckStatusUnknown CheckStatus = "unknown" 45 | ) 46 | 47 | // CheckStaticAddress details the address of the object that performed a given 48 | // check. The static address points to the overall resource, as opposed to the 49 | // dynamic address which contains the instance key for any resource that has 50 | // multiple instances. 51 | type CheckStaticAddress struct { 52 | // ToDisplay is a formatted and ready to display representation of the 53 | // address. 54 | ToDisplay string `json:"to_display"` 55 | 56 | // Kind represents the CheckKind of this check. 57 | Kind CheckKind `json:"kind"` 58 | 59 | // Module is the module part of the address. This will be empty for any 60 | // resources in the root module. 61 | Module string `json:"module,omitempty"` 62 | 63 | // Mode is the ResourceMode of the resource that contains this check. This 64 | // field is only set is Kind equals CheckKindResource. 65 | Mode ResourceMode `json:"mode,omitempty"` 66 | 67 | // Type is the resource type for the resource that contains this check. This 68 | // field is only set if Kind equals CheckKindResource. 69 | Type string `json:"type,omitempty"` 70 | 71 | // Name is the name of the resource, check block, or output that contains 72 | // this check. 73 | Name string `json:"name,omitempty"` 74 | } 75 | 76 | // CheckDynamicAddress contains the InstanceKey field for any resources that 77 | // have multiple instances. A complete address can be built by combining the 78 | // CheckStaticAddress with the CheckDynamicAddress. 79 | type CheckDynamicAddress struct { 80 | // ToDisplay is a formatted and ready to display representation of the 81 | // full address, including the additional information from the relevant 82 | // CheckStaticAddress. 83 | ToDisplay string `json:"to_display"` 84 | 85 | // Module is the module part of the address. This address will include the 86 | // instance key for any module expansions resulting from foreach or count 87 | // arguments. This field will be empty for any resources within the root 88 | // module. 89 | Module string `json:"module,omitempty"` 90 | 91 | // InstanceKey is the instance key for any instances of a given resource. 92 | // 93 | // InstanceKey will be empty if there was no foreach or count argument 94 | // defined on the containing object. 95 | InstanceKey interface{} `json:"instance_key,omitempty"` 96 | } 97 | 98 | // CheckResultStatic is the container for a "checkable object". 99 | // 100 | // A "checkable object" is a resource or data source, an output, or a check 101 | // block. 102 | type CheckResultStatic struct { 103 | // Address is the absolute address of the "checkable object" 104 | Address CheckStaticAddress `json:"address"` 105 | 106 | // Status is the overall status for all the checks within this object. 107 | Status CheckStatus `json:"status"` 108 | 109 | // Instances contains the results for dynamic object that belongs to this 110 | // static object. For example, any instances created from an object using 111 | // the foreach or count meta arguments. 112 | // 113 | // Check blocks and outputs will only contain a single instance, while 114 | // resources can contain 1 to many. 115 | Instances []CheckResultDynamic `json:"instances,omitempty"` 116 | } 117 | 118 | // CheckResultDynamic describes the check result for a dynamic object that 119 | // results from the expansion of the containing object. 120 | type CheckResultDynamic struct { 121 | // Address is the relative address of this instance given the Address in the 122 | // parent object. 123 | Address CheckDynamicAddress `json:"address"` 124 | 125 | // Status is the overall status for the checks within this dynamic object. 126 | Status CheckStatus `json:"status"` 127 | 128 | // Problems describes any additional optional details about this check if 129 | // the check failed. 130 | // 131 | // This will not include the errors resulting from this check block, as they 132 | // will be exposed as diagnostics in the original terraform execution. It 133 | // may contain any failure messages even if the overall status is 134 | // CheckStatusError, however, as the instance could contain multiple checks 135 | // that returned a mix of error and failure statuses. 136 | Problems []CheckResultProblem `json:"problems,omitempty"` 137 | } 138 | 139 | // CheckResultProblem describes one of potentially several problems that led to 140 | // a check being classied as CheckStatusFail. 141 | type CheckResultProblem struct { 142 | // Message is the condition error message provided by the original check 143 | // author. 144 | Message string `json:"message"` 145 | } 146 | -------------------------------------------------------------------------------- /cmd/round-trip-dumper/README.md: -------------------------------------------------------------------------------- 1 | # Round-Trip Dumper/Diagnostic Tool 2 | 3 | This directory contains a simple tool that will load a plan JSON file, and then 4 | immediately dump it back out to stdout. It's helpful when troubleshooting large 5 | parsing errors, which should (hopefully) be rare. 6 | 7 | `go build ./` in this directory to build the binary. `go run` also works if you 8 | don't need the binary permanently. 9 | 10 | ## Diffing 11 | 12 | The `-diff` flag will automatically diff the result for you. `colordiff` is used 13 | if it's present, otherwise regular `diff` is used. 14 | -------------------------------------------------------------------------------- /cmd/round-trip-dumper/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | 14 | tfjson "github.com/hashicorp/terraform-json" 15 | ) 16 | 17 | var ( 18 | diff = flag.Bool("diff", false, "diff output instead of writing") 19 | schema = flag.Bool("schema", false, "input is a schema, not a plan") 20 | ) 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | if flag.NArg() < 1 { 26 | fmt.Fprintf(os.Stderr, "usage: %s FILE\n\n", os.Args[0]) 27 | os.Exit(1) 28 | } 29 | 30 | path := flag.Arg(0) 31 | 32 | f, err := os.Open(path) 33 | if err != nil { 34 | fmt.Fprintln(os.Stderr, err) 35 | os.Exit(1) 36 | } 37 | 38 | defer f.Close() 39 | 40 | var parsed interface{} 41 | if *schema { 42 | parsed = &tfjson.ProviderSchemas{} 43 | } else { 44 | parsed = &tfjson.Plan{} 45 | } 46 | 47 | dec := json.NewDecoder(f) 48 | dec.DisallowUnknownFields() 49 | if err = dec.Decode(parsed); err != nil { 50 | fmt.Fprintln(os.Stderr, err) 51 | os.Exit(1) 52 | } 53 | 54 | out, err := json.MarshalIndent(parsed, "", " ") 55 | if err != nil { 56 | fmt.Fprintln(os.Stderr, err) 57 | os.Exit(1) 58 | } 59 | 60 | out = append(out, byte('\n')) 61 | 62 | if *diff { 63 | var diffCmd string 64 | if _, err := exec.LookPath("colordiff"); err == nil { 65 | diffCmd = "colordiff" 66 | } else { 67 | diffCmd = "diff" 68 | } 69 | 70 | cmd := exec.Command(diffCmd, "-urN", path, "-") 71 | cmd.Stdin = bytes.NewBuffer(out) 72 | cmd.Stdout = os.Stdout 73 | cmd.Stderr = os.Stderr 74 | if err := cmd.Run(); err != nil { 75 | if err.(*exec.ExitError).ProcessState.ExitCode() > 1 { 76 | os.Exit(1) 77 | } 78 | } else { 79 | fmt.Fprintln(os.Stderr, "[no diff]") 80 | } 81 | } else { 82 | os.Stdout.Write(out) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | ) 10 | 11 | // Config represents the complete configuration source. 12 | type Config struct { 13 | // A map of all provider instances across all modules in the 14 | // configuration. 15 | // 16 | // The index for this field is opaque and should not be parsed. Use 17 | // the individual fields in ProviderConfig to discern actual data 18 | // about the provider such as name, alias, or defined module. 19 | ProviderConfigs map[string]*ProviderConfig `json:"provider_config,omitempty"` 20 | 21 | // The root module in the configuration. Any child modules descend 22 | // off of here. 23 | RootModule *ConfigModule `json:"root_module,omitempty"` 24 | } 25 | 26 | // Validate checks to ensure that the config is present. 27 | func (c *Config) Validate() error { 28 | if c == nil { 29 | return errors.New("config is nil") 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func (c *Config) UnmarshalJSON(b []byte) error { 36 | type rawConfig Config 37 | var config rawConfig 38 | 39 | err := json.Unmarshal(b, &config) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | *c = *(*Config)(&config) 45 | 46 | return c.Validate() 47 | } 48 | 49 | // ProviderConfig describes a provider configuration instance. 50 | type ProviderConfig struct { 51 | // The name of the provider, ie: "aws". 52 | Name string `json:"name,omitempty"` 53 | 54 | // The fully-specified name of the provider, ie: "registry.terraform.io/hashicorp/aws". 55 | FullName string `json:"full_name,omitempty"` 56 | 57 | // The alias of the provider, ie: "us-east-1". 58 | Alias string `json:"alias,omitempty"` 59 | 60 | // The address of the module the provider is declared in. 61 | ModuleAddress string `json:"module_address,omitempty"` 62 | 63 | // Any non-special configuration values in the provider, indexed by 64 | // key. 65 | Expressions map[string]*Expression `json:"expressions,omitempty"` 66 | 67 | // The defined version constraint for this provider. 68 | VersionConstraint string `json:"version_constraint,omitempty"` 69 | } 70 | 71 | // ConfigModule describes a module in Terraform configuration. 72 | type ConfigModule struct { 73 | // The outputs defined in the module. 74 | Outputs map[string]*ConfigOutput `json:"outputs,omitempty"` 75 | 76 | // The resources defined in the module. 77 | Resources []*ConfigResource `json:"resources,omitempty"` 78 | 79 | // Any "module" stanzas within the specific module. 80 | ModuleCalls map[string]*ModuleCall `json:"module_calls,omitempty"` 81 | 82 | // The variables defined in the module. 83 | Variables map[string]*ConfigVariable `json:"variables,omitempty"` 84 | } 85 | 86 | // ConfigOutput defines an output as defined in configuration. 87 | type ConfigOutput struct { 88 | // Indicates whether or not the output was marked as sensitive. 89 | Sensitive bool `json:"sensitive,omitempty"` 90 | 91 | // The defined value of the output. 92 | Expression *Expression `json:"expression,omitempty"` 93 | 94 | // The defined description of this output. 95 | Description string `json:"description,omitempty"` 96 | 97 | // The defined dependencies tied to this output. 98 | DependsOn []string `json:"depends_on,omitempty"` 99 | } 100 | 101 | // ConfigResource is the configuration representation of a resource. 102 | type ConfigResource struct { 103 | // The address of the resource relative to the module that it is 104 | // in. 105 | Address string `json:"address,omitempty"` 106 | 107 | // The resource mode. 108 | Mode ResourceMode `json:"mode,omitempty"` 109 | 110 | // The type of resource, ie: "null_resource" in 111 | // "null_resource.foo". 112 | Type string `json:"type,omitempty"` 113 | 114 | // The name of the resource, ie: "foo" in "null_resource.foo". 115 | Name string `json:"name,omitempty"` 116 | 117 | // An opaque key representing the provider configuration this 118 | // module uses. Note that there are more than one circumstance that 119 | // this key will not match what is found in the ProviderConfigs 120 | // field in the root Config structure, and as such should not be 121 | // relied on for that purpose. 122 | ProviderConfigKey string `json:"provider_config_key,omitempty"` 123 | 124 | // The list of provisioner defined for this configuration. This 125 | // will be nil if no providers are defined. 126 | Provisioners []*ConfigProvisioner `json:"provisioners,omitempty"` 127 | 128 | // Any non-special configuration values in the resource, indexed by 129 | // key. 130 | Expressions map[string]*Expression `json:"expressions,omitempty"` 131 | 132 | // The resource's configuration schema version. With access to the 133 | // specific Terraform provider for this resource, this can be used 134 | // to determine the correct schema for the configuration data 135 | // supplied in Expressions. 136 | SchemaVersion uint64 `json:"schema_version"` 137 | 138 | // The expression data for the "count" value in the resource. 139 | CountExpression *Expression `json:"count_expression,omitempty"` 140 | 141 | // The expression data for the "for_each" value in the resource. 142 | ForEachExpression *Expression `json:"for_each_expression,omitempty"` 143 | 144 | // The contents of the "depends_on" config directive, which 145 | // declares explicit dependencies for this resource. 146 | DependsOn []string `json:"depends_on,omitempty"` 147 | } 148 | 149 | // ConfigVariable defines a variable as defined in configuration. 150 | type ConfigVariable struct { 151 | // The defined default value of the variable. 152 | Default interface{} `json:"default,omitempty"` 153 | 154 | // The defined text description of the variable. 155 | Description string `json:"description,omitempty"` 156 | 157 | // Whether the variable is marked as sensitive 158 | Sensitive bool `json:"sensitive,omitempty"` 159 | } 160 | 161 | // ConfigProvisioner describes a provisioner declared in a resource 162 | // configuration. 163 | type ConfigProvisioner struct { 164 | // The type of the provisioner, ie: "local-exec". 165 | Type string `json:"type,omitempty"` 166 | 167 | // Any non-special configuration values in the provisioner, indexed by 168 | // key. 169 | Expressions map[string]*Expression `json:"expressions,omitempty"` 170 | } 171 | 172 | // ModuleCall describes a declared "module" within a configuration. 173 | // It also contains the data for the module itself. 174 | type ModuleCall struct { 175 | // The contents of the "source" field. 176 | Source string `json:"source,omitempty"` 177 | 178 | // Any non-special configuration values in the module, indexed by 179 | // key. 180 | Expressions map[string]*Expression `json:"expressions,omitempty"` 181 | 182 | // The expression data for the "count" value in the module. 183 | CountExpression *Expression `json:"count_expression,omitempty"` 184 | 185 | // The expression data for the "for_each" value in the module. 186 | ForEachExpression *Expression `json:"for_each_expression,omitempty"` 187 | 188 | // The configuration data for the module itself. 189 | Module *ConfigModule `json:"module,omitempty"` 190 | 191 | // The version constraint for modules that come from the registry. 192 | VersionConstraint string `json:"version_constraint,omitempty"` 193 | 194 | // The explicit resource dependencies for the "depends_on" value. 195 | // As it must be a slice of references, Expression is not used. 196 | DependsOn []string `json:"depends_on,omitempty"` 197 | } 198 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestConfigValidate(t *testing.T) { 13 | f, err := os.Open("testdata/basic/plan.json") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer f.Close() 18 | 19 | var plan *Plan 20 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if err := plan.Config.Validate(); err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /expression.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import "encoding/json" 7 | 8 | type unknownConstantValue struct{} 9 | 10 | // UnknownConstantValue is a singleton type that denotes that a 11 | // constant value is explicitly unknown. This is set during an 12 | // unmarshal when references are found in an expression to help more 13 | // explicitly differentiate between an explicit null and unknown 14 | // value. 15 | var UnknownConstantValue = &unknownConstantValue{} 16 | 17 | // Expression describes the format for an individual key in a 18 | // Terraform configuration. 19 | // 20 | // This struct wraps ExpressionData to support custom JSON parsing. 21 | type Expression struct { 22 | *ExpressionData 23 | } 24 | 25 | // ExpressionData describes the format for an individual key in a 26 | // Terraform configuration. 27 | type ExpressionData struct { 28 | // If the *entire* expression is a constant-defined value, this 29 | // will contain the Go representation of the expression's data. 30 | // 31 | // Note that a nil here denotes and explicit null. When a value is 32 | // unknown on part of the value coming from an expression that 33 | // cannot be resolved at parse time, this field will contain 34 | // UnknownConstantValue. 35 | ConstantValue interface{} `json:"constant_value,omitempty"` 36 | 37 | // If any part of the expression contained values that were not 38 | // able to be resolved at parse-time, this will contain a list of 39 | // the referenced identifiers that caused the value to be unknown. 40 | References []string `json:"references,omitempty"` 41 | 42 | // A list of complex objects that were nested in this expression. 43 | // If this value is a nested block in configuration, sometimes 44 | // referred to as a "sub-resource", this field will contain those 45 | // values, and ConstantValue and References will be blank. 46 | NestedBlocks []map[string]*Expression `json:"-"` 47 | } 48 | 49 | // UnmarshalJSON implements json.Unmarshaler for Expression. 50 | func (e *Expression) UnmarshalJSON(b []byte) error { 51 | result := new(ExpressionData) 52 | 53 | // Check to see if this is an array first. If it is, this is more 54 | // than likely a list of nested blocks. 55 | var rawNested []map[string]json.RawMessage 56 | if err := json.Unmarshal(b, &rawNested); err == nil { 57 | result.NestedBlocks, err = unmarshalExpressionBlocks(rawNested) 58 | if err != nil { 59 | return err 60 | } 61 | } else { 62 | // It's a non-nested expression block, parse normally 63 | if err := json.Unmarshal(b, &result); err != nil { 64 | return err 65 | } 66 | 67 | // If References is non-zero, then ConstantValue is unknown. Set 68 | // this explicitly. 69 | if len(result.References) > 0 { 70 | result.ConstantValue = UnknownConstantValue 71 | } 72 | } 73 | 74 | e.ExpressionData = result 75 | return nil 76 | } 77 | 78 | func unmarshalExpressionBlocks(raw []map[string]json.RawMessage) ([]map[string]*Expression, error) { 79 | var result []map[string]*Expression 80 | 81 | for _, rawBlock := range raw { 82 | block := make(map[string]*Expression) 83 | for k, rawExpr := range rawBlock { 84 | var expr *Expression 85 | if err := json.Unmarshal(rawExpr, &expr); err != nil { 86 | return nil, err 87 | } 88 | 89 | block[k] = expr 90 | } 91 | 92 | result = append(result, block) 93 | } 94 | 95 | return result, nil 96 | } 97 | 98 | // MarshalJSON implements json.Marshaler for Expression. 99 | func (e *Expression) MarshalJSON() ([]byte, error) { 100 | switch { 101 | case len(e.ExpressionData.NestedBlocks) > 0: 102 | return marshalExpressionBlocks(e.ExpressionData.NestedBlocks) 103 | 104 | case e.ExpressionData.ConstantValue == UnknownConstantValue: 105 | return json.Marshal(&ExpressionData{ 106 | References: e.ExpressionData.References, 107 | }) 108 | } 109 | 110 | return json.Marshal(e.ExpressionData) 111 | } 112 | 113 | func marshalExpressionBlocks(nested []map[string]*Expression) ([]byte, error) { 114 | var rawNested []map[string]json.RawMessage 115 | for _, block := range nested { 116 | rawBlock := make(map[string]json.RawMessage) 117 | for k, expr := range block { 118 | raw, err := json.Marshal(expr) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | rawBlock[k] = raw 124 | } 125 | 126 | rawNested = append(rawNested, rawBlock) 127 | } 128 | 129 | return json.Marshal(rawNested) 130 | } 131 | -------------------------------------------------------------------------------- /expression_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/davecgh/go-spew/spew" 12 | ) 13 | 14 | func TestUnmarshalExpressions(t *testing.T) { 15 | cases := []struct { 16 | name string 17 | in string 18 | expected *ConfigResource 19 | }{ 20 | { 21 | name: "basic", 22 | in: ` 23 | { 24 | "address": "aws_instance.foo", 25 | "mode": "managed", 26 | "type": "aws_instance", 27 | "name": "foo", 28 | "provider_config_key": "aws", 29 | "expressions": { 30 | "ami": { 31 | "constant_value": "ami-foobar" 32 | }, 33 | "ebs_block_device": [ 34 | { 35 | "device_name": { 36 | "references": [ 37 | "var.foo" 38 | ] 39 | } 40 | } 41 | ], 42 | "instance_type": { 43 | "constant_value": "t2.micro" 44 | } 45 | }, 46 | "schema_version": 1 47 | } 48 | `, 49 | expected: &ConfigResource{ 50 | Address: "aws_instance.foo", 51 | Mode: ManagedResourceMode, 52 | Type: "aws_instance", 53 | Name: "foo", 54 | ProviderConfigKey: "aws", 55 | Expressions: map[string]*Expression{ 56 | "ami": { 57 | ExpressionData: &ExpressionData{ 58 | ConstantValue: "ami-foobar", 59 | }, 60 | }, 61 | "ebs_block_device": { 62 | ExpressionData: &ExpressionData{ 63 | NestedBlocks: []map[string]*Expression{ 64 | { 65 | "device_name": { 66 | ExpressionData: &ExpressionData{ 67 | ConstantValue: UnknownConstantValue, 68 | References: []string{"var.foo"}, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | "instance_type": { 76 | ExpressionData: &ExpressionData{ 77 | ConstantValue: "t2.micro", 78 | }, 79 | }, 80 | }, 81 | SchemaVersion: 1, 82 | }, 83 | }, 84 | { 85 | name: "explicit null in contstant value", 86 | in: ` 87 | { 88 | "address": "null_resource.foo", 89 | "mode": "managed", 90 | "type": "null_resource", 91 | "name": "foo", 92 | "provider_config_key": "null", 93 | "expressions": { 94 | "triggers": { 95 | "constant_value": { 96 | "foo": null 97 | } 98 | } 99 | }, 100 | "schema_version": 0 101 | } 102 | `, 103 | expected: &ConfigResource{ 104 | Address: "null_resource.foo", 105 | Mode: ManagedResourceMode, 106 | Type: "null_resource", 107 | Name: "foo", 108 | ProviderConfigKey: "null", 109 | Expressions: map[string]*Expression{ 110 | "triggers": { 111 | ExpressionData: &ExpressionData{ 112 | ConstantValue: map[string]interface{}{ 113 | "foo": nil, 114 | }, 115 | }, 116 | }, 117 | }, 118 | SchemaVersion: 0, 119 | }, 120 | }, 121 | } 122 | 123 | for _, tc := range cases { 124 | t.Run(tc.name, func(t *testing.T) { 125 | var actual *ConfigResource 126 | if err := json.Unmarshal([]byte(tc.in), &actual); err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | if !reflect.DeepEqual(tc.expected, actual) { 131 | t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s\n\n", spew.Sdump(tc.expected), spew.Sdump(actual)) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/terraform-json 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/google/go-cmp v0.7.0 8 | github.com/hashicorp/go-version v1.7.0 9 | github.com/mitchellh/copystructure v1.2.0 10 | github.com/sebdah/goldie v1.0.0 11 | github.com/zclconf/go-cty v1.16.3 12 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b 13 | ) 14 | 15 | require ( 16 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 17 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 18 | golang.org/x/text v0.11.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 2 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 3 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 9 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 10 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 11 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 12 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 17 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 18 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 19 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 20 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= 24 | github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 27 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 28 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 29 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 30 | github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= 31 | github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 32 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= 33 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 34 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 35 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 38 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 39 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 40 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/hashicorp/go-version" 12 | "github.com/zclconf/go-cty/cty" 13 | ) 14 | 15 | // MetadataFunctionsFormatVersionConstraints defines the versions of the JSON 16 | // metadata functions format that are supported by this package. 17 | var MetadataFunctionsFormatVersionConstraints = "~> 1.0" 18 | 19 | // MetadataFunctions is the top-level object returned when exporting function 20 | // signatures 21 | type MetadataFunctions struct { 22 | // The version of the format. This should always match the 23 | // MetadataFunctionsFormatVersionConstraints in this package, else 24 | // unmarshaling will fail. 25 | FormatVersion string `json:"format_version"` 26 | 27 | // The signatures of the functions available in a Terraform version. 28 | Signatures map[string]*FunctionSignature `json:"function_signatures,omitempty"` 29 | } 30 | 31 | // Validate checks to ensure that MetadataFunctions is present, and the 32 | // version matches the version supported by this library. 33 | func (f *MetadataFunctions) Validate() error { 34 | if f == nil { 35 | return errors.New("metadata functions data is nil") 36 | } 37 | 38 | if f.FormatVersion == "" { 39 | return errors.New("unexpected metadata functions data, format version is missing") 40 | } 41 | 42 | constraint, err := version.NewConstraint(MetadataFunctionsFormatVersionConstraints) 43 | if err != nil { 44 | return fmt.Errorf("invalid version constraint: %w", err) 45 | } 46 | 47 | version, err := version.NewVersion(f.FormatVersion) 48 | if err != nil { 49 | return fmt.Errorf("invalid format version %q: %w", f.FormatVersion, err) 50 | } 51 | 52 | if !constraint.Check(version) { 53 | return fmt.Errorf("unsupported metadata functions format version: %q does not satisfy %q", 54 | version, constraint) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (f *MetadataFunctions) UnmarshalJSON(b []byte) error { 61 | type rawFunctions MetadataFunctions 62 | var functions rawFunctions 63 | 64 | err := json.Unmarshal(b, &functions) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | *f = *(*MetadataFunctions)(&functions) 70 | 71 | return f.Validate() 72 | } 73 | 74 | // FunctionSignature represents a function signature. 75 | type FunctionSignature struct { 76 | // Description is an optional human-readable description 77 | // of the function 78 | Description string `json:"description,omitempty"` 79 | 80 | // Summary is an optional shortened description of the function 81 | Summary string `json:"summary,omitempty"` 82 | 83 | // DeprecationMessage is an optional message that indicates that the 84 | // function should be considered deprecated and what actions should be 85 | // performed by the practitioner to handle the deprecation. 86 | DeprecationMessage string `json:"deprecation_message,omitempty"` 87 | 88 | // ReturnType is the ctyjson representation of the function's 89 | // return types based on supplying all parameters using 90 | // dynamic types. Functions can have dynamic return types. 91 | ReturnType cty.Type `json:"return_type"` 92 | 93 | // Parameters describes the function's fixed positional parameters. 94 | Parameters []*FunctionParameter `json:"parameters,omitempty"` 95 | 96 | // VariadicParameter describes the function's variadic 97 | // parameter if it is supported. 98 | VariadicParameter *FunctionParameter `json:"variadic_parameter,omitempty"` 99 | } 100 | 101 | // FunctionParameter represents a parameter to a function. 102 | type FunctionParameter struct { 103 | // Name is an optional name for the argument. 104 | Name string `json:"name,omitempty"` 105 | 106 | // Description is an optional human-readable description 107 | // of the argument 108 | Description string `json:"description,omitempty"` 109 | 110 | // IsNullable is true if null is acceptable value for the argument 111 | IsNullable bool `json:"is_nullable,omitempty"` 112 | 113 | // A type that any argument for this parameter must conform to. 114 | Type cty.Type `json:"type"` 115 | } 116 | -------------------------------------------------------------------------------- /metadata_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestMetadataFunctionsValidate(t *testing.T) { 13 | f, err := os.Open("testdata/basic/functions.json") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer f.Close() 18 | 19 | var functions *MetadataFunctions 20 | if err := json.NewDecoder(f).Decode(&functions); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if err := functions.Validate(); err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | ) 16 | 17 | const testFixtureDir = "testdata" 18 | const testGoldenPlanFileName = "plan.json" 19 | const testGoldenStateFileName = "state.json" 20 | const testGoldenSchemasFileName = "schemas.json" 21 | 22 | func testParse(t *testing.T, filename string, typ reflect.Type) { 23 | entries, err := os.ReadDir(testFixtureDir) 24 | if err != nil { 25 | t.Fatalf("err: %s", err) 26 | } 27 | 28 | for _, e := range entries { 29 | if !e.IsDir() { 30 | continue 31 | } 32 | 33 | t.Run(e.Name(), func(t *testing.T) { 34 | expected, err := os.ReadFile(filepath.Join(testFixtureDir, e.Name(), filename)) 35 | if err != nil { 36 | if os.IsNotExist(err) { 37 | t.Skip(err.Error()) 38 | } 39 | t.Fatal(err) 40 | } 41 | 42 | parsed := reflect.New(typ).Interface() 43 | dec := json.NewDecoder(bytes.NewBuffer(expected)) 44 | dec.DisallowUnknownFields() 45 | if err = dec.Decode(parsed); err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | actual, err := json.Marshal(parsed) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | // Add a newline at the end 55 | actual = append(actual, byte('\n')) 56 | 57 | // TODO: Compare the actual struct instead of byte slice 58 | // because JSON does not guarantee consistent key ordering 59 | 60 | if diff := cmp.Diff(expected, actual); diff != "" { 61 | t.Fatalf("unexpected: %s", diff) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestParsePlan(t *testing.T) { 68 | testParse(t, testGoldenPlanFileName, reflect.TypeOf(Plan{})) 69 | } 70 | 71 | func TestParseSchemas(t *testing.T) { 72 | testParse(t, testGoldenSchemasFileName, reflect.TypeOf(ProviderSchemas{})) 73 | } 74 | 75 | func TestParseState(t *testing.T) { 76 | testParse(t, testGoldenStateFileName, reflect.TypeOf(State{})) 77 | } 78 | 79 | func lineAt(text []byte, offs int) []byte { 80 | i := offs 81 | for i < len(text) && text[i] != '\n' { 82 | i++ 83 | } 84 | return text[offs:i] 85 | } 86 | -------------------------------------------------------------------------------- /plan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestPlanValidate(t *testing.T) { 16 | cases := map[string]struct { 17 | planPath string 18 | }{ 19 | "basic plan": { 20 | planPath: "testdata/basic/plan.json", 21 | }, 22 | "plan with identity": { 23 | planPath: "testdata/identity/plan.json", 24 | }, 25 | } 26 | 27 | for tn, tc := range cases { 28 | t.Run(tn, func(t *testing.T) { 29 | f, err := os.Open(tc.planPath) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer f.Close() 34 | 35 | var plan *Plan 36 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err := plan.Validate(); err != nil { 41 | t.Fatal(err) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestPlan_015(t *testing.T) { 48 | f, err := os.Open("testdata/basic/plan-0.15.json") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | defer f.Close() 53 | 54 | var plan *Plan 55 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if err := plan.Validate(); err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | expectedChange := &Change{ 64 | Actions: Actions{"create"}, 65 | After: map[string]interface{}{"ami": "boop"}, 66 | AfterUnknown: map[string]interface{}{"id": true}, 67 | BeforeSensitive: false, 68 | AfterSensitive: map[string]interface{}{"ami": true}, 69 | } 70 | if diff := cmp.Diff(expectedChange, plan.ResourceChanges[0].Change); diff != "" { 71 | t.Fatalf("unexpected change: %s", diff) 72 | } 73 | 74 | expectedVariable := map[string]*ConfigVariable{ 75 | "test_var": { 76 | Default: "boop", 77 | Sensitive: true, 78 | }, 79 | } 80 | if diff := cmp.Diff(expectedVariable, plan.Config.RootModule.Variables); diff != "" { 81 | t.Fatalf("unexpected variables: %s", diff) 82 | } 83 | } 84 | 85 | func TestPlan_withChecks(t *testing.T) { 86 | f, err := os.Open("testdata/has_checks/plan.json") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | defer f.Close() 91 | 92 | var plan *Plan 93 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | if err := plan.Validate(); err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | if len(plan.Checks) == 0 { 102 | t.Fatal("expected checks to not be empty") 103 | } 104 | 105 | for _, c := range plan.Checks { 106 | for _, instance := range c.Instances { 107 | k := reflect.TypeOf(instance.Address.InstanceKey).Kind() 108 | switch k { 109 | case reflect.Int, reflect.Float32, reflect.Float64, reflect.String: 110 | t.Log("instance key is a valid type") 111 | default: 112 | t.Fatalf("unexpected type %s, expected string or int", k.String()) 113 | } 114 | } 115 | } 116 | } 117 | 118 | func TestPlan_movedBlock(t *testing.T) { 119 | f, err := os.Open("testdata/moved_block/plan.json") 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | defer f.Close() 124 | 125 | var plan *Plan 126 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | if err := plan.Validate(); err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | if plan.ResourceChanges[0].PreviousAddress != "random_id.test" { 135 | t.Fatalf("unexpected previous address %s, expected is random_id.test", plan.ResourceChanges[0].PreviousAddress) 136 | } 137 | } 138 | 139 | func TestPlan_UnmarshalJSON(t *testing.T) { 140 | t.Parallel() 141 | 142 | b, err := os.ReadFile("testdata/numerics/plan.json") 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | testCases := map[string]struct { 148 | useJSONNumber bool 149 | expected any 150 | }{ 151 | "float64": { 152 | expected: 1.23, 153 | }, 154 | "json-number": { 155 | useJSONNumber: true, 156 | expected: json.Number("1.23"), 157 | }, 158 | } 159 | 160 | for name, testCase := range testCases { 161 | name, testCase := name, testCase 162 | 163 | t.Run(name, func(t *testing.T) { 164 | t.Parallel() 165 | 166 | plan := &Plan{} 167 | 168 | plan.UseJSONNumber(testCase.useJSONNumber) 169 | 170 | err = plan.UnmarshalJSON(b) 171 | 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | 176 | after, ok := plan.ResourceChanges[0].Change.After.(map[string]any) 177 | 178 | if !ok { 179 | t.Fatal("plan.ResourceChanges[0].Change.After cannot be asserted as map[string]any") 180 | } 181 | 182 | attr, ok := after["configurable_attribute"] 183 | 184 | if !ok { 185 | t.Fatal("configurable attribute not found") 186 | } 187 | 188 | if diff := cmp.Diff(attr, testCase.expected); diff != "" { 189 | t.Errorf("unexpected difference: %s", diff) 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /sanitize/copy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "reflect" 8 | 9 | tfjson "github.com/hashicorp/terraform-json" 10 | "github.com/mitchellh/copystructure" 11 | ) 12 | 13 | // copyStructureCopy is an internal function that wraps copystructure.Copy with 14 | // a shallow copier for unknown values. 15 | // 16 | // Performing the shallow copy of the unknown values is important 17 | // here, as unknown values are parsed in with the main terraform-json 18 | // package as singletons, and must continue to be comparable. 19 | func copyStructureCopy(v interface{}) (interface{}, error) { 20 | c := ©structure.Config{ 21 | ShallowCopiers: map[reflect.Type]struct{}{ 22 | reflect.TypeOf(tfjson.UnknownConstantValue): struct{}{}, 23 | }, 24 | } 25 | 26 | return c.Copy(v) 27 | } 28 | 29 | // copyChange copies a Change value and returns the copy. 30 | func copyChange(old *tfjson.Change) (*tfjson.Change, error) { 31 | c, err := copyStructureCopy(old) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return c.(*tfjson.Change), nil 37 | } 38 | 39 | // copyPlan copies a Plan value and returns the copy. 40 | func copyPlan(old *tfjson.Plan) (*tfjson.Plan, error) { 41 | c, err := copyStructureCopy(old) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return c.(*tfjson.Plan), nil 47 | } 48 | 49 | // copyPlanVariable copies a PlanVariable value and returns the copy. 50 | func copyPlanVariable(old *tfjson.PlanVariable) (*tfjson.PlanVariable, error) { 51 | c, err := copyStructureCopy(old) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return c.(*tfjson.PlanVariable), nil 57 | } 58 | 59 | // copyStateResource copies a StateResource value and returns the copy. 60 | func copyStateResource(old *tfjson.StateResource) (*tfjson.StateResource, error) { 61 | c, err := copyStructureCopy(old) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return c.(*tfjson.StateResource), nil 67 | } 68 | 69 | // copyStateOutput copies a StateOutput value and returns the copy. 70 | func copyStateOutputs(old map[string]*tfjson.StateOutput) (map[string]*tfjson.StateOutput, error) { 71 | c, err := copystructure.Copy(old) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return c.(map[string]*tfjson.StateOutput), nil 77 | } 78 | -------------------------------------------------------------------------------- /sanitize/copy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "testing" 8 | 9 | tfjson "github.com/hashicorp/terraform-json" 10 | ) 11 | 12 | func TestCopyStructureCopy(t *testing.T) { 13 | in := tfjson.UnknownConstantValue 14 | out, err := copyStructureCopy(in) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if in != out { 20 | t.Fatal("did not shallow copy") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sanitize/sanitize_change.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | tfjson "github.com/hashicorp/terraform-json" 8 | ) 9 | 10 | // SanitizeChange traverses a Change and replaces all values at 11 | // the particular locations marked by BeforeSensitive AfterSensitive 12 | // with the value supplied as replaceWith. 13 | // 14 | // A new change is issued. 15 | func SanitizeChange(old *tfjson.Change, replaceWith interface{}) (*tfjson.Change, error) { 16 | result, err := copyChange(old) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | result.Before = sanitizeChangeValue(result.Before, result.BeforeSensitive, replaceWith) 22 | result.After = sanitizeChangeValue(result.After, result.AfterSensitive, replaceWith) 23 | 24 | return result, nil 25 | } 26 | 27 | func sanitizeChangeValue(old, sensitive, replaceWith interface{}) interface{} { 28 | // Only expect deep types that we would normally see in JSON, so 29 | // arrays and objects. 30 | switch x := old.(type) { 31 | case []interface{}: 32 | if filterSlice, ok := sensitive.([]interface{}); ok { 33 | for i := range filterSlice { 34 | if i >= len(x) { 35 | break 36 | } 37 | 38 | x[i] = sanitizeChangeValue(x[i], filterSlice[i], replaceWith) 39 | } 40 | } 41 | case map[string]interface{}: 42 | if filterMap, ok := sensitive.(map[string]interface{}); ok { 43 | for filterKey := range filterMap { 44 | if value, ok := x[filterKey]; ok { 45 | x[filterKey] = sanitizeChangeValue(value, filterMap[filterKey], replaceWith) 46 | } 47 | } 48 | } 49 | } 50 | 51 | if shouldFilter, ok := sensitive.(bool); ok && shouldFilter { 52 | return replaceWith 53 | } 54 | 55 | return old 56 | } 57 | -------------------------------------------------------------------------------- /sanitize/sanitize_change_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | tfjson "github.com/hashicorp/terraform-json" 11 | ) 12 | 13 | type testChangeCase struct { 14 | name string 15 | old *tfjson.Change 16 | expected *tfjson.Change 17 | } 18 | 19 | func changeCases() []testChangeCase { 20 | return []testChangeCase{ 21 | { 22 | name: "basic", 23 | old: &tfjson.Change{ 24 | Before: map[string]interface{}{ 25 | "foo": map[string]interface{}{"a": "foo"}, 26 | "bar": map[string]interface{}{"a": "foo"}, 27 | "baz": map[string]interface{}{"a": "foo"}, 28 | "qux": map[string]interface{}{ 29 | "a": map[string]interface{}{ 30 | "b": "foo", 31 | }, 32 | "c": "bar", 33 | }, 34 | "quxx": map[string]interface{}{ 35 | "a": map[string]interface{}{ 36 | "b": "foo", 37 | }, 38 | "c": "bar", 39 | }, 40 | }, 41 | After: map[string]interface{}{ 42 | "one": map[string]interface{}{"x": "one"}, 43 | "two": map[string]interface{}{"x": "one"}, 44 | "three": map[string]interface{}{"x": "one"}, 45 | "four": map[string]interface{}{ 46 | "x": map[string]interface{}{ 47 | "y": "one", 48 | }, 49 | "z": "two", 50 | }, 51 | "five": map[string]interface{}{ 52 | "x": map[string]interface{}{ 53 | "y": "one", 54 | }, 55 | "z": "two", 56 | }, 57 | }, 58 | BeforeSensitive: map[string]interface{}{ 59 | "foo": map[string]interface{}{}, 60 | "bar": true, 61 | "baz": map[string]interface{}{"a": true}, 62 | "qux": map[string]interface{}{}, 63 | "quxx": map[string]interface{}{"c": true}, 64 | }, 65 | AfterSensitive: map[string]interface{}{ 66 | "one": map[string]interface{}{}, 67 | "two": true, 68 | "three": map[string]interface{}{"x": true}, 69 | "four": map[string]interface{}{}, 70 | "five": map[string]interface{}{"z": true}, 71 | }, 72 | }, 73 | expected: &tfjson.Change{ 74 | Before: map[string]interface{}{ 75 | "foo": map[string]interface{}{"a": "foo"}, 76 | "bar": DefaultSensitiveValue, 77 | "baz": map[string]interface{}{"a": DefaultSensitiveValue}, 78 | "qux": map[string]interface{}{ 79 | "a": map[string]interface{}{ 80 | "b": "foo", 81 | }, 82 | "c": "bar", 83 | }, 84 | "quxx": map[string]interface{}{ 85 | "a": map[string]interface{}{ 86 | "b": "foo", 87 | }, 88 | "c": DefaultSensitiveValue, 89 | }, 90 | }, 91 | After: map[string]interface{}{ 92 | "one": map[string]interface{}{"x": "one"}, 93 | "two": DefaultSensitiveValue, 94 | "three": map[string]interface{}{"x": DefaultSensitiveValue}, 95 | "four": map[string]interface{}{ 96 | "x": map[string]interface{}{ 97 | "y": "one", 98 | }, 99 | "z": "two", 100 | }, 101 | "five": map[string]interface{}{ 102 | "x": map[string]interface{}{ 103 | "y": "one", 104 | }, 105 | "z": DefaultSensitiveValue, 106 | }, 107 | }, 108 | BeforeSensitive: map[string]interface{}{ 109 | "foo": map[string]interface{}{}, 110 | "bar": true, 111 | "baz": map[string]interface{}{"a": true}, 112 | "qux": map[string]interface{}{}, 113 | "quxx": map[string]interface{}{"c": true}, 114 | }, 115 | AfterSensitive: map[string]interface{}{ 116 | "one": map[string]interface{}{}, 117 | "two": true, 118 | "three": map[string]interface{}{"x": true}, 119 | "four": map[string]interface{}{}, 120 | "five": map[string]interface{}{"z": true}, 121 | }, 122 | }, 123 | }, 124 | } 125 | } 126 | 127 | func TestSanitizeChange(t *testing.T) { 128 | for i, tc := range changeCases() { 129 | tc := tc 130 | t.Run(tc.name, func(t *testing.T) { 131 | actual, err := SanitizeChange(tc.old, DefaultSensitiveValue) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | 136 | if diff := cmp.Diff(tc.expected, actual); diff != "" { 137 | t.Errorf("SanitizeChange() mismatch (-expected +actual):\n%s", diff) 138 | } 139 | 140 | if diff := cmp.Diff(changeCases()[i].old, tc.old); diff != "" { 141 | t.Errorf("SanitizeChange() altered original (-expected +actual):\n%s", diff) 142 | } 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /sanitize/sanitize_plan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "errors" 8 | 9 | tfjson "github.com/hashicorp/terraform-json" 10 | ) 11 | 12 | const DefaultSensitiveValue = "REDACTED_SENSITIVE" 13 | 14 | var NilPlanError = errors.New("nil plan supplied") 15 | 16 | // SanitizePlan sanitizes the entirety of a Plan, replacing sensitive 17 | // values with the default value in DefaultSensitiveValue. 18 | // 19 | // See SanitizePlanWithValue for full detail on the where replacement 20 | // takes place. 21 | func SanitizePlan(old *tfjson.Plan) (*tfjson.Plan, error) { 22 | return SanitizePlanWithValue(old, DefaultSensitiveValue) 23 | } 24 | 25 | // SanitizePlanWithValue sanitizes the entirety of a Plan to the best 26 | // of its ability, depending on the provided metadata on sensitive 27 | // values. These are found in: 28 | // 29 | // * ResourceChanges: Sanitized based on BeforeSensitive and 30 | // AfterSensitive fields. 31 | // 32 | // * Variables: Based on variable config data found in the root 33 | // module of the Config. 34 | // 35 | // * PlannedValues: Sanitized based on the values found in 36 | // AfterSensitive in ResourceChanges. Outputs are sanitized 37 | // according to the appropriate sensitivity flags provided for the 38 | // output. 39 | // 40 | // * PriorState: Sanitized based on the values found in 41 | // BeforeSensitive in ResourceChanges. Outputs are sanitized according 42 | // to the appropriate sensitivity flags provided for the output. 43 | // 44 | // * OutputChanges: Sanitized based on the values found in 45 | // BeforeSensitive and AfterSensitive. This generally means that 46 | // any sensitive output will have OutputChange fully obfuscated as 47 | // the BeforeSensitive and AfterSensitive in outputs are opaquely the 48 | // same. 49 | // 50 | // Sensitive values are replaced with the value supplied with 51 | // replaceWith. A copy of the Plan is returned. 52 | func SanitizePlanWithValue(old *tfjson.Plan, replaceWith interface{}) (*tfjson.Plan, error) { 53 | if old == nil { 54 | return nil, NilPlanError 55 | } 56 | 57 | result, err := copyPlan(old) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // Sanitize ResourceChanges 63 | for i := range result.ResourceChanges { 64 | result.ResourceChanges[i].Change, err = SanitizeChange(result.ResourceChanges[i].Change, replaceWith) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } 69 | 70 | // Sanitize Variables 71 | result.Variables, err = SanitizePlanVariables(result.Variables, result.Config.RootModule.Variables, replaceWith) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | // Sanitize PlannedValues 77 | result.PlannedValues.RootModule, err = SanitizeStateModule( 78 | result.PlannedValues.RootModule, 79 | result.ResourceChanges, 80 | SanitizeStateModuleChangeModeAfter, 81 | replaceWith) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | result.PlannedValues.Outputs, err = SanitizeStateOutputs(result.PlannedValues.Outputs, replaceWith) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | // Sanitize PriorState 92 | if result.PriorState != nil { 93 | result.PriorState.Values.RootModule, err = SanitizeStateModule( 94 | result.PriorState.Values.RootModule, 95 | result.ResourceChanges, 96 | SanitizeStateModuleChangeModeBefore, 97 | replaceWith) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | result.PriorState.Values.Outputs, err = SanitizeStateOutputs(result.PriorState.Values.Outputs, replaceWith) 103 | if err != nil { 104 | return nil, err 105 | } 106 | } 107 | 108 | // Sanitize OutputChanges 109 | for k := range result.OutputChanges { 110 | result.OutputChanges[k], err = SanitizeChange(result.OutputChanges[k], replaceWith) 111 | if err != nil { 112 | return nil, err 113 | } 114 | } 115 | 116 | return result, nil 117 | } 118 | -------------------------------------------------------------------------------- /sanitize/sanitize_plan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | 13 | tfjson "github.com/hashicorp/terraform-json" 14 | "github.com/sebdah/goldie" 15 | ) 16 | 17 | const testDataDir = "testdata" 18 | 19 | func TestSanitizePlanGolden(t *testing.T) { 20 | cases, err := goldenCases() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | for _, tc := range cases { 26 | t.Run(tc.Name(), testSanitizePlanGoldenEntry(tc)) 27 | } 28 | } 29 | 30 | func testSanitizePlanGoldenEntry(c testGoldenCase) func(t *testing.T) { 31 | return func(t *testing.T) { 32 | p := new(tfjson.Plan) 33 | err := json.Unmarshal(c.InputData, p) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | p, err = SanitizePlan(p) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | goldie.AssertJson(t, c.Name(), p) 44 | } 45 | } 46 | 47 | type testGoldenCase struct { 48 | FileName string 49 | InputData []byte 50 | } 51 | 52 | func (c *testGoldenCase) Name() string { 53 | return strings.TrimSuffix(c.FileName, filepath.Ext(c.FileName)) 54 | } 55 | 56 | func goldenCases() ([]testGoldenCase, error) { 57 | d, err := os.Open(testDataDir) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | entries, err := d.ReadDir(0) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | result := make([]testGoldenCase, 0) 68 | for _, e := range entries { 69 | if !e.Type().IsRegular() || !strings.HasSuffix(e.Name(), ".json") { 70 | continue 71 | } 72 | 73 | data, err := os.ReadFile(filepath.Join(testDataDir, e.Name())) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | result = append(result, testGoldenCase{ 79 | FileName: e.Name(), 80 | InputData: data, 81 | }) 82 | } 83 | 84 | return result, err 85 | } 86 | 87 | func init() { 88 | goldie.FixtureDir = testDataDir 89 | } 90 | -------------------------------------------------------------------------------- /sanitize/sanitize_plan_variables.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | tfjson "github.com/hashicorp/terraform-json" 8 | ) 9 | 10 | // SanitizePlanVariables traverses a map of PlanVariable and replaces 11 | // any sensitive values with the value supplied in replaceWith. 12 | // configs should be the map of ConfigVariables from the root module 13 | // (so Plan.Config.RootModule.Variables). 14 | // 15 | // A new copy of the PlanVariable map is returned. 16 | func SanitizePlanVariables( 17 | old map[string]*tfjson.PlanVariable, 18 | configs map[string]*tfjson.ConfigVariable, 19 | replaceWith interface{}, 20 | ) (map[string]*tfjson.PlanVariable, error) { 21 | result := make(map[string]*tfjson.PlanVariable, len(old)) 22 | for k := range old { 23 | v, err := sanitizeVariable(old[k], configs[k], replaceWith) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | result[k] = v 29 | } 30 | 31 | return result, nil 32 | } 33 | 34 | func sanitizeVariable( 35 | old *tfjson.PlanVariable, 36 | config *tfjson.ConfigVariable, 37 | replaceWith interface{}, 38 | ) (*tfjson.PlanVariable, error) { 39 | result, err := copyPlanVariable(old) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | if config != nil && config.Sensitive { 45 | result.Value = replaceWith 46 | } 47 | 48 | return result, nil 49 | } 50 | -------------------------------------------------------------------------------- /sanitize/sanitize_plan_variables_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | tfjson "github.com/hashicorp/terraform-json" 11 | ) 12 | 13 | type testVariablesCase struct { 14 | name string 15 | old map[string]*tfjson.PlanVariable 16 | configs map[string]*tfjson.ConfigVariable 17 | expected map[string]*tfjson.PlanVariable 18 | } 19 | 20 | func variablesCases() []testVariablesCase { 21 | return []testVariablesCase{ 22 | { 23 | name: "basic", 24 | old: map[string]*tfjson.PlanVariable{ 25 | "foo": &tfjson.PlanVariable{ 26 | Value: "test-foo", 27 | }, 28 | "bar": &tfjson.PlanVariable{ 29 | Value: "test-bar", 30 | }, 31 | }, 32 | configs: map[string]*tfjson.ConfigVariable{ 33 | "foo": &tfjson.ConfigVariable{ 34 | Sensitive: false, 35 | }, 36 | "bar": &tfjson.ConfigVariable{ 37 | Sensitive: true, 38 | }, 39 | }, 40 | expected: map[string]*tfjson.PlanVariable{ 41 | "foo": &tfjson.PlanVariable{ 42 | Value: "test-foo", 43 | }, 44 | "bar": &tfjson.PlanVariable{ 45 | Value: DefaultSensitiveValue, 46 | }, 47 | }, 48 | }, 49 | } 50 | } 51 | 52 | func TestSanitizePlanVariables(t *testing.T) { 53 | for i, tc := range variablesCases() { 54 | tc := tc 55 | t.Run(tc.name, func(t *testing.T) { 56 | actual, err := SanitizePlanVariables(tc.old, tc.configs, DefaultSensitiveValue) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | if diff := cmp.Diff(tc.expected, actual); diff != "" { 62 | t.Errorf("SanitizePlanVariables() mismatch (-expected +actual):\n%s", diff) 63 | } 64 | 65 | if diff := cmp.Diff(variablesCases()[i].old, tc.old); diff != "" { 66 | t.Errorf("SanitizePlanVariables() altered original (-expected +actual):\n%s", diff) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sanitize/sanitize_state.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "fmt" 8 | 9 | tfjson "github.com/hashicorp/terraform-json" 10 | ) 11 | 12 | type SanitizeStateModuleChangeMode string 13 | 14 | const ( 15 | SanitizeStateModuleChangeModeBefore SanitizeStateModuleChangeMode = "before_sensitive" 16 | SanitizeStateModuleChangeModeAfter SanitizeStateModuleChangeMode = "after_sensitive" 17 | ) 18 | 19 | // SanitizeStateModule traverses a StateModule, consulting the 20 | // supplied ResourceChange set for resources to determine whether or 21 | // not particular values should be obfuscated. 22 | // 23 | // Use mode to supply the SanitizeStateModuleChangeMode that 24 | // represents what sensitive field should be consulted to determine 25 | // whether or not the value should be obfuscated: 26 | // 27 | // * SanitizeStateModuleChangeModeBefore for before_sensitive 28 | // * SanitizeStateModuleChangeModeAfter for after_sensitive 29 | // 30 | // Sensitive values are replaced with the supplied replaceWith value. 31 | // A new state module tree is issued. 32 | func SanitizeStateModule( 33 | old *tfjson.StateModule, 34 | resourceChanges []*tfjson.ResourceChange, 35 | mode SanitizeStateModuleChangeMode, 36 | replaceWith interface{}, 37 | ) (*tfjson.StateModule, error) { 38 | result := &tfjson.StateModule{ 39 | Resources: make([]*tfjson.StateResource, len(old.Resources)), 40 | Address: old.Address, 41 | ChildModules: make([]*tfjson.StateModule, len(old.ChildModules)), 42 | } 43 | 44 | for i := range old.Resources { 45 | var err error 46 | result.Resources[i], err = sanitizeStateResource( 47 | old.Resources[i], 48 | findResourceChange(resourceChanges, old.Resources[i].Address), 49 | mode, 50 | replaceWith, 51 | ) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | 57 | for i := range old.ChildModules { 58 | var err error 59 | result.ChildModules[i], err = SanitizeStateModule( 60 | old.ChildModules[i], 61 | resourceChanges, 62 | mode, 63 | replaceWith, 64 | ) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } 69 | 70 | return result, nil 71 | } 72 | 73 | func sanitizeStateResource( 74 | old *tfjson.StateResource, 75 | rc *tfjson.ResourceChange, 76 | mode SanitizeStateModuleChangeMode, 77 | replaceWith interface{}, 78 | ) (*tfjson.StateResource, error) { 79 | result, err := copyStateResource(old) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | if rc == nil { 85 | return result, nil 86 | } 87 | 88 | var sensitive interface{} 89 | switch mode { 90 | case SanitizeStateModuleChangeModeBefore: 91 | sensitive = rc.Change.BeforeSensitive 92 | 93 | case SanitizeStateModuleChangeModeAfter: 94 | sensitive = rc.Change.AfterSensitive 95 | 96 | default: 97 | panic(fmt.Sprintf("invalid change mode %q", mode)) 98 | } 99 | 100 | // We can re-use sanitizeChangeValue here to do the sanitization. 101 | result.AttributeValues = sanitizeChangeValue(result.AttributeValues, sensitive, replaceWith).(map[string]interface{}) 102 | return result, nil 103 | } 104 | 105 | func findResourceChange(resourceChanges []*tfjson.ResourceChange, addr string) *tfjson.ResourceChange { 106 | // Linear search here, unfortunately :P 107 | for _, rc := range resourceChanges { 108 | if rc.Address == addr { 109 | return rc 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | // SanitizeStateOutputs scans the supplied map of StateOutputs and 117 | // replaces any values of outputs marked as Sensitive with the value 118 | // supplied in replaceWith. 119 | // 120 | // A new copy of StateOutputs is returned. 121 | func SanitizeStateOutputs(old map[string]*tfjson.StateOutput, replaceWith interface{}) (map[string]*tfjson.StateOutput, error) { 122 | result, err := copyStateOutputs(old) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | for k := range result { 128 | if result[k].Sensitive { 129 | result[k].Value = replaceWith 130 | } 131 | } 132 | 133 | return result, nil 134 | } 135 | -------------------------------------------------------------------------------- /sanitize/sanitize_state_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sanitize 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | tfjson "github.com/hashicorp/terraform-json" 11 | "github.com/zclconf/go-cty-debug/ctydebug" 12 | ) 13 | 14 | type testStateCase struct { 15 | name string 16 | old *tfjson.StateModule 17 | resourceChanges []*tfjson.ResourceChange 18 | mode SanitizeStateModuleChangeMode 19 | expected *tfjson.StateModule 20 | } 21 | 22 | func stateCases() []testStateCase { 23 | return []testStateCase{ 24 | { 25 | name: "before", 26 | old: &tfjson.StateModule{ 27 | Resources: []*tfjson.StateResource{ 28 | { 29 | Address: "null_resource.foo", 30 | AttributeValues: map[string]interface{}{ 31 | "foo": "bar", 32 | "baz": "qux", 33 | }, 34 | }, 35 | }, 36 | Address: "", 37 | ChildModules: []*tfjson.StateModule{ 38 | &tfjson.StateModule{ 39 | Resources: []*tfjson.StateResource{ 40 | { 41 | Address: "module.foo.null_resource.bar", 42 | AttributeValues: map[string]interface{}{ 43 | "a": "b", 44 | "c": "d", 45 | }, 46 | }, 47 | }, 48 | Address: "module.foo", 49 | ChildModules: []*tfjson.StateModule{}, 50 | }, 51 | }, 52 | }, 53 | resourceChanges: []*tfjson.ResourceChange{ 54 | { 55 | Address: "null_resource.foo", 56 | Change: &tfjson.Change{ 57 | BeforeSensitive: map[string]interface{}{ 58 | "baz": true, 59 | }, 60 | AfterSensitive: map[string]interface{}{ 61 | "foo": true, 62 | }, 63 | }, 64 | }, 65 | { 66 | Address: "module.foo.null_resource.bar", 67 | Change: &tfjson.Change{ 68 | BeforeSensitive: map[string]interface{}{ 69 | "a": true, 70 | }, 71 | AfterSensitive: map[string]interface{}{ 72 | "c": true, 73 | }, 74 | }, 75 | }, 76 | }, 77 | mode: SanitizeStateModuleChangeModeBefore, 78 | expected: &tfjson.StateModule{ 79 | Resources: []*tfjson.StateResource{ 80 | { 81 | Address: "null_resource.foo", 82 | AttributeValues: map[string]interface{}{ 83 | "foo": "bar", 84 | "baz": DefaultSensitiveValue, 85 | }, 86 | }, 87 | }, 88 | Address: "", 89 | ChildModules: []*tfjson.StateModule{ 90 | &tfjson.StateModule{ 91 | Resources: []*tfjson.StateResource{ 92 | { 93 | Address: "module.foo.null_resource.bar", 94 | AttributeValues: map[string]interface{}{ 95 | "a": DefaultSensitiveValue, 96 | "c": "d", 97 | }, 98 | }, 99 | }, 100 | Address: "module.foo", 101 | ChildModules: []*tfjson.StateModule{}, 102 | }, 103 | }, 104 | }, 105 | }, 106 | { 107 | name: "after", 108 | old: &tfjson.StateModule{ 109 | Resources: []*tfjson.StateResource{ 110 | { 111 | Address: "null_resource.foo", 112 | AttributeValues: map[string]interface{}{ 113 | "foo": "bar", 114 | "baz": "qux", 115 | }, 116 | }, 117 | }, 118 | Address: "", 119 | ChildModules: []*tfjson.StateModule{ 120 | &tfjson.StateModule{ 121 | Resources: []*tfjson.StateResource{ 122 | { 123 | Address: "module.foo.null_resource.bar", 124 | AttributeValues: map[string]interface{}{ 125 | "a": "b", 126 | "c": "d", 127 | }, 128 | }, 129 | }, 130 | Address: "module.foo", 131 | ChildModules: []*tfjson.StateModule{}, 132 | }, 133 | }, 134 | }, 135 | resourceChanges: []*tfjson.ResourceChange{ 136 | { 137 | Address: "null_resource.foo", 138 | Change: &tfjson.Change{ 139 | BeforeSensitive: map[string]interface{}{ 140 | "baz": true, 141 | }, 142 | AfterSensitive: map[string]interface{}{ 143 | "foo": true, 144 | }, 145 | }, 146 | }, 147 | { 148 | Address: "module.foo.null_resource.bar", 149 | Change: &tfjson.Change{ 150 | BeforeSensitive: map[string]interface{}{ 151 | "a": true, 152 | }, 153 | AfterSensitive: map[string]interface{}{ 154 | "c": true, 155 | }, 156 | }, 157 | }, 158 | }, 159 | mode: SanitizeStateModuleChangeModeAfter, 160 | expected: &tfjson.StateModule{ 161 | Resources: []*tfjson.StateResource{ 162 | { 163 | Address: "null_resource.foo", 164 | AttributeValues: map[string]interface{}{ 165 | "foo": DefaultSensitiveValue, 166 | "baz": "qux", 167 | }, 168 | }, 169 | }, 170 | Address: "", 171 | ChildModules: []*tfjson.StateModule{ 172 | &tfjson.StateModule{ 173 | Resources: []*tfjson.StateResource{ 174 | { 175 | Address: "module.foo.null_resource.bar", 176 | AttributeValues: map[string]interface{}{ 177 | "a": "b", 178 | "c": DefaultSensitiveValue, 179 | }, 180 | }, 181 | }, 182 | Address: "module.foo", 183 | ChildModules: []*tfjson.StateModule{}, 184 | }, 185 | }, 186 | }, 187 | }, 188 | } 189 | } 190 | 191 | func TestSanitizeStateModule(t *testing.T) { 192 | for i, tc := range stateCases() { 193 | tc := tc 194 | t.Run(tc.name, func(t *testing.T) { 195 | actual, err := SanitizeStateModule(tc.old, tc.resourceChanges, tc.mode, DefaultSensitiveValue) 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | if diff := cmp.Diff(tc.expected, actual); diff != "" { 201 | t.Errorf("SanitizeStateModule() mismatch (-expected +actual):\n%s", diff) 202 | } 203 | 204 | if diff := cmp.Diff(stateCases()[i].old, tc.old); diff != "" { 205 | t.Errorf("SanitizeStateModule() altered original (-expected +actual):\n%s", diff) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | type testOutputCase struct { 212 | name string 213 | old map[string]*tfjson.StateOutput 214 | expected map[string]*tfjson.StateOutput 215 | } 216 | 217 | func outputCases() []testOutputCase { 218 | return []testOutputCase{ 219 | { 220 | name: "basic", 221 | old: map[string]*tfjson.StateOutput{ 222 | "foo": { 223 | Value: "bar", 224 | }, 225 | "a": { 226 | Value: "b", 227 | Sensitive: true, 228 | }, 229 | }, 230 | expected: map[string]*tfjson.StateOutput{ 231 | "foo": { 232 | Value: "bar", 233 | }, 234 | "a": { 235 | Value: DefaultSensitiveValue, 236 | Sensitive: true, 237 | }, 238 | }, 239 | }, 240 | } 241 | } 242 | 243 | func TestSanitizeStateOutputs(t *testing.T) { 244 | for i, tc := range outputCases() { 245 | tc := tc 246 | t.Run(tc.name, func(t *testing.T) { 247 | actual, err := SanitizeStateOutputs(tc.old, DefaultSensitiveValue) 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | 252 | if diff := cmp.Diff(tc.expected, actual, ctydebug.CmpOptions); diff != "" { 253 | t.Errorf("SanitizeStateOutputs() mismatch (-expected +actual):\n%s", diff) 254 | } 255 | 256 | if diff := cmp.Diff(outputCases()[i].old, tc.old, ctydebug.CmpOptions); diff != "" { 257 | t.Errorf("SanitizeStateOutputs() altered original (-expected +actual):\n%s", diff) 258 | } 259 | }) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /schemas_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestProviderSchemasValidate(t *testing.T) { 13 | cases := map[string]struct { 14 | testDataPath string 15 | }{ 16 | "a basic provider schema is validated": { 17 | testDataPath: "testdata/basic/schemas.json", 18 | }, 19 | "a provider schema including functions is validated": { 20 | testDataPath: "testdata/functions/schemas.json", 21 | }, 22 | "a provider schema including ephemeral resources is validated": { 23 | testDataPath: "testdata/ephemeral_resources/schemas.json", 24 | }, 25 | "a provider schema including a resource with write-only attribute(s) is validated": { 26 | testDataPath: "testdata/write_only_attribute_on_resource/schemas.json", 27 | }, 28 | "a provider schema including resource identity schemas is validated": { 29 | testDataPath: "testdata/identity/schemas.json", 30 | }, 31 | } 32 | 33 | for tn, tc := range cases { 34 | t.Run(tn, func(t *testing.T) { 35 | f, err := os.Open(tc.testDataPath) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | defer f.Close() 40 | 41 | var schemas *ProviderSchemas 42 | if err := json.NewDecoder(f).Decode(&schemas); err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if err := schemas.Validate(); err != nil { 47 | t.Fatal(err) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | // TestProviderSchemas_writeOnlyAttribute asserts that write-only attributes in a resource in a 54 | // provider schema JSON file are marked as WriteOnly once decoded into a ProviderSchemas struct 55 | func TestProviderSchemas_writeOnlyAttribute(t *testing.T) { 56 | f, err := os.Open("testdata/write_only_attribute_on_resource/schemas.json") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer f.Close() 61 | 62 | var schemas *ProviderSchemas 63 | if err := json.NewDecoder(f).Decode(&schemas); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | resourceSchema := schemas.Schemas["terraform.io/builtin/terraform"].ResourceSchemas["terraform_example"] 68 | if resourceSchema.Block.Attributes["wo_attr"].WriteOnly != true { 69 | t.Fatal("expected terraform_example.wo_attr to be marked as write-only") 70 | } 71 | if resourceSchema.Block.Attributes["foo"].WriteOnly != false { 72 | t.Fatal("expected terraform_example.foo to not be marked as write-only") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | 12 | "github.com/hashicorp/go-version" 13 | "github.com/zclconf/go-cty/cty" 14 | ) 15 | 16 | // StateFormatVersionConstraints defines the versions of the JSON state format 17 | // that are supported by this package. 18 | var StateFormatVersionConstraints = ">= 0.1, < 2.0" 19 | 20 | // State is the top-level representation of a Terraform state. 21 | type State struct { 22 | // useJSONNumber opts into the behavior of calling 23 | // json.Decoder.UseNumber prior to decoding the state, which turns 24 | // numbers into json.Numbers instead of float64s. Set it using 25 | // State.UseJSONNumber. 26 | useJSONNumber bool 27 | 28 | // The version of the state format. This should always match the 29 | // StateFormatVersion constant in this package, or else am 30 | // unmarshal will be unstable. 31 | FormatVersion string `json:"format_version,omitempty"` 32 | 33 | // The Terraform version used to make the state. 34 | TerraformVersion string `json:"terraform_version,omitempty"` 35 | 36 | // The values that make up the state. 37 | Values *StateValues `json:"values,omitempty"` 38 | 39 | // Checks contains the results of any conditional checks when Values was 40 | // last updated. 41 | Checks []CheckResultStatic `json:"checks,omitempty"` 42 | } 43 | 44 | // UseJSONNumber controls whether the State will be decoded using the 45 | // json.Number behavior or the float64 behavior. When b is true, the State will 46 | // represent numbers in StateOutputs as json.Numbers. When b is false, the 47 | // State will represent numbers in StateOutputs as float64s. 48 | func (s *State) UseJSONNumber(b bool) { 49 | s.useJSONNumber = b 50 | } 51 | 52 | // Validate checks to ensure that the state is present, and the 53 | // version matches the version supported by this library. 54 | func (s *State) Validate() error { 55 | if s == nil { 56 | return errors.New("state is nil") 57 | } 58 | 59 | if s.FormatVersion == "" { 60 | return errors.New("unexpected state input, format version is missing") 61 | } 62 | 63 | constraint, err := version.NewConstraint(StateFormatVersionConstraints) 64 | if err != nil { 65 | return fmt.Errorf("invalid version constraint: %w", err) 66 | } 67 | 68 | version, err := version.NewVersion(s.FormatVersion) 69 | if err != nil { 70 | return fmt.Errorf("invalid format version %q: %w", s.FormatVersion, err) 71 | } 72 | 73 | if !constraint.Check(version) { 74 | return fmt.Errorf("unsupported state format version: %q does not satisfy %q", 75 | version, constraint) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (s *State) UnmarshalJSON(b []byte) error { 82 | type rawState State 83 | var state rawState 84 | 85 | dec := json.NewDecoder(bytes.NewReader(b)) 86 | if s.useJSONNumber { 87 | dec.UseNumber() 88 | } 89 | err := dec.Decode(&state) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | *s = *(*State)(&state) 95 | 96 | return s.Validate() 97 | } 98 | 99 | // StateValues is the common representation of resolved values for both the 100 | // prior state (which is always complete) and the planned new state. 101 | type StateValues struct { 102 | // The Outputs for this common state representation. 103 | Outputs map[string]*StateOutput `json:"outputs,omitempty"` 104 | 105 | // The root module in this state representation. 106 | RootModule *StateModule `json:"root_module,omitempty"` 107 | } 108 | 109 | // StateModule is the representation of a module in the common state 110 | // representation. This can be the root module or a child module. 111 | type StateModule struct { 112 | // All resources or data sources within this module. 113 | Resources []*StateResource `json:"resources,omitempty"` 114 | 115 | // The absolute module address, omitted for the root module. 116 | Address string `json:"address,omitempty"` 117 | 118 | // Any child modules within this module. 119 | ChildModules []*StateModule `json:"child_modules,omitempty"` 120 | } 121 | 122 | // StateResource is the representation of a resource in the common 123 | // state representation. 124 | type StateResource struct { 125 | // The absolute resource address. 126 | Address string `json:"address,omitempty"` 127 | 128 | // The resource mode. 129 | Mode ResourceMode `json:"mode,omitempty"` 130 | 131 | // The resource type, example: "aws_instance" for aws_instance.foo. 132 | Type string `json:"type,omitempty"` 133 | 134 | // The resource name, example: "foo" for aws_instance.foo. 135 | Name string `json:"name,omitempty"` 136 | 137 | // The instance key for any resources that have been created using 138 | // "count" or "for_each". If neither of these apply the key will be 139 | // empty. 140 | // 141 | // This value can be either an integer (int) or a string. 142 | Index interface{} `json:"index,omitempty"` 143 | 144 | // The name of the provider this resource belongs to. This allows 145 | // the provider to be interpreted unambiguously in the unusual 146 | // situation where a provider offers a resource type whose name 147 | // does not start with its own name, such as the "googlebeta" 148 | // provider offering "google_compute_instance". 149 | ProviderName string `json:"provider_name,omitempty"` 150 | 151 | // The version of the resource type schema the "values" property 152 | // conforms to. 153 | SchemaVersion uint64 `json:"schema_version,"` 154 | 155 | // The JSON representation of the attribute values of the resource, 156 | // whose structure depends on the resource type schema. Any unknown 157 | // values are omitted or set to null, making them indistinguishable 158 | // from absent values. 159 | AttributeValues map[string]interface{} `json:"values,omitempty"` 160 | 161 | // The JSON representation of the sensitivity of the resource's 162 | // attribute values. Only attributes which are sensitive 163 | // are included in this structure. 164 | SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` 165 | 166 | // The addresses of the resources that this resource depends on. 167 | DependsOn []string `json:"depends_on,omitempty"` 168 | 169 | // If true, the resource has been marked as tainted and will be 170 | // re-created on the next update. 171 | Tainted bool `json:"tainted,omitempty"` 172 | 173 | // DeposedKey is set if the resource instance has been marked Deposed and 174 | // will be destroyed on the next apply. 175 | DeposedKey string `json:"deposed_key,omitempty"` 176 | 177 | // The version of the resource identity schema the "identity" property 178 | // conforms to. 179 | IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"` 180 | 181 | // The JSON representation of the resource identity, whose structure 182 | // depends on the resource identity schema. 183 | IdentityValues map[string]interface{} `json:"identity,omitempty"` 184 | } 185 | 186 | // StateOutput represents an output value in a common state 187 | // representation. 188 | type StateOutput struct { 189 | // Whether or not the output was marked as sensitive. 190 | Sensitive bool `json:"sensitive"` 191 | 192 | // The value of the output. 193 | Value interface{} `json:"value,omitempty"` 194 | 195 | // The type of the output. 196 | Type cty.Type `json:"type,omitempty"` 197 | } 198 | 199 | // jsonStateOutput describes an output value in a middle-step internal 200 | // representation before marshalled into a more useful StateOutput with cty.Type. 201 | // 202 | // This avoid panic on marshalling cty.NilType (from cty upstream) 203 | // which the default Go marshaller cannot ignore because it's a 204 | // not nil-able struct. 205 | type jsonStateOutput struct { 206 | Sensitive bool `json:"sensitive"` 207 | Value interface{} `json:"value,omitempty"` 208 | Type json.RawMessage `json:"type,omitempty"` 209 | } 210 | 211 | func (so *StateOutput) MarshalJSON() ([]byte, error) { 212 | jsonSa := &jsonStateOutput{ 213 | Sensitive: so.Sensitive, 214 | Value: so.Value, 215 | } 216 | if so.Type != cty.NilType { 217 | outputType, _ := so.Type.MarshalJSON() 218 | jsonSa.Type = outputType 219 | } 220 | return json.Marshal(jsonSa) 221 | } 222 | -------------------------------------------------------------------------------- /state_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestStateValidate_raw(t *testing.T) { 14 | cases := map[string]struct { 15 | statePath string 16 | }{ 17 | "basic state": { 18 | statePath: "testdata/no_changes/state.json", 19 | }, 20 | "state with identity": { 21 | statePath: "testdata/identity/state.json", 22 | }, 23 | } 24 | 25 | for tn, tc := range cases { 26 | t.Run(tn, func(t *testing.T) { 27 | f, err := os.Open(tc.statePath) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer f.Close() 32 | 33 | var state State 34 | if err := json.NewDecoder(f).Decode(&state); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | if err := state.Validate(); err != nil { 39 | t.Fatal(err) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestStateUnmarshal_valid(t *testing.T) { 46 | f, err := os.Open("testdata/no_changes/state.json") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer f.Close() 51 | 52 | b, err := io.ReadAll(f) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | var state State 58 | err = json.Unmarshal(b, &state) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | 64 | func TestStateUnmarshal_internalState(t *testing.T) { 65 | f, err := os.Open("testdata/no_changes/terraform.tfstate") 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | defer f.Close() 70 | 71 | b, err := io.ReadAll(f) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | var state State 77 | err = json.Unmarshal(b, &state) 78 | if err == nil { 79 | t.Fatal("expected unmarshal to fail") 80 | } 81 | got := err.Error() 82 | expected := "unexpected state input, format version is missing" 83 | if expected != got { 84 | t.Fatalf("error mismatch.\nexpected: %q\ngot: %q\n", expected, got) 85 | } 86 | } 87 | 88 | func TestStateValidate_fromPlan(t *testing.T) { 89 | f, err := os.Open("testdata/no_changes/plan.json") 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | defer f.Close() 94 | 95 | var plan *Plan 96 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if err := plan.PriorState.Validate(); err != nil { 101 | t.Fatal(err) 102 | } 103 | } 104 | 105 | func TestStateValidate_fromPlan110(t *testing.T) { 106 | f, err := os.Open("testdata/110_basic/plan.json") 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | defer f.Close() 111 | 112 | var plan *Plan 113 | if err := json.NewDecoder(f).Decode(&plan); err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | if err := plan.PriorState.Validate(); err != nil { 118 | t.Fatal(err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /testdata/013_module_depends_on/main.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "vancluever/module/null" 3 | 4 | depends_on = [ 5 | null_resource.bar 6 | ] 7 | } 8 | 9 | resource "null_resource" "bar" {} -------------------------------------------------------------------------------- /testdata/013_module_depends_on/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.13.1","planned_values":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":null}}],"child_modules":[{"resources":[{"address":"module.foo.data.null_data_source.data","mode":"data","type":"null_data_source","name":"data","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"inputs":{"key":"foo"}}},{"address":"module.foo.null_resource.resource","mode":"managed","type":"null_resource","name":"resource","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"number":"one"}}}],"address":"module.foo"}]}},"resource_changes":[{"address":"module.foo.data.null_data_source.data","module_address":"module.foo","mode":"data","type":"null_data_source","name":"data","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["read"],"before":null,"after":{"inputs":{"key":"foo"}},"after_unknown":{"has_computed_default":true,"id":true,"inputs":{},"outputs":true,"random":true}}},{"address":"module.foo.null_resource.resource","module_address":"module.foo","mode":"managed","type":"null_resource","name":"resource","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":{"number":"one"}},"after_unknown":{"id":true,"triggers":{}}}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}}],"configuration":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","schema_version":0}],"module_calls":{"foo":{"source":"vancluever/module/null","module":{"outputs":{"null_data_source_id":{"expression":{"references":["data.null_data_source.data"]},"description":"The `id` of the `null_data_source` data source in this module."},"null_resource_id":{"expression":{"references":["null_resource.resource"]},"description":"The `id` of the `null_resource` resource in this module."}},"resources":[{"address":"null_resource.resource","mode":"managed","type":"null_resource","name":"resource","provider_config_key":"foo:null","expressions":{"triggers":{"references":["var.trigger"]}},"schema_version":0},{"address":"data.null_data_source.data","mode":"data","type":"null_data_source","name":"data","provider_config_key":"foo:null","expressions":{"inputs":{"references":["var.input"]}},"schema_version":0}],"variables":{"input":{"default":"foo","description":"The input value for the `null_data_source` data source in this module."},"trigger":{"default":"one","description":"The trigger value for the `null_resource` resource in this module."}}},"depends_on":["null_resource.bar"]}}}}} 2 | -------------------------------------------------------------------------------- /testdata/013_module_depends_on/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","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_kind":"plain","optional":true,"computed":true},"triggers":{"type":["map","string"],"description_kind":"plain","optional":true}},"description_kind":"plain"}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","description_kind":"plain","optional":true,"computed":true},"id":{"type":"string","description_kind":"plain","optional":true,"computed":true},"inputs":{"type":["map","string"],"description_kind":"plain","optional":true},"outputs":{"type":["map","string"],"description_kind":"plain","computed":true},"random":{"type":"string","description_kind":"plain","computed":true}},"description_kind":"plain"}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/110_basic/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.58.0" 6 | hashes = [ 7 | "h1:kx8p0xV/mGa+7vnGOk02AenU/ZKxF08yT/0jynZgSQI=", 8 | "h1:m1OyrF89jldDJ+SUdCeU/TuHHoeWTERHPrjNpNsgP5s=", 9 | "h1:wEiBCAJc++7RYfo4Yg6l+tW47ml+XZozU+vy5zso+3s=", 10 | "zh:47118f7a69c8962d74ff67be3c13cac76ba99c900941eed4750c347252a0986d", 11 | "zh:4f3322399bef25cdf43e62a00248d991c5e1ef9d744e9b7cd2f8c398bb60bef3", 12 | "zh:55c2223b32f2f114ce5a0ccadbf1ffb8a892bb3862365a0dc4c8ea1248ff1814", 13 | "zh:5a79cfdfde4c132544173fde8d8672659fd6e1bebdacdac856f103915238b1e4", 14 | "zh:67fbddb0c184c25283fa66d4299b458079a3ed2d82189dc3338fda796b6be14c", 15 | "zh:78ad660c2b965e5817c06aadf267d311e2320e7f6d3faad8061c1e29ef4b60ef", 16 | "zh:a20eace4dbb0fb168f6152599b7ce75a9cf419c85f9ae26d8463baf14abe7ddc", 17 | "zh:a31a99c9b924d98e9a5bf173ee8fe3aee73c9a92cd63b2b10c6f350d8b5c5e49", 18 | "zh:a9210d9b0b65324bbf712318044e89f959a2f9aaff6f69c0a91bb294aff7101e", 19 | "zh:aa885bc6647d218c8b73fcfe33cfee20c11d88078655aa1e194b013fde17a204", 20 | "zh:c5a5122e5dcb1c9e9d8fde2c994bab7ba08d63904cabff61129e139cbf97332f", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/null" { 25 | version = "3.1.0" 26 | hashes = [ 27 | "h1:SFT7X3zY18CLWjoH2GfQyapxsRv6GDKsy9cF1aRwncc=", 28 | "h1:vpC6bgUQoJ0znqIKVFevOdq+YQw42bRq0u+H3nto8nA=", 29 | "h1:xhbHC6in3nQryvTQBWKxebi3inG5OCgHgc4fRxL0ymc=", 30 | "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", 31 | "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", 32 | "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", 33 | "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", 34 | "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", 35 | "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", 36 | "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", 37 | "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", 38 | "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", 39 | "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", 40 | "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/110_basic/foo/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | configuration_aliases = [null.aliased] 6 | } 7 | } 8 | } 9 | 10 | variable "bar" { 11 | type = string 12 | } 13 | 14 | variable "one" { 15 | type = string 16 | } 17 | 18 | resource "null_resource" "foo" { 19 | triggers = { 20 | foo = "bar" 21 | } 22 | } 23 | 24 | resource "null_resource" "aliased" { 25 | provider = null.aliased 26 | } 27 | 28 | output "foo" { 29 | value = "bar" 30 | } 31 | -------------------------------------------------------------------------------- /testdata/110_basic/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | 7 | providers = { 8 | null.aliased = null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /testdata/110_basic/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/110_basic/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","terraform_version":"1.1.0-dev","variables":{"foo":{"value":"bar"},"map":{"value":{"foo":"bar","number":42}},"number":{"value":42}},"planned_values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated":{"sensitive":false},"interpolated_deep":{"sensitive":false},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced":{"sensitive":false},"referenced_deep":{"sensitive":false},"string":{"sensitive":false,"value":"foo"}},"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"child_modules":[{"resources":[{"address":"module.foo.null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":null},"sensitive_values":{}},{"address":"module.foo.null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"address":"module.foo"}]}},"resource_changes":[{"address":"module.foo.null_resource.aliased","module_address":"module.foo","mode":"managed","type":"null_resource","name":"aliased","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.foo.null_resource.foo","module_address":"module.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"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":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":false,"after_sensitive":{"triggers":{}}}}],"output_changes":{"foo":{"actions":["create"],"before":null,"after":"bar","after_unknown":false,"before_sensitive":true,"after_sensitive":true},"interpolated":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"interpolated_deep":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"list":{"actions":["create"],"before":null,"after":["foo","bar"],"after_unknown":false,"before_sensitive":false,"after_sensitive":false},"map":{"actions":["create"],"before":null,"after":{"foo":"bar","number":42},"after_unknown":false,"before_sensitive":false,"after_sensitive":false},"referenced":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"referenced_deep":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"string":{"actions":["create"],"before":null,"after":"foo","after_unknown":false,"before_sensitive":false,"after_sensitive":false}},"prior_state":{"format_version":"1.0","terraform_version":"1.1.0","values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"string":{"sensitive":false,"value":"foo"}},"root_module":{}}},"configuration":{"provider_config":{"aws":{"name":"aws","expressions":{"region":{"constant_value":"us-west-2"}}},"aws.east":{"name":"aws","alias":"east","expressions":{"region":{"constant_value":"us-east-1"}}},"module.foo:null":{"name":"null","module_address":"module.foo"},"null":{"name":"null"}},"root_module":{"outputs":{"foo":{"sensitive":true,"expression":{"constant_value":"bar"}},"interpolated":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"interpolated_deep":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"list":{"expression":{"constant_value":["foo","bar"]}},"map":{"expression":{"constant_value":{"foo":"bar","number":42}}},"referenced":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"referenced_deep":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"string":{"expression":{"constant_value":"foo"}}},"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo.id","null_resource.foo"]}},"schema_version":0},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo.id","null_resource.foo"]}},"schema_version":0,"count_expression":{"constant_value":3}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","provisioners":[{"type":"local-exec","expressions":{"command":{"constant_value":"echo hello"}}}],"expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0}],"module_calls":{"foo":{"source":"./foo","expressions":{"bar":{"constant_value":"baz"},"one":{"constant_value":"two"}},"module":{"outputs":{"foo":{"expression":{"constant_value":"bar"}}},"resources":[{"address":"null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_config_key":"foo:null.aliased","schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"foo:null","expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0}],"variables":{"bar":{},"one":{}}}}},"variables":{"foo":{"default":"bar","description":"foobar"},"map":{"default":{"foo":"bar","number":42}},"number":{"default":42}}}}} 2 | -------------------------------------------------------------------------------- /testdata/110_basic/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | } 6 | 7 | aws = { 8 | source = "hashicorp/aws" 9 | } 10 | } 11 | } 12 | 13 | provider "aws" { 14 | region = "us-west-2" 15 | } 16 | 17 | provider "aws" { 18 | alias = "east" 19 | region = "us-east-1" 20 | } 21 | -------------------------------------------------------------------------------- /testdata/110_basic/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | resource "null_resource" "baz" { 18 | count = 3 19 | 20 | triggers = { 21 | foo_id = "${null_resource.foo.id}" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/110_basic/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/foo/main.tf: -------------------------------------------------------------------------------- 1 | variable "bar" { 2 | type = string 3 | } 4 | 5 | variable "one" { 6 | type = string 7 | } 8 | 9 | terraform { 10 | required_providers { 11 | null = { 12 | source = "hashicorp/null" 13 | configuration_aliases = [null.aliased] 14 | } 15 | } 16 | } 17 | 18 | resource "null_resource" "foo" { 19 | triggers = { 20 | foo = "bar" 21 | } 22 | } 23 | 24 | resource "null_resource" "aliased" { 25 | provider = null.aliased 26 | } 27 | 28 | output "foo" { 29 | value = "bar" 30 | } 31 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | 7 | providers = { 8 | null.aliased = null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.2","terraform_version":"1.1.0-dev","variables":{"foo":{"value":"bar"},"map":{"value":{"foo":"bar","number":42}},"number":{"value":42}},"planned_values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated":{"sensitive":false},"interpolated_deep":{"sensitive":false},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced":{"sensitive":false},"referenced_deep":{"sensitive":false},"string":{"sensitive":false,"value":"foo"}},"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"sensitive_values":{"triggers":{}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"child_modules":[{"resources":[{"address":"module.foo.null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":null},"sensitive_values":{}},{"address":"module.foo.null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"address":"module.foo"}]}},"resource_changes":[{"address":"module.foo.null_resource.aliased","module_address":"module.foo","mode":"managed","type":"null_resource","name":"aliased","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"module.foo.null_resource.foo","module_address":"module.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true},"before_sensitive":false,"after_sensitive":{"triggers":{}}}},{"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":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":false,"after_sensitive":{"triggers":{}}}}],"output_changes":{"foo":{"actions":["create"],"before":null,"after":"bar","after_unknown":false,"before_sensitive":true,"after_sensitive":true},"interpolated":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"interpolated_deep":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"list":{"actions":["create"],"before":null,"after":["foo","bar"],"after_unknown":false,"before_sensitive":false,"after_sensitive":false},"map":{"actions":["create"],"before":null,"after":{"foo":"bar","number":42},"after_unknown":false,"before_sensitive":false,"after_sensitive":false},"referenced":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"referenced_deep":{"actions":["create"],"before":null,"after_unknown":true,"before_sensitive":false,"after_sensitive":false},"string":{"actions":["create"],"before":null,"after":"foo","after_unknown":false,"before_sensitive":false,"after_sensitive":false}},"prior_state":{"format_version":"0.2","terraform_version":"1.1.0","values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"string":{"sensitive":false,"value":"foo"}},"root_module":{}}},"configuration":{"provider_config":{"aws":{"name":"aws","expressions":{"region":{"constant_value":"us-west-2"}}},"aws.east":{"name":"aws","alias":"east","expressions":{"region":{"constant_value":"us-east-1"}}},"module.foo:null":{"name":"null","module_address":"module.foo"},"null":{"name":"null"}},"root_module":{"outputs":{"foo":{"sensitive":true,"expression":{"constant_value":"bar"}},"interpolated":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"interpolated_deep":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"list":{"expression":{"constant_value":["foo","bar"]}},"map":{"expression":{"constant_value":{"foo":"bar","number":42}}},"referenced":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"referenced_deep":{"expression":{"references":["null_resource.foo.id","null_resource.foo"]}},"string":{"expression":{"constant_value":"foo"}}},"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo.id","null_resource.foo"]}},"schema_version":0},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo.id","null_resource.foo"]}},"schema_version":0,"count_expression":{"constant_value":3}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","provisioners":[{"type":"local-exec","expressions":{"command":{"constant_value":"echo hello"}}}],"expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0}],"module_calls":{"foo":{"source":"./foo","expressions":{"bar":{"constant_value":"baz"},"one":{"constant_value":"two"}},"module":{"outputs":{"foo":{"expression":{"constant_value":"bar"}}},"resources":[{"address":"null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_config_key":"foo:null.aliased","schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"foo:null","expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0}],"variables":{"bar":{},"one":{}}}}},"variables":{"foo":{"default":"bar","description":"foobar"},"map":{"default":{"foo":"bar","number":42}},"number":{"default":42}}}}} 2 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | } 6 | } 7 | } 8 | provider "aws" { 9 | region = "us-west-2" 10 | } 11 | 12 | provider "aws" { 13 | alias = "east" 14 | region = "us-east-1" 15 | } 16 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | resource "null_resource" "baz" { 18 | count = 3 19 | 20 | triggers = { 21 | foo_id = "${null_resource.foo.id}" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/state.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.2","terraform_version":"1.1.0","values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated":{"sensitive":false,"value":"7914344597979736746"},"interpolated_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":"7914344597979736746"},"number":42}},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced":{"sensitive":false,"value":"7914344597979736746"},"referenced_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":"7914344597979736746"},"number":42}},"string":{"sensitive":false,"value":"foo"}},"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"346205755248437621","triggers":{"foo_id":"7914344597979736746"}},"sensitive_values":{"triggers":{}},"depends_on":["null_resource.foo"]},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"8125409023088484730","triggers":{"foo_id":"7914344597979736746"}},"sensitive_values":{"triggers":{}},"depends_on":["null_resource.foo"]},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"4055263173373670778","triggers":{"foo_id":"7914344597979736746"}},"sensitive_values":{"triggers":{}},"depends_on":["null_resource.foo"]},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"7188960170253950057","triggers":{"foo_id":"7914344597979736746"}},"sensitive_values":{"triggers":{}},"depends_on":["null_resource.foo"]},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"7914344597979736746","triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"child_modules":[{"resources":[{"address":"module.foo.null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"8187802253811954885","triggers":null},"sensitive_values":{}},{"address":"module.foo.null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"712346592830392361","triggers":{"foo":"bar"}},"sensitive_values":{"triggers":{}}}],"address":"module.foo"}]}}} 2 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.1.0", 4 | "serial": 8, 5 | "lineage": "fa0debfe-854c-b885-a355-bbb09fa86dde", 6 | "outputs": { 7 | "foo": { 8 | "value": "bar", 9 | "type": "string", 10 | "sensitive": true 11 | }, 12 | "interpolated": { 13 | "value": "7914344597979736746", 14 | "type": "string" 15 | }, 16 | "interpolated_deep": { 17 | "value": { 18 | "foo": "bar", 19 | "map": { 20 | "bar": "baz", 21 | "id": "7914344597979736746" 22 | }, 23 | "number": 42 24 | }, 25 | "type": [ 26 | "object", 27 | { 28 | "foo": "string", 29 | "map": [ 30 | "object", 31 | { 32 | "bar": "string", 33 | "id": "string" 34 | } 35 | ], 36 | "number": "number" 37 | } 38 | ] 39 | }, 40 | "list": { 41 | "value": [ 42 | "foo", 43 | "bar" 44 | ], 45 | "type": [ 46 | "tuple", 47 | [ 48 | "string", 49 | "string" 50 | ] 51 | ] 52 | }, 53 | "map": { 54 | "value": { 55 | "foo": "bar", 56 | "number": 42 57 | }, 58 | "type": [ 59 | "object", 60 | { 61 | "foo": "string", 62 | "number": "number" 63 | } 64 | ] 65 | }, 66 | "referenced": { 67 | "value": "7914344597979736746", 68 | "type": "string" 69 | }, 70 | "referenced_deep": { 71 | "value": { 72 | "foo": "bar", 73 | "map": { 74 | "bar": "baz", 75 | "id": "7914344597979736746" 76 | }, 77 | "number": 42 78 | }, 79 | "type": [ 80 | "object", 81 | { 82 | "foo": "string", 83 | "map": [ 84 | "object", 85 | { 86 | "bar": "string", 87 | "id": "string" 88 | } 89 | ], 90 | "number": "number" 91 | } 92 | ] 93 | }, 94 | "string": { 95 | "value": "foo", 96 | "type": "string" 97 | } 98 | }, 99 | "resources": [ 100 | { 101 | "mode": "managed", 102 | "type": "null_resource", 103 | "name": "bar", 104 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 105 | "instances": [ 106 | { 107 | "schema_version": 0, 108 | "attributes": { 109 | "id": "346205755248437621", 110 | "triggers": { 111 | "foo_id": "7914344597979736746" 112 | } 113 | }, 114 | "sensitive_attributes": [], 115 | "private": "bnVsbA==", 116 | "dependencies": [ 117 | "null_resource.foo" 118 | ] 119 | } 120 | ] 121 | }, 122 | { 123 | "mode": "managed", 124 | "type": "null_resource", 125 | "name": "baz", 126 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 127 | "instances": [ 128 | { 129 | "index_key": 0, 130 | "schema_version": 0, 131 | "attributes": { 132 | "id": "8125409023088484730", 133 | "triggers": { 134 | "foo_id": "7914344597979736746" 135 | } 136 | }, 137 | "sensitive_attributes": [], 138 | "private": "bnVsbA==", 139 | "dependencies": [ 140 | "null_resource.foo" 141 | ] 142 | }, 143 | { 144 | "index_key": 1, 145 | "schema_version": 0, 146 | "attributes": { 147 | "id": "4055263173373670778", 148 | "triggers": { 149 | "foo_id": "7914344597979736746" 150 | } 151 | }, 152 | "sensitive_attributes": [], 153 | "private": "bnVsbA==", 154 | "dependencies": [ 155 | "null_resource.foo" 156 | ] 157 | }, 158 | { 159 | "index_key": 2, 160 | "schema_version": 0, 161 | "attributes": { 162 | "id": "7188960170253950057", 163 | "triggers": { 164 | "foo_id": "7914344597979736746" 165 | } 166 | }, 167 | "sensitive_attributes": [], 168 | "private": "bnVsbA==", 169 | "dependencies": [ 170 | "null_resource.foo" 171 | ] 172 | } 173 | ] 174 | }, 175 | { 176 | "mode": "managed", 177 | "type": "null_resource", 178 | "name": "foo", 179 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 180 | "instances": [ 181 | { 182 | "schema_version": 0, 183 | "attributes": { 184 | "id": "7914344597979736746", 185 | "triggers": { 186 | "foo": "bar" 187 | } 188 | }, 189 | "sensitive_attributes": [], 190 | "private": "bnVsbA==" 191 | } 192 | ] 193 | }, 194 | { 195 | "module": "module.foo", 196 | "mode": "managed", 197 | "type": "null_resource", 198 | "name": "aliased", 199 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 200 | "instances": [ 201 | { 202 | "schema_version": 0, 203 | "attributes": { 204 | "id": "8187802253811954885", 205 | "triggers": null 206 | }, 207 | "sensitive_attributes": [], 208 | "private": "bnVsbA==" 209 | } 210 | ] 211 | }, 212 | { 213 | "module": "module.foo", 214 | "mode": "managed", 215 | "type": "null_resource", 216 | "name": "foo", 217 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 218 | "instances": [ 219 | { 220 | "schema_version": 0, 221 | "attributes": { 222 | "id": "712346592830392361", 223 | "triggers": { 224 | "foo": "bar" 225 | } 226 | }, 227 | "sensitive_attributes": [], 228 | "private": "bnVsbA==" 229 | } 230 | ] 231 | } 232 | ] 233 | } 234 | -------------------------------------------------------------------------------- /testdata/110_sensitive_values/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/120_basic/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.13.0" 6 | hashes = [ 7 | "h1:3reDkc0ysWUlUFPFL/mLZDlPNODFDMF8s66DEEx4xIY=", 8 | "zh:215226bc0372077d2ae6dba4e2f08f6361f8e4953d20bc4c682d40fdf5002544", 9 | "zh:42777cbdc046181986c0260ea17027ef1364c31d73a57eb0ab539f6e1a3e0780", 10 | "zh:78079d2f5fc35f3c43eb2a131cb49c2c77ddd04943bca97080f33355808d39cc", 11 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 12 | "zh:9c0404a044eae741f10f3d217dc28658e0f04082963918913b024d3305c11e79", 13 | "zh:a1b5a53f60d4f7bff1cc84180fef6205c95b8793741dbc8c0564a6200424ca73", 14 | "zh:ba6711064a855ddb55924342b70667e9bed660bde8552dc0bde4b7f8947a2ec4", 15 | "zh:d0f77ed514d54f7380d7e1ef585d853f50f1bee381d6abbf3a68429b68de6045", 16 | "zh:d5c454d2ac9aed01ae00c477192c93d54c8362357a87684a3171055dcec25f44", 17 | "zh:dfd381ed7da945cb85b99df843ee7eab339dd1799fa70d1ad3e94331605aad01", 18 | "zh:eb6dc84414714f61b9de0ac190c69f598af9b16d144a44f573df484c06c8d4ef", 19 | "zh:f02e79599af3f8f63e4b885c5715be3a4060cbf98eb4bf46d616aa0d9f2b5cd3", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/null" { 24 | version = "3.1.1" 25 | hashes = [ 26 | "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", 27 | "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", 28 | "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", 29 | "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", 30 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 31 | "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", 32 | "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", 33 | "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", 34 | "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", 35 | "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", 36 | "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", 37 | "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", 38 | "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /testdata/120_basic/foo/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | configuration_aliases = [null.aliased] 6 | } 7 | } 8 | } 9 | 10 | variable "bar" { 11 | type = string 12 | } 13 | 14 | variable "one" { 15 | type = string 16 | } 17 | 18 | resource "null_resource" "foo" { 19 | triggers = { 20 | foo = "bar" 21 | } 22 | } 23 | 24 | resource "null_resource" "aliased" { 25 | provider = null.aliased 26 | } 27 | 28 | output "foo" { 29 | value = "bar" 30 | } 31 | -------------------------------------------------------------------------------- /testdata/120_basic/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | 7 | providers = { 8 | null.aliased = null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /testdata/120_basic/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/120_basic/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | } 6 | 7 | aws = { 8 | source = "hashicorp/aws" 9 | } 10 | } 11 | } 12 | 13 | provider "aws" { 14 | region = "us-west-2" 15 | } 16 | 17 | provider "aws" { 18 | alias = "east" 19 | region = "us-east-1" 20 | } 21 | -------------------------------------------------------------------------------- /testdata/120_basic/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | resource "null_resource" "baz" { 18 | count = 3 19 | 20 | triggers = { 21 | foo_id = "${null_resource.foo.id}" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/120_basic/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/Makefile: -------------------------------------------------------------------------------- 1 | AWS_ENV_VARS=AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_REGION 2 | TERRAFORM_DOCKER_CMD=docker run $(foreach var,$(AWS_ENV_VARS),--env $(var)) --rm --workdir "$$(pwd)" --volume "$$(pwd)":"$$(pwd)" 3 | 4 | TERRAFORM_012_VERSION=0.12.11 5 | TERRAFORM012=$(TERRAFORM_DOCKER_CMD) docker.mirror.hashicorp.services/hashicorp/terraform:$(TERRAFORM_012_VERSION) 6 | 7 | TERRAFORM_013_VERSION=0.13.1 8 | TERRAFORM013=$(TERRAFORM_DOCKER_CMD) docker.mirror.hashicorp.services/hashicorp/terraform:$(TERRAFORM_013_VERSION) 9 | 10 | FIXTURES ?= $(shell find * -maxdepth 0 -type d -not -name "013*") 11 | 013FIXTURES ?= $(shell find * -maxdepth 0 -type d -name "013*") 12 | 13 | .PHONY: $(FIXTURES) 14 | $(FIXTURES): 15 | cd $@/ && $(TERRAFORM012) init 16 | cd $@/ && $(TERRAFORM012) plan -out=plan.tfplan 17 | cd $@/ && $(TERRAFORM012) show -json plan.tfplan > plan.json 18 | cd $@/ && $(TERRAFORM012) providers schema -json > schemas.json 19 | 20 | .PHONY: $(013FIXTURES) 21 | $(013FIXTURES): 22 | cd $@/ && $(TERRAFORM013) init 23 | cd $@/ && $(TERRAFORM013) plan -out=plan.tfplan 24 | cd $@/ && $(TERRAFORM013) show -json plan.tfplan > plan.json 25 | cd $@/ && $(TERRAFORM013) providers schema -json > schemas.json 26 | 27 | .PHONY: generate 28 | generate: $(FIXTURES) $(013FIXTURES) 29 | -------------------------------------------------------------------------------- /testdata/basic/foo/main.tf: -------------------------------------------------------------------------------- 1 | variable "bar" { 2 | type = "string" 3 | } 4 | 5 | variable "one" { 6 | type = "string" 7 | } 8 | 9 | provider "null" { 10 | alias = "aliased" 11 | } 12 | 13 | resource "null_resource" "foo" { 14 | triggers = { 15 | foo = "bar" 16 | } 17 | } 18 | 19 | resource "null_resource" "aliased" { 20 | provider = "null.aliased" 21 | } 22 | 23 | output "foo" { 24 | value = "bar" 25 | } 26 | -------------------------------------------------------------------------------- /testdata/basic/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/basic/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/basic/plan-0.15.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "0.1", 3 | "variables": { 4 | "test_var": { 5 | "value": "boop" 6 | } 7 | }, 8 | "planned_values": { 9 | "outputs": { 10 | "test": { 11 | "sensitive": true, 12 | "value": "boop" 13 | } 14 | }, 15 | "root_module": { 16 | "resources": [ 17 | { 18 | "address": "test_instance.test", 19 | "mode": "managed", 20 | "type": "test_instance", 21 | "name": "test", 22 | "provider_name": "registry.terraform.io/hashicorp/test", 23 | "schema_version": 0, 24 | "values": { 25 | "ami": "boop" 26 | } 27 | } 28 | ] 29 | } 30 | }, 31 | "resource_changes": [ 32 | { 33 | "address": "test_instance.test", 34 | "mode": "managed", 35 | "type": "test_instance", 36 | "provider_name": "registry.terraform.io/hashicorp/test", 37 | "name": "test", 38 | "change": { 39 | "actions": [ 40 | "create" 41 | ], 42 | "before": null, 43 | "after": { 44 | "ami": "boop" 45 | }, 46 | "after_unknown": { 47 | "id": true 48 | }, 49 | "after_sensitive": { 50 | "ami": true 51 | }, 52 | "before_sensitive": false 53 | } 54 | } 55 | ], 56 | "output_changes": { 57 | "test": { 58 | "actions": [ 59 | "create" 60 | ], 61 | "before": null, 62 | "after": "boop", 63 | "after_unknown": false, 64 | "before_sensitive": true, 65 | "after_sensitive": true 66 | } 67 | }, 68 | "prior_state": { 69 | "format_version": "0.1", 70 | "values": { 71 | "outputs": { 72 | "test": { 73 | "sensitive": true, 74 | "value": "boop" 75 | } 76 | }, 77 | "root_module": {} 78 | } 79 | }, 80 | "configuration": { 81 | "root_module": { 82 | "outputs": { 83 | "test": { 84 | "expression": { 85 | "references": [ 86 | "test_instance.test" 87 | ] 88 | }, 89 | "sensitive": true 90 | } 91 | }, 92 | "resources": [ 93 | { 94 | "address": "test_instance.test", 95 | "mode": "managed", 96 | "type": "test_instance", 97 | "name": "test", 98 | "provider_config_key": "test", 99 | "schema_version": 0, 100 | "expressions": { 101 | "ami": { 102 | "references": [ 103 | "var.test_var" 104 | ] 105 | } 106 | } 107 | } 108 | ], 109 | "variables": { 110 | "test_var": { 111 | "default": "boop", 112 | "sensitive": true 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /testdata/basic/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","variables":{"foo":{"value":"bar"},"map":{"value":{"foo":"bar","number":42}},"number":{"value":42}},"planned_values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated":{"sensitive":false},"interpolated_deep":{"sensitive":false},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced":{"sensitive":false},"referenced_deep":{"sensitive":false},"string":{"sensitive":false,"value":"foo"}},"root_module":{"resources":[{"address":"data.null_data_source.baz","mode":"data","type":"null_data_source","name":"baz","provider_name":"null","schema_version":0,"values":{"inputs":{}}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"null","schema_version":0},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"null","schema_version":0},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"null","schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"triggers":{"foo":"bar"}}}],"child_modules":[{"resources":[{"address":"module.foo.null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_name":"null.aliased","schema_version":0,"values":{"triggers":null}},{"address":"module.foo.null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"triggers":{"foo":"bar"}}}],"address":"module.foo"}]}},"resource_changes":[{"address":"data.null_data_source.baz","mode":"data","type":"null_data_source","name":"baz","provider_name":"null","change":{"actions":["read"],"before":null,"after":{"inputs":{}},"after_unknown":{"has_computed_default":true,"id":true,"inputs":{"bar_id":true,"foo_id":true},"outputs":true,"random":true}}},{"address":"module.foo.null_resource.aliased","module_address":"module.foo","mode":"managed","type":"null_resource","name":"aliased","provider_name":"null.aliased","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}},{"address":"module.foo.null_resource.foo","module_address":"module.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}}}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true}}},{"address":"null_resource.baz[0]","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true}}},{"address":"null_resource.baz[1]","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true}}},{"address":"null_resource.baz[2]","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"null","change":{"actions":["create"],"before":null,"after":{},"after_unknown":{"id":true,"triggers":true}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"bar"}},"after_unknown":{"id":true,"triggers":{}}}}],"output_changes":{"foo":{"actions":["create"],"before":null,"after":"bar","after_unknown":false},"interpolated":{"actions":["create"],"before":null,"after_unknown":true},"interpolated_deep":{"actions":["create"],"before":null,"after_unknown":true},"list":{"actions":["create"],"before":null,"after":["foo","bar"],"after_unknown":false},"map":{"actions":["create"],"before":null,"after":{"foo":"bar","number":42},"after_unknown":false},"referenced":{"actions":["create"],"before":null,"after_unknown":true},"referenced_deep":{"actions":["create"],"before":null,"after_unknown":true},"string":{"actions":["create"],"before":null,"after":"foo","after_unknown":false}},"prior_state":{"format_version":"0.1","terraform_version":"0.12.11","values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":null},"number":42}},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":null},"number":42}},"string":{"sensitive":false,"value":"foo"}},"root_module":{}}},"configuration":{"provider_config":{"aws":{"name":"aws","expressions":{"region":{"constant_value":"us-west-2"}}},"aws.east":{"name":"aws","alias":"east","expressions":{"region":{"constant_value":"us-east-1"}}},"foo:null.aliased":{"name":"null","alias":"aliased","module_address":"foo"},"null":{"name":"null"}},"root_module":{"outputs":{"foo":{"sensitive":true,"expression":{"constant_value":"bar"}},"interpolated":{"expression":{"references":["null_resource.foo"]}},"interpolated_deep":{"expression":{"references":["null_resource.foo"]}},"list":{"expression":{"constant_value":["foo","bar"]}},"map":{"expression":{"constant_value":{"foo":"bar","number":42}}},"referenced":{"expression":{"references":["null_resource.foo"]}},"referenced_deep":{"expression":{"references":["null_resource.foo"]}},"string":{"expression":{"constant_value":"foo"}}},"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo"]}},"schema_version":0},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_config_key":"null","expressions":{"triggers":{"references":["null_resource.foo"]}},"schema_version":0,"count_expression":{"constant_value":3}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","provisioners":[{"type":"local-exec","expressions":{"command":{"constant_value":"echo hello"}}}],"expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0},{"address":"data.null_data_source.baz","mode":"data","type":"null_data_source","name":"baz","provider_config_key":"null","expressions":{"inputs":{"references":["null_resource.foo","null_resource.bar"]}},"schema_version":0}],"module_calls":{"foo":{"source":"./foo","expressions":{"bar":{"constant_value":"baz"},"one":{"constant_value":"two"}},"module":{"outputs":{"foo":{"expression":{"constant_value":"bar"}}},"resources":[{"address":"null_resource.aliased","mode":"managed","type":"null_resource","name":"aliased","provider_config_key":"foo:null.aliased","schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"foo:null","expressions":{"triggers":{"constant_value":{"foo":"bar"}}},"schema_version":0}],"variables":{"bar":{},"one":{}}}}},"variables":{"foo":{"default":"bar","description":"foobar"},"map":{"default":{"foo":"bar","number":42}},"number":{"default":42}}}}} 2 | -------------------------------------------------------------------------------- /testdata/basic/providers.tf: -------------------------------------------------------------------------------- 1 | provider "null" {} 2 | 3 | provider "aws" { 4 | region = "us-west-2" 5 | } 6 | 7 | provider "aws" { 8 | alias = "east" 9 | region = "us-east-1" 10 | } 11 | -------------------------------------------------------------------------------- /testdata/basic/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | data "null_data_source" "baz" { 18 | inputs = { 19 | foo_id = "${null_resource.foo.id}" 20 | bar_id = "${null_resource.bar.id}" 21 | } 22 | } 23 | 24 | resource "null_resource" "baz" { 25 | count = 3 26 | 27 | triggers = { 28 | foo_id = "${null_resource.foo.id}" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /testdata/basic/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/config_resource_depends_on/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" {} 2 | 3 | resource "null_resource" "bar" { 4 | depends_on = ["null_resource.foo"] 5 | } 6 | -------------------------------------------------------------------------------- /testdata/config_resource_depends_on/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","planned_values":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0,"values":{"triggers":null}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"id":"4379128789376396166","triggers":null}}]}},"resource_changes":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","change":{"actions":["delete","create"],"before":{"id":"8595481736657951026","triggers":{"foo":"4379128789376396166"}},"after":{"triggers":null},"after_unknown":{"id":true}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["no-op"],"before":{"id":"4379128789376396166","triggers":null},"after":{"id":"4379128789376396166","triggers":null},"after_unknown":{}}}],"prior_state":{"format_version":"0.1","terraform_version":"0.12.11","values":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0,"values":{"id":"8595481736657951026","triggers":{"foo":"4379128789376396166"}},"depends_on":["null_resource.foo"]},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"id":"4379128789376396166","triggers":null}}]}}},"configuration":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","schema_version":0,"depends_on":["null_resource.foo"]},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}} 2 | -------------------------------------------------------------------------------- /testdata/config_resource_depends_on/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/config_resource_depends_on/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 3, 5 | "lineage": "af5a607f-9eae-15f8-afd8-9d32a8a9fc24", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "bar", 12 | "provider": "provider.null", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "id": "8595481736657951026", 18 | "triggers": { 19 | "foo": "4379128789376396166" 20 | } 21 | }, 22 | "depends_on": [ 23 | "null_resource.foo" 24 | ] 25 | } 26 | ] 27 | }, 28 | { 29 | "mode": "managed", 30 | "type": "null_resource", 31 | "name": "foo", 32 | "provider": "provider.null", 33 | "instances": [ 34 | { 35 | "schema_version": 0, 36 | "attributes": { 37 | "id": "4379128789376396166", 38 | "triggers": null 39 | } 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /testdata/deep_module/foo/bar/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "baz" {} 2 | -------------------------------------------------------------------------------- /testdata/deep_module/foo/main.tf: -------------------------------------------------------------------------------- 1 | module "bar" { 2 | source = "./bar" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/deep_module/main.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/deep_module/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","planned_values":{"root_module":{"child_modules":[{"address":"module.foo","child_modules":[{"resources":[{"address":"module.foo.module.bar.null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_name":"null","schema_version":0,"values":{"triggers":null}}],"address":"module.foo.module.bar"}]}]}},"resource_changes":[{"address":"module.foo.module.bar.null_resource.baz","module_address":"module.foo.module.bar","mode":"managed","type":"null_resource","name":"baz","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}}],"configuration":{"root_module":{"module_calls":{"foo":{"source":"./foo","module":{"module_calls":{"bar":{"source":"./bar","module":{"resources":[{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_config_key":"bar:null","schema_version":0}]}}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/deep_module/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/ephemeral_resources/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","provider_schemas":{"registry.terraform.io/hashicorp/random":{"provider":{"version":0,"block":{"description_kind":"plain"}},"ephemeral_resource_schemas":{"random_password":{"version":0,"block":{"attributes":{"bcrypt_hash":{"type":"string","description":"A bcrypt hash of the generated random string. **NOTE**: If the generated random string is greater than 72 bytes in length, `bcrypt_hash` will contain a hash of the first 72 bytes.","description_kind":"plain","computed":true,"sensitive":true},"length":{"type":"number","description":"The length of the string desired. The minimum value for length is 1 and, length must also be \u003e= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).","description_kind":"plain","required":true},"lower":{"type":"bool","description":"Include lowercase alphabet characters in the result. Default value is `true`.","description_kind":"plain","optional":true,"computed":true},"min_lower":{"type":"number","description":"Minimum number of lowercase alphabet characters in the result. Default value is `0`.","description_kind":"plain","optional":true,"computed":true},"min_numeric":{"type":"number","description":"Minimum number of numeric characters in the result. Default value is `0`.","description_kind":"plain","optional":true,"computed":true},"min_special":{"type":"number","description":"Minimum number of special characters in the result. Default value is `0`.","description_kind":"plain","optional":true,"computed":true},"min_upper":{"type":"number","description":"Minimum number of uppercase alphabet characters in the result. Default value is `0`.","description_kind":"plain","optional":true,"computed":true},"numeric":{"type":"bool","description":"Include numeric characters in the result. Default value is `true`. If `numeric`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`.","description_kind":"plain","optional":true,"computed":true},"override_special":{"type":"string","description":"Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation.","description_kind":"plain","optional":true},"result":{"type":"string","description":"The generated random string.","description_kind":"plain","computed":true,"sensitive":true},"special":{"type":"bool","description":"Include special characters in the result. These are `!@#$%\u0026*()-_=+[]{}\u003c\u003e:?`. Default value is `true`.","description_kind":"plain","optional":true,"computed":true},"upper":{"type":"bool","description":"Include uppercase alphabet characters in the result. Default value is `true`.","description_kind":"plain","optional":true,"computed":true}},"description_kind":"plain"}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/explicit_null/main.tf: -------------------------------------------------------------------------------- 1 | variable "one" { 2 | type = "string" 3 | } 4 | 5 | resource "null_resource" "foo" { 6 | triggers = { 7 | foo = null 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo = var.one 14 | } 15 | } 16 | 17 | resource "null_resource" "baz" { 18 | triggers = { 19 | foo = var.one 20 | bar = null 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testdata/explicit_null/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","variables":{"one":{"value":"two"}},"planned_values":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0,"values":{"triggers":{"foo":"two"}}},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_name":"null","schema_version":0,"values":{"triggers":{"foo":"two"}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"triggers":null}}]}},"resource_changes":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"two"}},"after_unknown":{"id":true,"triggers":{}}}},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":{"foo":"two"}},"after_unknown":{"id":true,"triggers":{}}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}}],"configuration":{"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","expressions":{"triggers":{"references":["var.one"]}},"schema_version":0},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","provider_config_key":"null","expressions":{"triggers":{"references":["var.one"]}},"schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","expressions":{"triggers":{"constant_value":{"foo":null}}},"schema_version":0}],"variables":{"one":{}}}}} 2 | -------------------------------------------------------------------------------- /testdata/explicit_null/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/explicit_null/terraform.tfvars: -------------------------------------------------------------------------------- 1 | one = "two" 2 | -------------------------------------------------------------------------------- /testdata/functions/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","provider_schemas":{"example":{"provider":{"version":0,"block":{"attributes":{"example":{"type":"string","description_kind":"plain","optional":true}},"description_kind":"plain"}},"resource_schemas":{"framework_example":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"Example identifier","description_kind":"markdown","computed":true}},"description":"Example resource","description_kind":"markdown"}}},"data_source_schemas":{"framework_example":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"Example identifier","description_kind":"markdown","computed":true}},"description":"Example data source","description_kind":"markdown"}}},"functions":{"example":{"description":"Echoes given argument as result","summary":"Example function","return_type":"string","parameters":[{"name":"input","description":"String to echo","type":"string"}]}}}}} 2 | -------------------------------------------------------------------------------- /testdata/has_changes/foo/main.tf: -------------------------------------------------------------------------------- 1 | variable "bar" { 2 | type = "string" 3 | } 4 | 5 | variable "one" { 6 | type = "string" 7 | } 8 | 9 | resource "null_resource" "foo" { 10 | triggers = { 11 | foo = "bar" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/has_changes/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/has_changes/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/has_changes/providers.tf: -------------------------------------------------------------------------------- 1 | provider "null" {} 2 | 3 | provider "aws" { 4 | region = "us-west-2" 5 | } 6 | 7 | provider "aws" { 8 | alias = "east" 9 | region = "us-east-1" 10 | } 11 | -------------------------------------------------------------------------------- /testdata/has_changes/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | data "null_data_source" "baz" { 18 | inputs = { 19 | foo_id = "${null_resource.foo.id}" 20 | bar_id = "${null_resource.bar.id}" 21 | } 22 | } 23 | 24 | resource "null_resource" "baz" { 25 | count = 3 26 | 27 | triggers = { 28 | foo_id = "${null_resource.foo.id}" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /testdata/has_changes/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 8, 5 | "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", 6 | "outputs": { 7 | "foo": { 8 | "value": "bar", 9 | "type": "string", 10 | "sensitive": true 11 | }, 12 | "interpolated": { 13 | "value": "424881806176056736", 14 | "type": "string" 15 | }, 16 | "interpolated_deep": { 17 | "value": { 18 | "foo": "bar", 19 | "map": { 20 | "bar": "baz", 21 | "id": "424881806176056736" 22 | }, 23 | "number": 42 24 | }, 25 | "type": [ 26 | "object", 27 | { 28 | "foo": "string", 29 | "map": [ 30 | "object", 31 | { 32 | "bar": "string", 33 | "id": "string" 34 | } 35 | ], 36 | "number": "number" 37 | } 38 | ] 39 | }, 40 | "list": { 41 | "value": [ 42 | "foo", 43 | "bar" 44 | ], 45 | "type": [ 46 | "tuple", 47 | [ 48 | "string", 49 | "string" 50 | ] 51 | ] 52 | }, 53 | "map": { 54 | "value": { 55 | "foo": "bar", 56 | "number": 42 57 | }, 58 | "type": [ 59 | "object", 60 | { 61 | "foo": "string", 62 | "number": "number" 63 | } 64 | ] 65 | }, 66 | "referenced": { 67 | "value": "424881806176056736", 68 | "type": "string" 69 | }, 70 | "referenced_deep": { 71 | "value": { 72 | "foo": "bar", 73 | "map": { 74 | "bar": "baz", 75 | "id": "424881806176056736" 76 | }, 77 | "number": 42 78 | }, 79 | "type": [ 80 | "object", 81 | { 82 | "foo": "string", 83 | "map": [ 84 | "object", 85 | { 86 | "bar": "string", 87 | "id": "string" 88 | } 89 | ], 90 | "number": "number" 91 | } 92 | ] 93 | }, 94 | "string": { 95 | "value": "foo", 96 | "type": "string" 97 | } 98 | }, 99 | "resources": [ 100 | { 101 | "mode": "data", 102 | "type": "null_data_source", 103 | "name": "baz", 104 | "provider": "provider.null", 105 | "instances": [ 106 | { 107 | "schema_version": 0, 108 | "attributes": { 109 | "has_computed_default": "default", 110 | "id": "static", 111 | "inputs": { 112 | "bar_id": "4347220156304926627", 113 | "foo_id": "424881806176056736" 114 | }, 115 | "outputs": { 116 | "bar_id": "4347220156304926627", 117 | "foo_id": "424881806176056736" 118 | }, 119 | "random": "1951353658349486401" 120 | }, 121 | "depends_on": [ 122 | "null_resource.bar", 123 | "null_resource.foo" 124 | ] 125 | } 126 | ] 127 | }, 128 | { 129 | "mode": "managed", 130 | "type": "null_resource", 131 | "name": "bar", 132 | "provider": "provider.null", 133 | "instances": [ 134 | { 135 | "schema_version": 0, 136 | "attributes": { 137 | "id": "4347220156304926627", 138 | "triggers": { 139 | "foo_id": "424881806176056736" 140 | } 141 | }, 142 | "depends_on": [ 143 | "null_resource.foo" 144 | ] 145 | } 146 | ] 147 | }, 148 | { 149 | "mode": "managed", 150 | "type": "null_resource", 151 | "name": "baz", 152 | "each": "list", 153 | "provider": "provider.null", 154 | "instances": [ 155 | { 156 | "index_key": 0, 157 | "schema_version": 0, 158 | "attributes": { 159 | "id": "751901236459396488", 160 | "triggers": { 161 | "foo_id": "424881806176056736" 162 | } 163 | }, 164 | "depends_on": [ 165 | "null_resource.foo" 166 | ] 167 | }, 168 | { 169 | "index_key": 1, 170 | "schema_version": 0, 171 | "attributes": { 172 | "id": "2106740714798375541", 173 | "triggers": { 174 | "foo_id": "424881806176056736" 175 | } 176 | }, 177 | "depends_on": [ 178 | "null_resource.foo" 179 | ] 180 | }, 181 | { 182 | "index_key": 2, 183 | "schema_version": 0, 184 | "attributes": { 185 | "id": "8665755682221598193", 186 | "triggers": { 187 | "foo_id": "424881806176056736" 188 | } 189 | }, 190 | "depends_on": [ 191 | "null_resource.foo" 192 | ] 193 | } 194 | ] 195 | }, 196 | { 197 | "mode": "managed", 198 | "type": "null_resource", 199 | "name": "foo", 200 | "provider": "provider.null", 201 | "instances": [ 202 | { 203 | "schema_version": 0, 204 | "attributes": { 205 | "id": "424881806176056736", 206 | "triggers": { 207 | "foo": "bar" 208 | } 209 | } 210 | } 211 | ] 212 | }, 213 | { 214 | "module": "module.foo", 215 | "mode": "managed", 216 | "type": "null_resource", 217 | "name": "foo", 218 | "provider": "provider.null", 219 | "instances": [ 220 | { 221 | "schema_version": 0, 222 | "attributes": { 223 | "id": "705267318028962447", 224 | "triggers": { 225 | "foo": "bar" 226 | } 227 | } 228 | } 229 | ] 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /testdata/has_changes/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/has_checks/child_moodule/main.tf: -------------------------------------------------------------------------------- 1 | variable "file_names" { 2 | type = set(string) 3 | default = [ 4 | "file1.txt", 5 | ] 6 | } 7 | 8 | resource "local_file" "foo" { 9 | for_each = var.file_names 10 | content = "Hello, World!" 11 | filename = each.value 12 | 13 | lifecycle { 14 | postcondition { 15 | condition = self.content == "Hello, World!" 16 | error_message = "File content is not correct" 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /testdata/has_checks/main.tf: -------------------------------------------------------------------------------- 1 | module "files" { 2 | source = "./child_module" 3 | file_names = ["file1.txt", "file2.txt"] 4 | } -------------------------------------------------------------------------------- /testdata/has_checks/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.2","terraform_version":"1.5.4","planned_values":{"root_module":{"child_modules":[{"resources":[{"address":"module.files.local_file.foo[\"file1.txt\"]","mode":"managed","type":"local_file","name":"foo","index":"file1.txt","provider_name":"registry.terraform.io/hashicorp/local","schema_version":0,"values":{"content":"Hello, World!","content_base64":null,"directory_permission":"0777","file_permission":"0777","filename":"file1.txt","sensitive_content":null,"source":null},"sensitive_values":{}},{"address":"module.files.local_file.foo[\"file2.txt\"]","mode":"managed","type":"local_file","name":"foo","index":"file2.txt","provider_name":"registry.terraform.io/hashicorp/local","schema_version":0,"values":{"content":"Hello, World!","content_base64":null,"directory_permission":"0777","file_permission":"0777","filename":"file2.txt","sensitive_content":null,"source":null},"sensitive_values":{}}],"address":"module.files"}]}},"resource_changes":[{"address":"module.files.local_file.foo[\"file1.txt\"]","module_address":"module.files","mode":"managed","type":"local_file","name":"foo","index":"file1.txt","provider_name":"registry.terraform.io/hashicorp/local","change":{"actions":["create"],"before":null,"after":{"content":"Hello, World!","content_base64":null,"directory_permission":"0777","file_permission":"0777","filename":"file1.txt","sensitive_content":null,"source":null},"after_unknown":{"content_base64sha256":true,"content_base64sha512":true,"content_md5":true,"content_sha1":true,"content_sha256":true,"content_sha512":true,"id":true},"before_sensitive":false,"after_sensitive":{"sensitive_content":true}}},{"address":"module.files.local_file.foo[\"file2.txt\"]","module_address":"module.files","mode":"managed","type":"local_file","name":"foo","index":"file2.txt","provider_name":"registry.terraform.io/hashicorp/local","change":{"actions":["create"],"before":null,"after":{"content":"Hello, World!","content_base64":null,"directory_permission":"0777","file_permission":"0777","filename":"file2.txt","sensitive_content":null,"source":null},"after_unknown":{"content_base64sha256":true,"content_base64sha512":true,"content_md5":true,"content_sha1":true,"content_sha256":true,"content_sha512":true,"id":true},"before_sensitive":false,"after_sensitive":{"sensitive_content":true}}}],"configuration":{"provider_config":{"module.files:local":{"name":"local","full_name":"registry.terraform.io/hashicorp/local","module_address":"module.files"}},"root_module":{"module_calls":{"files":{"source":"./child_module","expressions":{"file_names":{"constant_value":["file1.txt","file2.txt"]}},"module":{"resources":[{"address":"local_file.foo","mode":"managed","type":"local_file","name":"foo","provider_config_key":"module.files:local","expressions":{"content":{"constant_value":"Hello, World!"},"filename":{"references":["each.value"]}},"schema_version":0,"for_each_expression":{"references":["var.file_names"]}}],"variables":{"file_names":{"default":["file1.txt"]}}}}}}},"checks":[{"address":{"to_display":"module.files.local_file.foo","kind":"resource","module":"module.files","mode":"managed","type":"local_file","name":"foo"},"status":"pass","instances":[{"address":{"to_display":"module.files.local_file.foo[\"file1.txt\"]","module":"module.files","instance_key":"file1.txt"},"status":"pass"},{"address":{"to_display":"module.files.local_file.foo[\"file2.txt\"]","module":"module.files","instance_key":"file2.txt"},"status":"pass"}]}],"timestamp":"2023-08-30T10:38:39Z"} 2 | -------------------------------------------------------------------------------- /testdata/has_checks/state.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","terraform_version":"1.5.4","values":{"root_module":{"child_modules":[{"resources":[{"address":"module.files.local_file.foo[\"file1.txt\"]","mode":"managed","type":"local_file","name":"foo","index":"file1.txt","provider_name":"registry.terraform.io/hashicorp/local","schema_version":0,"values":{"content":"Hello, World!","content_base64":null,"content_base64sha256":"3/1gIbsr1bCvZ2KQgJ7DpTGR3YHH9wpLKGiKNiGCmG8=","content_base64sha512":"N015SpXNz9izWZMYX++bo2jxYNja9DLQi6nx7R5avmzGkpHg+i/gAGpSVw7xjBne9OYXwzzlLvCm5fvjGMsDhw==","content_md5":"65a8e27d8879283831b664bd8b7f0ad4","content_sha1":"0a0a9f2a6772942557ab5355d76af442f8f65e01","content_sha256":"dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f","content_sha512":"374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387","directory_permission":"0777","file_permission":"0777","filename":"file1.txt","id":"0a0a9f2a6772942557ab5355d76af442f8f65e01","sensitive_content":null,"source":null},"sensitive_values":{"sensitive_content":true}},{"address":"module.files.local_file.foo[\"file2.txt\"]","mode":"managed","type":"local_file","name":"foo","index":"file2.txt","provider_name":"registry.terraform.io/hashicorp/local","schema_version":0,"values":{"content":"Hello, World!","content_base64":null,"content_base64sha256":"3/1gIbsr1bCvZ2KQgJ7DpTGR3YHH9wpLKGiKNiGCmG8=","content_base64sha512":"N015SpXNz9izWZMYX++bo2jxYNja9DLQi6nx7R5avmzGkpHg+i/gAGpSVw7xjBne9OYXwzzlLvCm5fvjGMsDhw==","content_md5":"65a8e27d8879283831b664bd8b7f0ad4","content_sha1":"0a0a9f2a6772942557ab5355d76af442f8f65e01","content_sha256":"dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f","content_sha512":"374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387","directory_permission":"0777","file_permission":"0777","filename":"file2.txt","id":"0a0a9f2a6772942557ab5355d76af442f8f65e01","sensitive_content":null,"source":null},"sensitive_values":{"sensitive_content":true}}],"address":"module.files"}]}},"checks":[{"address":{"to_display":"module.files.local_file.foo","kind":"resource","module":"module.files","mode":"managed","type":"local_file","name":"foo"},"status":"pass","instances":[{"address":{"to_display":"module.files.local_file.foo[\"file1.txt\"]","module":"module.files","instance_key":"file1.txt"},"status":"pass"},{"address":{"to_display":"module.files.local_file.foo[\"file2.txt\"]","module":"module.files","instance_key":"file2.txt"},"status":"pass"}]}]} 2 | -------------------------------------------------------------------------------- /testdata/identity/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.2","terraform_version":"1.13.0-dev","planned_values":{"root_module":{"resources":[{"address":"corner_user_identity.user","mode":"managed","type":"corner_user_identity","name":"user","provider_name":"registry.terraform.io/hashicorp/corner","schema_version":0,"values":{"age":999,"email":"a@example.com","id":"a@example.com","name":"test"},"sensitive_values":{},"identity_schema_version":1,"identity":{"email":"a@example.com"}}]}},"resource_changes":[{"address":"corner_user_identity.user","mode":"managed","type":"corner_user_identity","name":"user","provider_name":"registry.terraform.io/hashicorp/corner","change":{"actions":["update"],"before":{"age":null,"email":"a@example.com","id":"a@example.com","name":null},"after":{"age":999,"email":"a@example.com","id":"a@example.com","name":"test"},"after_unknown":{},"before_sensitive":{},"after_sensitive":{},"importing":{"identity":{"email":"a@example.com"}},"before_identity":{"email":"a@example.com"},"after_identity":{"email":"a@example.com"}}}],"prior_state":{"format_version":"1.0","terraform_version":"1.13.0","values":{"root_module":{"resources":[{"address":"corner_user_identity.user","mode":"managed","type":"corner_user_identity","name":"user","provider_name":"registry.terraform.io/hashicorp/corner","schema_version":0,"values":{"age":null,"email":"a@example.com","id":"a@example.com","name":null},"sensitive_values":{},"identity_schema_version":1,"identity":{"email":"a@example.com"}}]}}},"configuration":{"provider_config":{"corner":{"name":"corner","full_name":"registry.terraform.io/hashicorp/corner"}},"root_module":{"resources":[{"address":"corner_user_identity.user","mode":"managed","type":"corner_user_identity","name":"user","provider_config_key":"corner","expressions":{"age":{"constant_value":999},"email":{"constant_value":"a@example.com"},"name":{"constant_value":"test"}},"schema_version":0}]}},"timestamp":"2025-04-30T11:34:17Z"} 2 | -------------------------------------------------------------------------------- /testdata/identity/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","provider_schemas":{"example":{"provider":{"version":0,"block":{"attributes":{"example":{"type":"string","description_kind":"plain","optional":true}},"description_kind":"plain"}},"resource_schemas":{"framework_example":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"Example identifier","description_kind":"markdown","computed":true}},"description":"Example resource","description_kind":"markdown"}}},"data_source_schemas":{"framework_example":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"Example identifier","description_kind":"markdown","computed":true}},"description":"Example data source","description_kind":"markdown"}}},"functions":{"example":{"description":"Echoes given argument as result","summary":"Example function","return_type":"string","parameters":[{"name":"input","description":"String to echo","type":"string"}]}},"resource_identity_schemas":{"framework_example":{"version":0,"attributes":{"number":{"type":"number","description":"A specific number","optional_for_import":true},"string":{"type":"string","required_for_import":true}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/identity/state.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","terraform_version":"1.12.0","values":{"root_module":{"resources":[{"address":"corner_bigint.number","mode":"managed","type":"corner_bigint","name":"number","provider_name":"registry.terraform.io/hashicorp/corner","schema_version":0,"values":{"id":"5","int64":5,"number":5},"sensitive_values":{}},{"address":"corner_user.user","mode":"managed","type":"corner_user","name":"user","provider_name":"registry.terraform.io/hashicorp/corner","schema_version":0,"values":{"age":999,"email":"a@example.com","id":"a@example.com","name":"test"},"sensitive_values":{},"identity_schema_version":0,"identity":{"age":999,"email":"a@example.com","name":"test"}}]}}} 2 | -------------------------------------------------------------------------------- /testdata/moved_block/main.tf: -------------------------------------------------------------------------------- 1 | moved { 2 | from = random_id.test 3 | to = random_id.test2 4 | } 5 | 6 | resource "random_id" "test2" { 7 | byte_length = 10 8 | } 9 | -------------------------------------------------------------------------------- /testdata/moved_block/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.2","terraform_version":"1.5.3","planned_values":{"root_module":{"resources":[{"address":"random_id.test2","mode":"managed","type":"random_id","name":"test2","provider_name":"registry.terraform.io/hashicorp/random","schema_version":0,"values":{"b64_std":"uBIJLwrgNTh6OQ==","b64_url":"uBIJLwrgNTh6OQ","byte_length":10,"dec":"869248136000969819847225","hex":"b812092f0ae035387a39","id":"uBIJLwrgNTh6OQ","keepers":null,"prefix":null},"sensitive_values":{}}]}},"resource_changes":[{"address":"random_id.test2","previous_address":"random_id.test","mode":"managed","type":"random_id","name":"test2","provider_name":"registry.terraform.io/hashicorp/random","change":{"actions":["no-op"],"before":{"b64_std":"uBIJLwrgNTh6OQ==","b64_url":"uBIJLwrgNTh6OQ","byte_length":10,"dec":"869248136000969819847225","hex":"b812092f0ae035387a39","id":"uBIJLwrgNTh6OQ","keepers":null,"prefix":null},"after":{"b64_std":"uBIJLwrgNTh6OQ==","b64_url":"uBIJLwrgNTh6OQ","byte_length":10,"dec":"869248136000969819847225","hex":"b812092f0ae035387a39","id":"uBIJLwrgNTh6OQ","keepers":null,"prefix":null},"after_unknown":{},"before_sensitive":{},"after_sensitive":{}}}],"prior_state":{"format_version":"1.0","terraform_version":"1.5.3","values":{"root_module":{"resources":[{"address":"random_id.test2","mode":"managed","type":"random_id","name":"test2","provider_name":"registry.terraform.io/hashicorp/random","schema_version":0,"values":{"b64_std":"uBIJLwrgNTh6OQ==","b64_url":"uBIJLwrgNTh6OQ","byte_length":10,"dec":"869248136000969819847225","hex":"b812092f0ae035387a39","id":"uBIJLwrgNTh6OQ","keepers":null,"prefix":null},"sensitive_values":{}}]}}},"configuration":{"provider_config":{"random":{"name":"random","full_name":"registry.terraform.io/hashicorp/random"}},"root_module":{"resources":[{"address":"random_id.test2","mode":"managed","type":"random_id","name":"test2","provider_config_key":"random","expressions":{"byte_length":{"constant_value":10}},"schema_version":0}]}},"timestamp":"2023-08-31T11:49:54Z"} 2 | -------------------------------------------------------------------------------- /testdata/moved_block/show.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","terraform_version":"1.5.3","values":{"root_module":{"resources":[{"address":"random_id.test","mode":"managed","type":"random_id","name":"test","provider_name":"registry.terraform.io/hashicorp/random","schema_version":0,"values":{"b64_std":"uBIJLwrgNTh6OQ==","b64_url":"uBIJLwrgNTh6OQ","byte_length":10,"dec":"869248136000969819847225","hex":"b812092f0ae035387a39","id":"uBIJLwrgNTh6OQ","keepers":null,"prefix":null},"sensitive_values":{}}]}}} 2 | -------------------------------------------------------------------------------- /testdata/nested_attributes/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.2","provider_schemas":{"registry.terraform.io/hashicorp/awscc":{"provider":{"version":0,"block":{"attributes":{"access_key":{"type":"string","description_kind":"plain","optional":true},"assume_role":{"nested_type":{"attributes":{"duration":{"type":"string","description_kind":"plain","optional":true},"external_id":{"type":"string","description_kind":"plain","optional":true}},"nesting_mode":"single"},"description_kind":"plain","optional":true}},"description_kind":"plain"}}}}} 2 | -------------------------------------------------------------------------------- /testdata/nested_config_keys/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } 4 | 5 | variable "foo" { 6 | default = "/dev/sda1" 7 | } 8 | 9 | resource "aws_instance" "foo" { 10 | ami = "ami-foobar" 11 | instance_type = "t2.micro" 12 | 13 | ebs_block_device { 14 | device_name = "${var.foo}" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/nested_config_keys/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","variables":{"foo":{"value":"/dev/sda1"}},"planned_values":{"root_module":{"resources":[{"address":"aws_instance.foo","mode":"managed","type":"aws_instance","name":"foo","provider_name":"aws","schema_version":1,"values":{"ami":"ami-foobar","credit_specification":[],"disable_api_termination":null,"ebs_block_device":[{"delete_on_termination":true,"device_name":"/dev/sda1"}],"ebs_optimized":null,"get_password_data":false,"hibernation":null,"iam_instance_profile":null,"instance_initiated_shutdown_behavior":null,"instance_type":"t2.micro","monitoring":null,"source_dest_check":true,"tags":null,"timeouts":null,"user_data":null,"user_data_base64":null}}]}},"resource_changes":[{"address":"aws_instance.foo","mode":"managed","type":"aws_instance","name":"foo","provider_name":"aws","change":{"actions":["create"],"before":null,"after":{"ami":"ami-foobar","credit_specification":[],"disable_api_termination":null,"ebs_block_device":[{"delete_on_termination":true,"device_name":"/dev/sda1"}],"ebs_optimized":null,"get_password_data":false,"hibernation":null,"iam_instance_profile":null,"instance_initiated_shutdown_behavior":null,"instance_type":"t2.micro","monitoring":null,"source_dest_check":true,"tags":null,"timeouts":null,"user_data":null,"user_data_base64":null},"after_unknown":{"arn":true,"associate_public_ip_address":true,"availability_zone":true,"cpu_core_count":true,"cpu_threads_per_core":true,"credit_specification":[],"ebs_block_device":[{"encrypted":true,"iops":true,"kms_key_id":true,"snapshot_id":true,"volume_id":true,"volume_size":true,"volume_type":true}],"ephemeral_block_device":true,"host_id":true,"id":true,"instance_state":true,"ipv6_address_count":true,"ipv6_addresses":true,"key_name":true,"metadata_options":true,"network_interface":true,"outpost_arn":true,"password_data":true,"placement_group":true,"primary_network_interface_id":true,"private_dns":true,"private_ip":true,"public_dns":true,"public_ip":true,"root_block_device":true,"secondary_private_ips":true,"security_groups":true,"subnet_id":true,"tenancy":true,"volume_tags":true,"vpc_security_group_ids":true}}}],"configuration":{"provider_config":{"aws":{"name":"aws","expressions":{"region":{"constant_value":"us-east-1"}}}},"root_module":{"resources":[{"address":"aws_instance.foo","mode":"managed","type":"aws_instance","name":"foo","provider_config_key":"aws","expressions":{"ami":{"constant_value":"ami-foobar"},"ebs_block_device":[{"device_name":{"references":["var.foo"]}}],"instance_type":{"constant_value":"t2.micro"}},"schema_version":1}],"variables":{"foo":{"default":"/dev/sda1"}}}}} 2 | -------------------------------------------------------------------------------- /testdata/no_changes/foo/main.tf: -------------------------------------------------------------------------------- 1 | variable "bar" { 2 | type = "string" 3 | } 4 | 5 | variable "one" { 6 | type = "string" 7 | } 8 | 9 | resource "null_resource" "foo" { 10 | triggers = { 11 | foo = "bar" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/no_changes/module.tf: -------------------------------------------------------------------------------- 1 | module "foo" { 2 | source = "./foo" 3 | 4 | bar = "baz" 5 | one = "two" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/no_changes/outputs.tf: -------------------------------------------------------------------------------- 1 | output "foo" { 2 | sensitive = true 3 | value = "bar" 4 | } 5 | 6 | output "string" { 7 | value = "foo" 8 | } 9 | 10 | output "list" { 11 | value = [ 12 | "foo", 13 | "bar", 14 | ] 15 | } 16 | 17 | output "map" { 18 | value = { 19 | foo = "bar" 20 | number = 42 21 | } 22 | } 23 | 24 | output "referenced" { 25 | value = null_resource.foo.id 26 | } 27 | 28 | output "interpolated" { 29 | value = "${null_resource.foo.id}" 30 | } 31 | 32 | output "referenced_deep" { 33 | value = { 34 | foo = "bar" 35 | number = 42 36 | map = { 37 | bar = "baz" 38 | id = null_resource.foo.id 39 | } 40 | } 41 | } 42 | 43 | output "interpolated_deep" { 44 | value = { 45 | foo = "bar" 46 | number = 42 47 | map = { 48 | bar = "baz" 49 | id = "${null_resource.foo.id}" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /testdata/no_changes/providers.tf: -------------------------------------------------------------------------------- 1 | provider "null" {} 2 | 3 | provider "aws" { 4 | region = "us-west-2" 5 | } 6 | 7 | provider "aws" { 8 | alias = "east" 9 | region = "us-east-1" 10 | } 11 | -------------------------------------------------------------------------------- /testdata/no_changes/resources.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" { 2 | triggers = { 3 | foo = "bar" 4 | } 5 | 6 | provisioner "local-exec" { 7 | command = "echo hello" 8 | } 9 | } 10 | 11 | resource "null_resource" "bar" { 12 | triggers = { 13 | foo_id = "${null_resource.foo.id}" 14 | } 15 | } 16 | 17 | data "null_data_source" "baz" { 18 | inputs = { 19 | foo_id = "${null_resource.foo.id}" 20 | bar_id = "${null_resource.bar.id}" 21 | } 22 | } 23 | 24 | resource "null_resource" "baz" { 25 | count = 3 26 | 27 | triggers = { 28 | foo_id = "${null_resource.foo.id}" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /testdata/no_changes/state.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.0","values":{"outputs":{"foo":{"sensitive":true,"value":"bar"},"interpolated":{"sensitive":false,"value":"424881806176056736"},"interpolated_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":"424881806176056736"},"number":42}},"list":{"sensitive":false,"value":["foo","bar"]},"map":{"sensitive":false,"value":{"foo":"bar","number":42}},"referenced":{"sensitive":false,"value":"424881806176056736"},"referenced_deep":{"sensitive":false,"value":{"foo":"bar","map":{"bar":"baz","id":"424881806176056736"},"number":42}},"string":{"sensitive":false,"value":"foo"}},"root_module":{"resources":[{"address":"data.null_data_source.baz","mode":"data","type":"null_data_source","name":"baz","provider_name":"null","schema_version":0,"values":{"has_computed_default":"default","id":"static","inputs":{"bar_id":"4347220156304926627","foo_id":"424881806176056736"},"outputs":{"bar_id":"4347220156304926627","foo_id":"424881806176056736"},"random":"1951353658349486401"}},{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0,"values":{"id":"4347220156304926627","triggers":{"foo_id":"424881806176056736"}}},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","index":0,"provider_name":"null","schema_version":0,"values":{"id":"751901236459396488","triggers":{"foo_id":"424881806176056736"}}},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","index":1,"provider_name":"null","schema_version":0,"values":{"id":"2106740714798375541","triggers":{"foo_id":"424881806176056736"}}},{"address":"null_resource.baz","mode":"managed","type":"null_resource","name":"baz","index":2,"provider_name":"null","schema_version":0,"values":{"id":"8665755682221598193","triggers":{"foo_id":"424881806176056736"}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"id":"424881806176056736","triggers":{"foo":"bar"}}}],"child_modules":[{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"id":"705267318028962447","triggers":{"foo":"bar"}}}],"address":"module.foo"}]}}} 2 | -------------------------------------------------------------------------------- /testdata/no_changes/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.0", 4 | "serial": 8, 5 | "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", 6 | "outputs": { 7 | "foo": { 8 | "value": "bar", 9 | "type": "string", 10 | "sensitive": true 11 | }, 12 | "interpolated": { 13 | "value": "424881806176056736", 14 | "type": "string" 15 | }, 16 | "interpolated_deep": { 17 | "value": { 18 | "foo": "bar", 19 | "map": { 20 | "bar": "baz", 21 | "id": "424881806176056736" 22 | }, 23 | "number": 42 24 | }, 25 | "type": [ 26 | "object", 27 | { 28 | "foo": "string", 29 | "map": [ 30 | "object", 31 | { 32 | "bar": "string", 33 | "id": "string" 34 | } 35 | ], 36 | "number": "number" 37 | } 38 | ] 39 | }, 40 | "list": { 41 | "value": [ 42 | "foo", 43 | "bar" 44 | ], 45 | "type": [ 46 | "tuple", 47 | [ 48 | "string", 49 | "string" 50 | ] 51 | ] 52 | }, 53 | "map": { 54 | "value": { 55 | "foo": "bar", 56 | "number": 42 57 | }, 58 | "type": [ 59 | "object", 60 | { 61 | "foo": "string", 62 | "number": "number" 63 | } 64 | ] 65 | }, 66 | "referenced": { 67 | "value": "424881806176056736", 68 | "type": "string" 69 | }, 70 | "referenced_deep": { 71 | "value": { 72 | "foo": "bar", 73 | "map": { 74 | "bar": "baz", 75 | "id": "424881806176056736" 76 | }, 77 | "number": 42 78 | }, 79 | "type": [ 80 | "object", 81 | { 82 | "foo": "string", 83 | "map": [ 84 | "object", 85 | { 86 | "bar": "string", 87 | "id": "string" 88 | } 89 | ], 90 | "number": "number" 91 | } 92 | ] 93 | }, 94 | "string": { 95 | "value": "foo", 96 | "type": "string" 97 | } 98 | }, 99 | "resources": [ 100 | { 101 | "mode": "data", 102 | "type": "null_data_source", 103 | "name": "baz", 104 | "provider": "provider.null", 105 | "instances": [ 106 | { 107 | "schema_version": 0, 108 | "attributes": { 109 | "has_computed_default": "default", 110 | "id": "static", 111 | "inputs": { 112 | "bar_id": "4347220156304926627", 113 | "foo_id": "424881806176056736" 114 | }, 115 | "outputs": { 116 | "bar_id": "4347220156304926627", 117 | "foo_id": "424881806176056736" 118 | }, 119 | "random": "1951353658349486401" 120 | }, 121 | "depends_on": [ 122 | "null_resource.bar", 123 | "null_resource.foo" 124 | ] 125 | } 126 | ] 127 | }, 128 | { 129 | "mode": "managed", 130 | "type": "null_resource", 131 | "name": "bar", 132 | "provider": "provider.null", 133 | "instances": [ 134 | { 135 | "schema_version": 0, 136 | "attributes": { 137 | "id": "4347220156304926627", 138 | "triggers": { 139 | "foo_id": "424881806176056736" 140 | } 141 | }, 142 | "depends_on": [ 143 | "null_resource.foo" 144 | ] 145 | } 146 | ] 147 | }, 148 | { 149 | "mode": "managed", 150 | "type": "null_resource", 151 | "name": "baz", 152 | "each": "list", 153 | "provider": "provider.null", 154 | "instances": [ 155 | { 156 | "index_key": 0, 157 | "schema_version": 0, 158 | "attributes": { 159 | "id": "751901236459396488", 160 | "triggers": { 161 | "foo_id": "424881806176056736" 162 | } 163 | }, 164 | "depends_on": [ 165 | "null_resource.foo" 166 | ] 167 | }, 168 | { 169 | "index_key": 1, 170 | "schema_version": 0, 171 | "attributes": { 172 | "id": "2106740714798375541", 173 | "triggers": { 174 | "foo_id": "424881806176056736" 175 | } 176 | }, 177 | "depends_on": [ 178 | "null_resource.foo" 179 | ] 180 | }, 181 | { 182 | "index_key": 2, 183 | "schema_version": 0, 184 | "attributes": { 185 | "id": "8665755682221598193", 186 | "triggers": { 187 | "foo_id": "424881806176056736" 188 | } 189 | }, 190 | "depends_on": [ 191 | "null_resource.foo" 192 | ] 193 | } 194 | ] 195 | }, 196 | { 197 | "mode": "managed", 198 | "type": "null_resource", 199 | "name": "foo", 200 | "provider": "provider.null", 201 | "instances": [ 202 | { 203 | "schema_version": 0, 204 | "attributes": { 205 | "id": "424881806176056736", 206 | "triggers": { 207 | "foo": "bar" 208 | } 209 | } 210 | } 211 | ] 212 | }, 213 | { 214 | "module": "module.foo", 215 | "mode": "managed", 216 | "type": "null_resource", 217 | "name": "foo", 218 | "provider": "provider.null", 219 | "instances": [ 220 | { 221 | "schema_version": 0, 222 | "attributes": { 223 | "id": "705267318028962447", 224 | "triggers": { 225 | "foo": "bar" 226 | } 227 | } 228 | } 229 | ] 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /testdata/no_changes/variables.tf: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | description = "foobar" 3 | default = "bar" 4 | } 5 | 6 | variable "number" { 7 | default = 42 8 | } 9 | 10 | variable "map" { 11 | default = { 12 | foo = "bar" 13 | number = 42 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testdata/numerics/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.2","terraform_version":"1.6.5","planned_values":{"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","schema_version":0,"values":{"configurable_attribute":1.23,"id":"one"},"sensitive_values":{}}]}},"resource_changes":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","change":{"actions":["create"],"before":null,"after":{"configurable_attribute":1.23,"id":"one"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"example":{"name":"example","full_name":"registry.terraform.io/hashicorp/example"}},"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_config_key":"example","expressions":{"configurable_attribute":{"constant_value":1.23},"id":{"constant_value":"one"}},"schema_version":0}]}},"timestamp":"2023-12-07T13:55:56Z"} 2 | -------------------------------------------------------------------------------- /testdata/output_depends_on/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "foo" {} 2 | 3 | resource "null_resource" "bar" {} 4 | 5 | output "id" { 6 | depends_on = ["null_resource.bar"] 7 | value = "${null_resource.foo.id}" 8 | } 9 | -------------------------------------------------------------------------------- /testdata/output_depends_on/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","planned_values":{"outputs":{"id":{"sensitive":false}},"root_module":{"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","schema_version":0,"values":{"triggers":null}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"triggers":null}}]}},"resource_changes":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}}],"output_changes":{"id":{"actions":["create"],"before":null,"after_unknown":true}},"configuration":{"root_module":{"outputs":{"id":{"expression":{"references":["null_resource.foo"]},"depends_on":["null_resource.bar"]}},"resources":[{"address":"null_resource.bar","mode":"managed","type":"null_resource","name":"bar","provider_config_key":"null","schema_version":0},{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}} 2 | -------------------------------------------------------------------------------- /testdata/output_depends_on/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/provider_version/main.tf: -------------------------------------------------------------------------------- 1 | provider "null" { 2 | version = "~> 2.1" 3 | } 4 | 5 | resource "null_resource" "foo" {} 6 | -------------------------------------------------------------------------------- /testdata/provider_version/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","planned_values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","schema_version":0,"values":{"triggers":null}}]}},"resource_changes":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true}}}],"configuration":{"provider_config":{"null":{"name":"null","version_constraint":"~\u003e 2.1"}},"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}} 2 | -------------------------------------------------------------------------------- /testdata/provider_version/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/registry_module/main.tf: -------------------------------------------------------------------------------- 1 | module "module" { 2 | source = "vancluever/module/null" 3 | version = "~> 1.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/registry_module/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","terraform_version":"0.12.11","planned_values":{"root_module":{"child_modules":[{"resources":[{"address":"module.module.null_resource.resource","mode":"managed","type":"null_resource","name":"resource","provider_name":"null","schema_version":0,"values":{"triggers":{"number":"one"}}}],"address":"module.module"}]}},"resource_changes":[{"address":"module.module.null_resource.resource","module_address":"module.module","mode":"managed","type":"null_resource","name":"resource","provider_name":"null","change":{"actions":["create"],"before":null,"after":{"triggers":{"number":"one"}},"after_unknown":{"id":true,"triggers":{}}}}],"prior_state":{"format_version":"0.1","terraform_version":"0.12.11","values":{"root_module":{"child_modules":[{"resources":[{"address":"data.null_data_source.data","mode":"data","type":"null_data_source","name":"data","provider_name":"null","schema_version":0,"values":{"has_computed_default":"default","id":"static","inputs":{"key":"foo"},"outputs":{"key":"foo"},"random":"7083402041033367103"}}],"address":"module.module"}]}}},"configuration":{"root_module":{"module_calls":{"module":{"source":"vancluever/module/null","module":{"outputs":{"null_data_source_id":{"expression":{"references":["data.null_data_source.data"]},"description":"The `id` of the `null_data_source` data source in this module."},"null_resource_id":{"expression":{"references":["null_resource.resource"]},"description":"The `id` of the `null_resource` resource in this module."}},"resources":[{"address":"null_resource.resource","mode":"managed","type":"null_resource","name":"resource","provider_config_key":"module:null","expressions":{"triggers":{"references":["var.trigger"]}},"schema_version":0},{"address":"data.null_data_source.data","mode":"data","type":"null_data_source","name":"data","provider_config_key":"module:null","expressions":{"inputs":{"references":["var.input"]}},"schema_version":0}],"variables":{"input":{"default":"foo","description":"The input value for the `null_data_source` data source in this module."},"trigger":{"default":"one","description":"The trigger value for the `null_resource` resource in this module."}}},"version_constraint":"~\u003e 1.0.1"}}}}} 2 | -------------------------------------------------------------------------------- /testdata/registry_module/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.1","provider_schemas":{"null":{"provider":{"version":0,"block":{}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","optional":true,"computed":true},"triggers":{"type":["map","string"],"optional":true}}}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","optional":true,"computed":true},"id":{"type":"string","optional":true,"computed":true},"inputs":{"type":["map","string"],"optional":true},"outputs":{"type":["map","string"],"computed":true},"random":{"type":"string","computed":true}}}}}}}} 2 | -------------------------------------------------------------------------------- /testdata/write_only_attribute_on_resource/schemas.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.0","provider_schemas":{"terraform.io/builtin/terraform":{"provider":{"version":0},"resource_schemas":{"terraform_example":{"version":0,"block":{"attributes":{"foo":{"type":"string","description_kind":"plain","optional":true},"wo_attr":{"type":"string","description_kind":"plain","optional":true,"write_only":true}},"description_kind":"plain"}}}}}} 2 | -------------------------------------------------------------------------------- /tfjson.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package tfjson is a de-coupled helper library containing types for 5 | // the plan format output by "terraform show -json" command. This 6 | // command is designed for the export of Terraform plan data in 7 | // a format that can be easily processed by tools unrelated to 8 | // Terraform. 9 | // 10 | // This format is stable and should be used over the binary plan data 11 | // whenever possible. 12 | package tfjson 13 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/hashicorp/go-version" 12 | ) 13 | 14 | // ValidateFormatVersionConstraints defines the versions of the JSON 15 | // validate format that are supported by this package. 16 | var ValidateFormatVersionConstraints = ">= 0.1, < 2.0" 17 | 18 | // Pos represents a position in a config file 19 | type Pos struct { 20 | Line int `json:"line"` 21 | Column int `json:"column"` 22 | Byte int `json:"byte"` 23 | } 24 | 25 | // Range represents a range of bytes between two positions 26 | type Range struct { 27 | Filename string `json:"filename"` 28 | Start Pos `json:"start"` 29 | End Pos `json:"end"` 30 | } 31 | 32 | type DiagnosticSeverity string 33 | 34 | // These severities map to the tfdiags.Severity values, plus an explicit 35 | // unknown in case that enum grows without us noticing here. 36 | const ( 37 | DiagnosticSeverityUnknown DiagnosticSeverity = "unknown" 38 | DiagnosticSeverityError DiagnosticSeverity = "error" 39 | DiagnosticSeverityWarning DiagnosticSeverity = "warning" 40 | ) 41 | 42 | // Diagnostic represents information to be presented to a user about an 43 | // error or anomaly in parsing or evaluating configuration 44 | type Diagnostic struct { 45 | Severity DiagnosticSeverity `json:"severity,omitempty"` 46 | 47 | Summary string `json:"summary,omitempty"` 48 | Detail string `json:"detail,omitempty"` 49 | Range *Range `json:"range,omitempty"` 50 | 51 | Snippet *DiagnosticSnippet `json:"snippet,omitempty"` 52 | } 53 | 54 | // DiagnosticSnippet represents source code information about the diagnostic. 55 | // It is possible for a diagnostic to have a source (and therefore a range) but 56 | // no source code can be found. In this case, the range field will be present and 57 | // the snippet field will not. 58 | type DiagnosticSnippet struct { 59 | // Context is derived from HCL's hcled.ContextString output. This gives a 60 | // high-level summary of the root context of the diagnostic: for example, 61 | // the resource block in which an expression causes an error. 62 | Context *string `json:"context"` 63 | 64 | // Code is a possibly-multi-line string of Terraform configuration, which 65 | // includes both the diagnostic source and any relevant context as defined 66 | // by the diagnostic. 67 | Code string `json:"code"` 68 | 69 | // StartLine is the line number in the source file for the first line of 70 | // the snippet code block. This is not necessarily the same as the value of 71 | // Range.Start.Line, as it is possible to have zero or more lines of 72 | // context source code before the diagnostic range starts. 73 | StartLine int `json:"start_line"` 74 | 75 | // HighlightStartOffset is the character offset into Code at which the 76 | // diagnostic source range starts, which ought to be highlighted as such by 77 | // the consumer of this data. 78 | HighlightStartOffset int `json:"highlight_start_offset"` 79 | 80 | // HighlightEndOffset is the character offset into Code at which the 81 | // diagnostic source range ends. 82 | HighlightEndOffset int `json:"highlight_end_offset"` 83 | 84 | // Values is a sorted slice of expression values which may be useful in 85 | // understanding the source of an error in a complex expression. 86 | Values []DiagnosticExpressionValue `json:"values"` 87 | } 88 | 89 | // DiagnosticExpressionValue represents an HCL traversal string (e.g. 90 | // "var.foo") and a statement about its value while the expression was 91 | // evaluated (e.g. "is a string", "will be known only after apply"). These are 92 | // intended to help the consumer diagnose why an expression caused a diagnostic 93 | // to be emitted. 94 | type DiagnosticExpressionValue struct { 95 | Traversal string `json:"traversal"` 96 | Statement string `json:"statement"` 97 | } 98 | 99 | // ValidateOutput represents JSON output from terraform validate 100 | // (available from 0.12 onwards) 101 | type ValidateOutput struct { 102 | FormatVersion string `json:"format_version"` 103 | 104 | Valid bool `json:"valid"` 105 | ErrorCount int `json:"error_count"` 106 | WarningCount int `json:"warning_count"` 107 | Diagnostics []Diagnostic `json:"diagnostics"` 108 | } 109 | 110 | // Validate checks to ensure that data is present, and the 111 | // version matches the version supported by this library. 112 | func (vo *ValidateOutput) Validate() error { 113 | if vo == nil { 114 | return errors.New("validation output is nil") 115 | } 116 | 117 | if vo.FormatVersion == "" { 118 | // The format was not versioned in the past 119 | return nil 120 | } 121 | 122 | constraint, err := version.NewConstraint(ValidateFormatVersionConstraints) 123 | if err != nil { 124 | return fmt.Errorf("invalid version constraint: %w", err) 125 | } 126 | 127 | version, err := version.NewVersion(vo.FormatVersion) 128 | if err != nil { 129 | return fmt.Errorf("invalid format version %q: %w", vo.FormatVersion, err) 130 | } 131 | 132 | if !constraint.Check(version) { 133 | return fmt.Errorf("unsupported validation output format version: %q does not satisfy %q", 134 | version, constraint) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (vo *ValidateOutput) UnmarshalJSON(b []byte) error { 141 | type rawOutput ValidateOutput 142 | var schemas rawOutput 143 | 144 | err := json.Unmarshal(b, &schemas) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | *vo = *(*ValidateOutput)(&schemas) 150 | 151 | return vo.Validate() 152 | } 153 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | // VersionOutput represents output from the version -json command 7 | // added in v0.13 8 | type VersionOutput struct { 9 | Version string `json:"terraform_version"` 10 | Revision string `json:"terraform_revision"` 11 | Platform string `json:"platform,omitempty"` 12 | ProviderSelections map[string]string `json:"provider_selections"` 13 | Outdated bool `json:"terraform_outdated"` 14 | } 15 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfjson 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestVersionOutput_013(t *testing.T) { 14 | errOutput := `{ 15 | "terraform_version": "0.13.5", 16 | "terraform_revision": "", 17 | "provider_selections": { 18 | "registry.terraform.io/hashicorp/github": "2.9.2", 19 | "registry.terraform.io/hashicorp/random": "3.0.0" 20 | }, 21 | "terraform_outdated": true 22 | }` 23 | var parsed VersionOutput 24 | if err := json.Unmarshal([]byte(errOutput), &parsed); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | expected := &VersionOutput{ 29 | Version: "0.13.5", 30 | ProviderSelections: map[string]string{ 31 | "registry.terraform.io/hashicorp/github": "2.9.2", 32 | "registry.terraform.io/hashicorp/random": "3.0.0", 33 | }, 34 | Outdated: true, 35 | } 36 | if diff := cmp.Diff(expected, &parsed); diff != "" { 37 | t.Fatalf("output mismatch: %s", diff) 38 | } 39 | } 40 | 41 | func TestVersionOutput_015(t *testing.T) { 42 | errOutput := `{ 43 | "terraform_version": "0.15.0-dev", 44 | "terraform_revision": "ae025248cc0712bf53c675dc2fe77af4276dd5cc", 45 | "platform": "darwin_amd64", 46 | "provider_selections": { 47 | "registry.terraform.io/hashicorp/github": "2.9.2", 48 | "registry.terraform.io/hashicorp/random": "3.0.0" 49 | }, 50 | "terraform_outdated": false 51 | }` 52 | var parsed VersionOutput 53 | if err := json.Unmarshal([]byte(errOutput), &parsed); err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | expected := &VersionOutput{ 58 | Version: "0.15.0-dev", 59 | Revision: "ae025248cc0712bf53c675dc2fe77af4276dd5cc", 60 | Platform: "darwin_amd64", 61 | ProviderSelections: map[string]string{ 62 | "registry.terraform.io/hashicorp/github": "2.9.2", 63 | "registry.terraform.io/hashicorp/random": "3.0.0", 64 | }, 65 | } 66 | if diff := cmp.Diff(expected, &parsed); diff != "" { 67 | t.Fatalf("output mismatch: %s", diff) 68 | } 69 | } 70 | --------------------------------------------------------------------------------