├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── deps.yml │ ├── release.yml │ ├── main.yml │ └── go-cross.yml ├── testdata ├── resource_create │ ├── before │ │ └── main.tf │ ├── output.golden │ ├── after │ │ └── main.tf │ └── plan.json ├── resource_delete │ ├── after │ │ └── main.tf │ ├── output.golden │ ├── before │ │ └── main.tf │ └── plan.json ├── resource_update │ ├── after │ │ └── main.tf │ ├── before │ │ └── main.tf │ ├── output.golden │ └── plan.json ├── resource_replace │ ├── before │ │ └── main.tf │ ├── after │ │ └── main.tf │ ├── output.golden │ └── plan.json ├── outputs │ ├── before │ │ └── main.tf │ ├── after │ │ └── main.tf │ ├── output.golden │ └── plan.json ├── sensitive │ ├── before │ │ └── main.tf │ ├── after │ │ └── main.tf │ ├── output.golden │ └── plan.json └── advanced │ ├── before │ └── main.tf │ ├── after │ └── main.tf │ ├── output.golden │ └── plan.json ├── .goreleaser.yml ├── LICENSE ├── main_test.go ├── Taskfile.yml ├── .gitignore ├── go.mod ├── main.go ├── README.md ├── resources └── tfreveal.cast ├── format.go └── go.sum /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout 2 | *.golden text eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /testdata/resource_create/before/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | -------------------------------------------------------------------------------- /testdata/resource_delete/after/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | -------------------------------------------------------------------------------- /testdata/resource_update/after/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | time = { 4 | source = "hashicorp/time" 5 | version = "0.11.1" 6 | } 7 | } 8 | } 9 | 10 | provider "time" {} 11 | 12 | resource "time_offset" "example" { 13 | offset_days = 8 14 | } 15 | -------------------------------------------------------------------------------- /testdata/resource_update/before/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | time = { 4 | source = "hashicorp/time" 5 | version = "0.11.1" 6 | } 7 | } 8 | } 9 | 10 | provider "time" {} 11 | 12 | resource "time_offset" "example" { 13 | offset_days = 7 14 | } 15 | -------------------------------------------------------------------------------- /testdata/resource_create/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | + null_resource.cluster = { 6 | + id = "(known after apply)" 7 | + triggers = { 8 | + secret = "very very secure" 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /testdata/resource_delete/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | - null_resource.cluster = { 6 | - id = "939058779081368068" 7 | - triggers = { 8 | - secret = "very very secure" 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /testdata/resource_replace/before/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | 12 | resource "null_resource" "cluster" { 13 | triggers = { 14 | secret = "secure" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/resource_create/after/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | 12 | resource "null_resource" "cluster" { 13 | triggers = { 14 | secret = "very very secure" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/resource_delete/before/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | 12 | resource "null_resource" "cluster" { 13 | triggers = { 14 | secret = "very very secure" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/resource_replace/after/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | } 8 | } 9 | 10 | provider "null" {} 11 | 12 | resource "null_resource" "cluster" { 13 | triggers = { 14 | secret = "very very secure" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/resource_update/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | ~ time_offset.example = { 6 | ~ day = 2 -> 3 7 | ~ offset_days = 7 -> 8 8 | ~ rfc3339 = "2024-06-02T15:18:10Z" -> "2024-06-03T15:18:10Z" 9 | ~ unix = 1717341490 -> 1717427890 10 | # (13 unchanged attribute hidden) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /testdata/resource_replace/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | # null_resource.cluster must be replaced 6 | -/+ null_resource.cluster = { 7 | ~ id = "5350362168280616586" -> "(known after apply)" 8 | triggers = { # forces replacement 9 | ~ secret = "secure" -> "very very secure" 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/deps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # https://github.com/actions/go-dependency-submission 4 | name: Dependency Submission 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | main: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: "stable" 21 | - uses: actions/go-dependency-submission@v2 22 | with: 23 | go-mod-path: go.mod 24 | -------------------------------------------------------------------------------- /testdata/outputs/before/main.tf: -------------------------------------------------------------------------------- 1 | output "string" { 2 | value = "some text" 3 | } 4 | 5 | output "number_equal" { 6 | value = 5 7 | } 8 | 9 | output "number_change" { 10 | value = 10 11 | } 12 | 13 | output "number_remove" { 14 | value = 20 15 | } 16 | 17 | output "object" { 18 | value = { 19 | key1 = "1" 20 | key3 = "3" 21 | key4 = "4 remove" 22 | } 23 | } 24 | 25 | output "array" { 26 | value = [ 27 | "foo", 28 | "bar", 29 | ] 30 | } 31 | 32 | output "jsonString" { 33 | value = jsonencode({ 34 | key1 = "1" 35 | key3 = "3" 36 | key4 = "4 remove" 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /testdata/outputs/after/main.tf: -------------------------------------------------------------------------------- 1 | output "string" { 2 | value = "some text 1" 3 | } 4 | 5 | output "number_equal" { 6 | value = 5 7 | } 8 | 9 | output "number_change" { 10 | value = 12 11 | } 12 | 13 | output "number_add" { 14 | value = 30 15 | } 16 | 17 | output "object" { 18 | value = { 19 | key1 = "1" 20 | key2 = "2 added" 21 | key3 = "3 changed" 22 | } 23 | } 24 | 25 | output "array" { 26 | value = [ 27 | "foo", 28 | "more", 29 | "bar", 30 | ] 31 | } 32 | 33 | output "jsonString" { 34 | value = jsonencode({ 35 | key1 = "1" 36 | key2 = "2 added" 37 | key3 = "3 changed" 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - main: . 8 | binary: tfreveal 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | archives: 16 | - name_template: >- 17 | {{- .Binary }}_ 18 | {{- .Version }}_ 19 | {{- title .Os }}_ 20 | {{- if eq .Arch "amd64" }}x86_64 21 | {{- else if eq .Arch "386" }}i386 22 | {{- else }}{{ .Arch }}{{ end }} 23 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 24 | snapshot: 25 | name_template: "{{ .Tag }}-next" 26 | changelog: 27 | disable: true 28 | release: 29 | github: 30 | owner: breml 31 | name: tfreveal 32 | gomod: 33 | proxy: false 34 | -------------------------------------------------------------------------------- /testdata/outputs/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Outputs: 4 | 5 | ~ array = [ 6 | "foo" 7 | ~ "bar" -> "more" 8 | + "bar" 9 | ] 10 | ~ jsonString = jsonencode( 11 | { 12 | + key2 = "2 added" 13 | ~ key3 = "3" -> "3 changed" 14 | - key4 = "4 remove" 15 | # (1 unchanged attribute hidden) 16 | } 17 | ) 18 | + number_add = 30 19 | ~ number_change = 10 -> 12 20 | - number_remove = 20 21 | ~ object = { 22 | + key2 = "2 added" 23 | ~ key3 = "3" -> "3 changed" 24 | - key4 = "4 remove" 25 | # (1 unchanged attribute hidden) 26 | } 27 | ~ string = "some text" -> "some text 1" 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | goreleaser: 11 | name: Release tfreveal 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "stable" 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v5 27 | with: 28 | distribution: goreleaser 29 | version: latest 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /testdata/sensitive/before/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | secret_value = sensitive("some secret value") 3 | sensitive_json = sensitive(jsonencode({ key = "value" })) 4 | json_with_nested_sensitive = jsonencode({ 5 | key = sensitive("value") 6 | }) 7 | } 8 | 9 | resource "null_resource" "sensitive" { 10 | triggers = { 11 | secret = local.secret_value 12 | } 13 | } 14 | 15 | output "null_resource" { 16 | value = nonsensitive(null_resource.sensitive) 17 | sensitive = true 18 | } 19 | 20 | output "sensitive" { 21 | value = nonsensitive(null_resource.sensitive.triggers.secret) 22 | } 23 | 24 | output "sensitive_json" { 25 | value = nonsensitive(local.sensitive_json) 26 | } 27 | 28 | output "json_with_nested_sensitive" { 29 | value = nonsensitive(local.json_with_nested_sensitive) 30 | } 31 | -------------------------------------------------------------------------------- /testdata/sensitive/after/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | secret_value = sensitive("new secret value") 3 | sensitive_json = sensitive(jsonencode({ key = "value2" })) 4 | json_with_nested_sensitive = jsonencode({ 5 | key = sensitive("value2") 6 | }) 7 | other_value = jsonencode( 8 | { 9 | key = "value" 10 | } 11 | ) 12 | } 13 | 14 | resource "null_resource" "sensitive" { 15 | triggers = { 16 | secret = local.secret_value 17 | } 18 | } 19 | 20 | output "null_resource" { 21 | value = nonsensitive(null_resource.sensitive) 22 | sensitive = true 23 | } 24 | 25 | output "other_value" { 26 | value = local.other_value 27 | } 28 | 29 | output "sensitive" { 30 | value = nonsensitive(null_resource.sensitive.triggers.secret) 31 | } 32 | 33 | output "sensitive_json" { 34 | value = nonsensitive(local.sensitive_json) 35 | } 36 | 37 | output "json_with_nested_sensitive" { 38 | value = nonsensitive(local.json_with_nested_sensitive) 39 | } 40 | -------------------------------------------------------------------------------- /testdata/sensitive/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | # null_resource.sensitive must be replaced 6 | -/+ null_resource.sensitive = { 7 | ~ id = "4190156157480914441" -> "(known after apply)" 8 | triggers = { # forces replacement 9 | ~ secret = "some secret value" -> "new secret value" 10 | } 11 | } 12 | 13 | Changes to Outputs: 14 | 15 | ~ json_with_nested_sensitive = jsonencode( 16 | { 17 | ~ key = "value" -> "value2" 18 | } 19 | ) 20 | ~ null_resource = { 21 | ~ id = "4190156157480914441" -> "(known after apply)" 22 | triggers = { 23 | ~ secret = "some secret value" -> "new secret value" 24 | } 25 | } 26 | + other_value = jsonencode( 27 | { 28 | + key = "value" 29 | } 30 | ) 31 | ~ sensitive = "some secret value" -> "new secret value" 32 | ~ sensitive_json = jsonencode( 33 | { 34 | ~ key = "value" -> "value2" 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lucas Bremgartner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMain0(t *testing.T) { 15 | files, err := filepath.Glob(filepath.Join("testdata", "*")) 16 | require.NoError(t, err) 17 | 18 | for _, filename := range files { 19 | filename := filename 20 | t.Run(filename, func(t *testing.T) { 21 | defer func(orig *os.File) { 22 | os.Stdout = orig 23 | }(os.Stdout) 24 | 25 | r, w, err := os.Pipe() 26 | require.NoError(t, err) 27 | 28 | var copyErr error 29 | buf := bytes.NewBuffer(nil) 30 | wg := sync.WaitGroup{} 31 | wg.Add(1) 32 | go func() { 33 | defer wg.Done() 34 | _, copyErr = io.Copy(buf, r) 35 | }() 36 | 37 | os.Stdout = w 38 | 39 | err = main0([]string{"tfreveal", "--no-color", filepath.Join(filename, "plan.json")}) 40 | require.NoError(t, err) 41 | err = w.Close() 42 | require.NoError(t, err) 43 | 44 | want, err := os.ReadFile(filepath.Join(filename, "output.golden")) 45 | require.NoError(t, err) 46 | 47 | wg.Wait() 48 | require.NoError(t, copyErr) 49 | 50 | require.Equal(t, string(want), buf.String()) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | build: 5 | cmds: 6 | - go build ./... 7 | 8 | gen-all: 9 | cmds: 10 | - for: 11 | - advanced 12 | - outputs 13 | - resource_create 14 | - resource_delete 15 | - resource_replace 16 | - resource_update 17 | - sensitive 18 | task: generate-one 19 | vars: 20 | RESOURCE_DIR: '{{ .ITEM }}' 21 | 22 | gen-*: 23 | cmds: 24 | - task: generate-one 25 | vars: 26 | RESOURCE_DIR: '{{index .MATCH 0}}' 27 | 28 | generate-one: 29 | internal: true 30 | cmds: 31 | - rm -rf testdata/tmp 32 | - mkdir testdata/tmp 33 | - terraform -chdir=testdata/tmp init -reconfigure -from-module=../{{ .RESOURCE_DIR }}/before 34 | - terraform -chdir=testdata/tmp apply -auto-approve 35 | - cp testdata/{{ .RESOURCE_DIR }}/after/main.tf testdata/tmp 36 | - terraform -chdir=testdata/tmp plan -out plan.out 37 | - terraform -chdir=testdata/tmp show -json plan.out | jq . > testdata/{{ .RESOURCE_DIR }}/plan.json 38 | - rm -rf testdata/tmp 39 | - go run . --no-color testdata/{{ .RESOURCE_DIR }}/plan.json > testdata/{{ .RESOURCE_DIR }}/output.golden 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Main 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | main: 15 | name: Main Process 16 | runs-on: ubuntu-latest 17 | env: 18 | GO_VERSION: "stable" 19 | GOLANGCI_LINT_VERSION: v1.64.6 20 | CGO_ENABLED: 0 21 | 22 | steps: 23 | - name: Set up Go ${{ env.GO_VERSION }} 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ env.GO_VERSION }} 27 | cache: false 28 | 29 | - name: Check out code 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Cache Go modules 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/go/pkg/mod 38 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 39 | restore-keys: | 40 | ${{ runner.os }}-go- 41 | 42 | - name: Check and get dependencies 43 | run: | 44 | go mod tidy 45 | git diff --exit-code go.mod 46 | git diff --exit-code go.sum 47 | 48 | - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} 49 | run: | 50 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} 51 | golangci-lint --version 52 | 53 | - name: Lint 54 | run: golangci-lint run 55 | -------------------------------------------------------------------------------- /.github/workflows/go-cross.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Go Matrix 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | cross: 14 | name: Go 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | CGO_ENABLED: 0 18 | 19 | strategy: 20 | matrix: 21 | go-version: [ "stable", "oldstable", "1.x" ] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | steps: 25 | - name: Set up Go ${{ matrix.go-version }} 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | cache: false 30 | 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | 34 | - name: Cache Go modules 35 | uses: actions/cache@v4 36 | with: 37 | # In order: 38 | # * Module download cache 39 | # * Build cache (Linux) 40 | # * Build cache (Mac) 41 | # * Build cache (Windows) 42 | path: | 43 | ~/go/pkg/mod 44 | ~/.cache/go-build 45 | ~/Library/Caches/go-build 46 | %LocalAppData%\go-build 47 | key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }} 48 | restore-keys: | 49 | ${{ runner.os }}-${{ matrix.go-version }}-go- 50 | 51 | - name: Test 52 | run: go test -v -cover ./... 53 | 54 | - name: Build 55 | run: go build -ldflags "-s -w" -trimpath ./... 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # === Go === 2 | 3 | # If you prefer the allow list template instead of the deny list, see community template: 4 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 5 | # 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | tfreveal 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | # Go workspace file 24 | go.work 25 | go.work.sum 26 | 27 | dist/ 28 | testdata/tmp 29 | 30 | # === Terraform === 31 | 32 | # Local .terraform directories 33 | **/.terraform/* 34 | 35 | # .tfstate files 36 | *.tfstate 37 | *.tfstate.* 38 | 39 | # Crash log files 40 | crash.log 41 | crash.*.log 42 | 43 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 44 | # password, private keys, and other secrets. These should not be part of version 45 | # control as they are data points which are potentially sensitive and subject 46 | # to change depending on the environment. 47 | *.tfvars 48 | *.tfvars.json 49 | 50 | # Ignore override files as they are usually used to override resources locally and so 51 | # are not checked in 52 | override.tf 53 | override.tf.json 54 | *_override.tf 55 | *_override.tf.json 56 | 57 | # Include override files you do wish to add to version control using negated pattern 58 | # !example_override.tf 59 | 60 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 61 | # example: *tfplan* 62 | 63 | # Ignore CLI configuration files 64 | .terraformrc 65 | terraform.rc 66 | 67 | .terraform.lock.hcl 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/breml/tfreveal 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/breml/jsondiffprinter v0.0.12 7 | github.com/ghetzel/go-stockutil v1.13.0 8 | github.com/hashicorp/terraform-json v0.27.2 9 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db 10 | github.com/stretchr/testify v1.11.1 11 | github.com/urfave/cli/v2 v2.27.7 12 | github.com/wI2L/jsondiff v0.7.0 13 | ) 14 | 15 | require ( 16 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 17 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/fatih/structs v1.1.0 // indirect 20 | github.com/ghetzel/uuid v0.0.0-20171129191014-dec09d789f3d // indirect 21 | github.com/hashicorp/errwrap v1.1.0 // indirect 22 | github.com/hashicorp/go-multierror v1.1.1 // indirect 23 | github.com/hashicorp/go-version v1.7.0 // indirect 24 | github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 // indirect 25 | github.com/jdkato/prose v1.2.1 // indirect 26 | github.com/juliangruber/go-intersect v1.1.0 // indirect 27 | github.com/mitchellh/mapstructure v1.5.0 // indirect 28 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 30 | github.com/tidwall/gjson v1.18.0 // indirect 31 | github.com/tidwall/match v1.1.1 // indirect 32 | github.com/tidwall/pretty v1.2.1 // indirect 33 | github.com/tidwall/sjson v1.2.5 // indirect 34 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 35 | github.com/zclconf/go-cty v1.16.4 // indirect 36 | golang.org/x/text v0.23.0 // indirect 37 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 38 | gopkg.in/neurosnap/sentences.v1 v1.0.7 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | k8s.io/client-go v0.33.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /testdata/resource_delete/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "root_module": {} 6 | }, 7 | "resource_changes": [ 8 | { 9 | "address": "null_resource.cluster", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "cluster", 13 | "provider_name": "registry.terraform.io/hashicorp/null", 14 | "change": { 15 | "actions": [ 16 | "delete" 17 | ], 18 | "before": { 19 | "id": "939058779081368068", 20 | "triggers": { 21 | "secret": "very very secure" 22 | } 23 | }, 24 | "after": null, 25 | "after_unknown": {}, 26 | "before_sensitive": { 27 | "triggers": {} 28 | }, 29 | "after_sensitive": false 30 | }, 31 | "action_reason": "delete_because_no_resource_config" 32 | } 33 | ], 34 | "prior_state": { 35 | "format_version": "1.0", 36 | "terraform_version": "1.7.2", 37 | "values": { 38 | "root_module": { 39 | "resources": [ 40 | { 41 | "address": "null_resource.cluster", 42 | "mode": "managed", 43 | "type": "null_resource", 44 | "name": "cluster", 45 | "provider_name": "registry.terraform.io/hashicorp/null", 46 | "schema_version": 0, 47 | "values": { 48 | "id": "939058779081368068", 49 | "triggers": { 50 | "secret": "very very secure" 51 | } 52 | }, 53 | "sensitive_values": { 54 | "triggers": {} 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | }, 61 | "configuration": { 62 | "provider_config": { 63 | "null": { 64 | "name": "null", 65 | "full_name": "registry.terraform.io/hashicorp/null", 66 | "version_constraint": "3.2.2" 67 | } 68 | }, 69 | "root_module": {} 70 | }, 71 | "timestamp": "2024-05-26T15:18:08Z", 72 | "errored": false 73 | } 74 | -------------------------------------------------------------------------------- /testdata/advanced/before/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | 8 | local = { 9 | source = "hashicorp/local" 10 | version = "2.5.1" 11 | } 12 | 13 | time = { 14 | source = "hashicorp/time" 15 | version = "0.11.1" 16 | } 17 | } 18 | } 19 | 20 | provider "null" {} 21 | 22 | provider "local" {} 23 | 24 | provider "time" {} 25 | 26 | resource "null_resource" "cluster_old" { 27 | triggers = { 28 | secret = "very very secure" 29 | } 30 | } 31 | 32 | resource "null_resource" "cluster" { 33 | triggers = { 34 | secret = "secure" 35 | } 36 | } 37 | 38 | resource "time_offset" "example" { 39 | offset_days = 7 40 | } 41 | 42 | resource "local_file" "foo" { 43 | content = jsonencode({ 44 | array_unchanged = [ 45 | "foo", 46 | "bar", 47 | "baz", 48 | ] 49 | array_changed = [ 50 | "foo", 51 | "bar", 52 | "baz", 53 | ] 54 | array_removed = [ 55 | "foo", 56 | "bar", 57 | "baz", 58 | ] 59 | array_item_removed = [ 60 | "foo", 61 | "bar", 62 | "baz", 63 | ] 64 | array_item_added = [ 65 | "foo", 66 | "bar", 67 | "baz", 68 | ] 69 | array_to_object = [ 70 | "foo", 71 | "bar", 72 | "baz", 73 | ] 74 | number_unchanged = 10 75 | number_changed = 10 76 | number_removed = 10 77 | object_unchanged = { 78 | key = "value" 79 | } 80 | object_changed = { 81 | key = "value" 82 | } 83 | object_removed = { 84 | key = "value" 85 | } 86 | string_unchanged = "foo" 87 | string_changed = "bar" 88 | string_removed = "removed" 89 | }) 90 | filename = "${path.module}/foo.bar" 91 | } 92 | 93 | resource "local_file" "string2json" { 94 | content = "some random string" 95 | filename = "${path.module}/string2json" 96 | } 97 | 98 | resource "local_file" "json2string" { 99 | content = jsonencode({ 100 | "key": "some random json" 101 | }) 102 | filename = "${path.module}/json2string" 103 | } 104 | 105 | output "string" { 106 | value = "some text" 107 | } 108 | -------------------------------------------------------------------------------- /testdata/resource_create/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "root_module": { 6 | "resources": [ 7 | { 8 | "address": "null_resource.cluster", 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "cluster", 12 | "provider_name": "registry.terraform.io/hashicorp/null", 13 | "schema_version": 0, 14 | "values": { 15 | "triggers": { 16 | "secret": "very very secure" 17 | } 18 | }, 19 | "sensitive_values": { 20 | "triggers": {} 21 | } 22 | } 23 | ] 24 | } 25 | }, 26 | "resource_changes": [ 27 | { 28 | "address": "null_resource.cluster", 29 | "mode": "managed", 30 | "type": "null_resource", 31 | "name": "cluster", 32 | "provider_name": "registry.terraform.io/hashicorp/null", 33 | "change": { 34 | "actions": [ 35 | "create" 36 | ], 37 | "before": null, 38 | "after": { 39 | "triggers": { 40 | "secret": "very very secure" 41 | } 42 | }, 43 | "after_unknown": { 44 | "id": true, 45 | "triggers": {} 46 | }, 47 | "before_sensitive": false, 48 | "after_sensitive": { 49 | "triggers": {} 50 | } 51 | } 52 | } 53 | ], 54 | "configuration": { 55 | "provider_config": { 56 | "null": { 57 | "name": "null", 58 | "full_name": "registry.terraform.io/hashicorp/null", 59 | "version_constraint": "3.2.2" 60 | } 61 | }, 62 | "root_module": { 63 | "resources": [ 64 | { 65 | "address": "null_resource.cluster", 66 | "mode": "managed", 67 | "type": "null_resource", 68 | "name": "cluster", 69 | "provider_config_key": "null", 70 | "expressions": { 71 | "triggers": { 72 | "constant_value": { 73 | "secret": "very very secure" 74 | } 75 | } 76 | }, 77 | "schema_version": 0 78 | } 79 | ] 80 | } 81 | }, 82 | "timestamp": "2024-05-26T15:18:06Z", 83 | "errored": false 84 | } 85 | -------------------------------------------------------------------------------- /testdata/advanced/after/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { 4 | source = "hashicorp/null" 5 | version = "3.2.2" 6 | } 7 | 8 | local = { 9 | source = "hashicorp/local" 10 | version = "2.5.1" 11 | } 12 | 13 | time = { 14 | source = "hashicorp/time" 15 | version = "0.11.1" 16 | } 17 | } 18 | } 19 | 20 | provider "null" {} 21 | 22 | provider "local" {} 23 | 24 | provider "time" {} 25 | 26 | resource "null_resource" "cluster_new" { 27 | triggers = { 28 | secret = "very very secure" 29 | } 30 | } 31 | 32 | resource "null_resource" "cluster" { 33 | triggers = { 34 | secret = "still secure" 35 | } 36 | } 37 | 38 | resource "time_offset" "example" { 39 | offset_days = 8 40 | } 41 | 42 | resource "local_file" "foo" { 43 | content = jsonencode({ 44 | array_unchanged = [ 45 | "foo", 46 | "bar", 47 | "baz", 48 | ] 49 | array_changed = [ 50 | "foo2", 51 | "bar", 52 | "baz2", 53 | ] 54 | array_new = [ 55 | "foo", 56 | "bar", 57 | "baz", 58 | ] 59 | array_item_removed = [ 60 | "foo", 61 | "baz", 62 | ] 63 | array_item_added = [ 64 | "foo", 65 | "bar", 66 | "baz", 67 | "biz", 68 | ] 69 | array_to_object = { 70 | 0 = "foo" 71 | 1 = "bar" 72 | 2 = "baz" 73 | } 74 | number_unchanged = 10 75 | number_changed = 14 76 | number_new = 14 77 | object_unchanged = { 78 | key = "value" 79 | } 80 | object_changed = { 81 | key = "new value" 82 | } 83 | object_new = { 84 | key = "value" 85 | } 86 | string_unchanged = "foo" 87 | string_changed = "bar changed" 88 | string_new = "new" 89 | }) 90 | filename = "${path.module}/foo.bar" 91 | file_permission = "0660" 92 | } 93 | 94 | resource "local_file" "string2json" { 95 | content = jsonencode({ 96 | "key": "some random json" 97 | }) 98 | filename = "${path.module}/string2json" 99 | } 100 | 101 | resource "local_file" "json2string" { 102 | content = "some random string" 103 | filename = "${path.module}/json2string" 104 | } 105 | 106 | output "string" { 107 | value = "some text 1" 108 | } 109 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | tfjson "github.com/hashicorp/terraform-json" 10 | "github.com/mitchellh/colorstring" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | var ( 15 | version = "dev" 16 | commit = "none" 17 | date = "unknown" 18 | ) 19 | 20 | func main() { 21 | if err := main0(os.Args); err != nil { 22 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func main0(osArgs []string) error { 28 | app := App{} 29 | 30 | executionPlanLegend := colorstring.Color(` 31 | Resource actions are indicated with the following symbols: 32 | [green]+[reset] create 33 | [yellow]~[reset] update in-place 34 | [red]-[reset] destroy 35 | [red]-[reset]/[green]+[reset] destroy and then create replacement 36 | [green]+[reset]/[red]-[reset] create replacement and then destroy 37 | `) 38 | 39 | cliapp := &cli.App{ 40 | Name: "tfreveal", 41 | Usage: "Show an execution plan with all sensitive values revealed.", 42 | Action: app.Reveal, 43 | Flags: []cli.Flag{ 44 | &cli.BoolFlag{ 45 | Name: "no-color", 46 | Usage: "Disable colorized output", 47 | Destination: &app.noColor, 48 | }, 49 | &cli.BoolFlag{ 50 | Name: "show-unchanged", 51 | Usage: "Show unchanged attributes", 52 | Destination: &app.showUnchanged, 53 | }, 54 | }, 55 | CustomAppHelpTemplate: cli.AppHelpTemplate + executionPlanLegend, 56 | 57 | Copyright: "© 2024, Lucas Bremgartner", 58 | Version: fmt.Sprintf("%s (%s, %s)", version, commit, date), 59 | } 60 | return cliapp.Run(osArgs) 61 | } 62 | 63 | type App struct { 64 | noColor bool 65 | showUnchanged bool 66 | } 67 | 68 | func (a *App) Reveal(c *cli.Context) error { 69 | var source io.Reader = os.Stdin 70 | 71 | if c.Args().Present() { 72 | file, err := os.Open(c.Args().First()) 73 | if err != nil { 74 | return fmt.Errorf(`open source: %w`, err) 75 | } 76 | defer file.Close() 77 | source = file 78 | } 79 | 80 | data, err := io.ReadAll(source) 81 | if err != nil { 82 | return fmt.Errorf(`read from source: %w`, err) 83 | } 84 | 85 | plan := tfjson.Plan{} 86 | err = json.Unmarshal(data, &plan) 87 | if err != nil { 88 | return fmt.Errorf(`unmarshal Terraform plan json: %w`, err) 89 | } 90 | 91 | fmt.Println("The provided execution plan contains the following changes.") 92 | fmt.Println() 93 | fmt.Print(a.resourceChanges(plan)) 94 | fmt.Print(a.outputChanges(plan)) 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfreveal 2 | 3 | [![Test Status](https://github.com/breml/tfreveal/workflows/Main/badge.svg)](https://github.com/breml/tfreveal/actions?query=workflow%3AMain) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/breml/tfreveal)](https://goreportcard.com/report/github.com/breml/tfreveal) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 5 | 6 | tfreveal is an open-source tool designed to enhance the visibility of Terraform 7 | plan files by displaying all differences in resources and outputs, including 8 | sensitive values. Unlike Terraform, which hides sensitive data, tfreveal reveals 9 | these values to ensure complete transparency in your infrastructure changes. 10 | 11 | [![asciicast](https://asciinema.org/a/672302.svg)](https://asciinema.org/a/672302) 12 | 13 | ## Motivation 14 | 15 | Terraform does mask sensitive values in the output (e.g. from `terraform plan`) 16 | in order to protect them from being revealed to unauthorized 3rd parties. 17 | 18 | But sometimes it is neccessary to see the exact changes, Terraform will perform 19 | to the infrastructure including all the changes to sensitive values. In 20 | particular, if one observes drift between the Terraform state and the actual 21 | state of the infrastructure, this becomes inevitable. So far, Terraform does not 22 | provide a feature to forcefully unmask the sensitive values in the 23 | [concise diff plan outputs](https://www.hashicorp.com/blog/terraform-0-14-adds-a-new-concise-diff-format-to-terraform-plans). 24 | 25 | The general advice given by the Terraform maintainers is to use the JSON output 26 | in such cases. While the JSON output does provide all the necessary information, 27 | it is not perticularely easy to read for humans and to spot small differences. 28 | It gets even more complicated, if the changes are contained in larger JSON 29 | encoded values, that are marked as sensitive. 30 | 31 | There exists instructions using for example `jq`, but the process stays manual, 32 | cumbersome and error prone. 33 | 34 | `tfreveal` is here to fix this and provide an easy way to show the concise diff 35 | plan outputs with all sensitive values revealed. 36 | 37 | ## Installation 38 | 39 | Download the latest release from the [releases page](https://github.com/breml/tfreveal/releases). 40 | 41 | ## Usage 42 | 43 | The plan file generated from Terraform can be directly piped to `tfreveal`: 44 | 45 | ```bash 46 | $ terraform plan -out plan.out 47 | $ terraform show -json plan.out | tfreveal 48 | ``` 49 | 50 | Alternatively, the plan file can also be passed as argument: 51 | 52 | ```bash 53 | $ terraform plan -out plan.out 54 | $ terraform show -json plan.out > plan.json 55 | $ tfreveal plan.json 56 | ``` 57 | 58 | ## Development 59 | 60 | The task to update the test data and the golden files is provided in the 61 | `Taskfile.yml` and can be executed by running `task gen-all`. This requires the 62 | `task` tool to be installed. Please refer to the 63 | [official documentation](https://taskfile.dev/installation/). 64 | 65 | Additionally the `terraform` command needs to be present in the `PATH`. Follow 66 | the [official installation instructions](https://developer.hashicorp.com/terraform/install). 67 | 68 | ## Author 69 | 70 | Copyright 2024 by Lucas Bremgartner ([breml](https://github.com/breml)) 71 | 72 | ## License 73 | 74 | [MIT License](LICENSE) 75 | 76 | ## Trademarks 77 | 78 | All other trademarks referenced herein are the property of their respective owners. 79 | -------------------------------------------------------------------------------- /testdata/resource_replace/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "root_module": { 6 | "resources": [ 7 | { 8 | "address": "null_resource.cluster", 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "cluster", 12 | "provider_name": "registry.terraform.io/hashicorp/null", 13 | "schema_version": 0, 14 | "values": { 15 | "triggers": { 16 | "secret": "very very secure" 17 | } 18 | }, 19 | "sensitive_values": { 20 | "triggers": {} 21 | } 22 | } 23 | ] 24 | } 25 | }, 26 | "resource_changes": [ 27 | { 28 | "address": "null_resource.cluster", 29 | "mode": "managed", 30 | "type": "null_resource", 31 | "name": "cluster", 32 | "provider_name": "registry.terraform.io/hashicorp/null", 33 | "change": { 34 | "actions": [ 35 | "delete", 36 | "create" 37 | ], 38 | "before": { 39 | "id": "5350362168280616586", 40 | "triggers": { 41 | "secret": "secure" 42 | } 43 | }, 44 | "after": { 45 | "triggers": { 46 | "secret": "very very secure" 47 | } 48 | }, 49 | "after_unknown": { 50 | "id": true, 51 | "triggers": {} 52 | }, 53 | "before_sensitive": { 54 | "triggers": {} 55 | }, 56 | "after_sensitive": { 57 | "triggers": {} 58 | }, 59 | "replace_paths": [ 60 | [ 61 | "triggers" 62 | ] 63 | ] 64 | }, 65 | "action_reason": "replace_because_cannot_update" 66 | } 67 | ], 68 | "prior_state": { 69 | "format_version": "1.0", 70 | "terraform_version": "1.7.2", 71 | "values": { 72 | "root_module": { 73 | "resources": [ 74 | { 75 | "address": "null_resource.cluster", 76 | "mode": "managed", 77 | "type": "null_resource", 78 | "name": "cluster", 79 | "provider_name": "registry.terraform.io/hashicorp/null", 80 | "schema_version": 0, 81 | "values": { 82 | "id": "5350362168280616586", 83 | "triggers": { 84 | "secret": "secure" 85 | } 86 | }, 87 | "sensitive_values": { 88 | "triggers": {} 89 | } 90 | } 91 | ] 92 | } 93 | } 94 | }, 95 | "configuration": { 96 | "provider_config": { 97 | "null": { 98 | "name": "null", 99 | "full_name": "registry.terraform.io/hashicorp/null", 100 | "version_constraint": "3.2.2" 101 | } 102 | }, 103 | "root_module": { 104 | "resources": [ 105 | { 106 | "address": "null_resource.cluster", 107 | "mode": "managed", 108 | "type": "null_resource", 109 | "name": "cluster", 110 | "provider_config_key": "null", 111 | "expressions": { 112 | "triggers": { 113 | "constant_value": { 114 | "secret": "very very secure" 115 | } 116 | } 117 | }, 118 | "schema_version": 0 119 | } 120 | ] 121 | } 122 | }, 123 | "timestamp": "2024-05-26T15:18:09Z", 124 | "errored": false 125 | } 126 | -------------------------------------------------------------------------------- /testdata/resource_update/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "root_module": { 6 | "resources": [ 7 | { 8 | "address": "time_offset.example", 9 | "mode": "managed", 10 | "type": "time_offset", 11 | "name": "example", 12 | "provider_name": "registry.terraform.io/hashicorp/time", 13 | "schema_version": 0, 14 | "values": { 15 | "base_rfc3339": "2024-05-26T15:18:10Z", 16 | "day": 3, 17 | "hour": 15, 18 | "id": "2024-05-26T15:18:10Z", 19 | "minute": 18, 20 | "month": 6, 21 | "offset_days": 8, 22 | "offset_hours": null, 23 | "offset_minutes": null, 24 | "offset_months": null, 25 | "offset_seconds": null, 26 | "offset_years": null, 27 | "rfc3339": "2024-06-03T15:18:10Z", 28 | "second": 10, 29 | "triggers": null, 30 | "unix": 1717427890, 31 | "year": 2024 32 | }, 33 | "sensitive_values": {} 34 | } 35 | ] 36 | } 37 | }, 38 | "resource_changes": [ 39 | { 40 | "address": "time_offset.example", 41 | "mode": "managed", 42 | "type": "time_offset", 43 | "name": "example", 44 | "provider_name": "registry.terraform.io/hashicorp/time", 45 | "change": { 46 | "actions": [ 47 | "update" 48 | ], 49 | "before": { 50 | "base_rfc3339": "2024-05-26T15:18:10Z", 51 | "day": 2, 52 | "hour": 15, 53 | "id": "2024-05-26T15:18:10Z", 54 | "minute": 18, 55 | "month": 6, 56 | "offset_days": 7, 57 | "offset_hours": null, 58 | "offset_minutes": null, 59 | "offset_months": null, 60 | "offset_seconds": null, 61 | "offset_years": null, 62 | "rfc3339": "2024-06-02T15:18:10Z", 63 | "second": 10, 64 | "triggers": null, 65 | "unix": 1717341490, 66 | "year": 2024 67 | }, 68 | "after": { 69 | "base_rfc3339": "2024-05-26T15:18:10Z", 70 | "day": 3, 71 | "hour": 15, 72 | "id": "2024-05-26T15:18:10Z", 73 | "minute": 18, 74 | "month": 6, 75 | "offset_days": 8, 76 | "offset_hours": null, 77 | "offset_minutes": null, 78 | "offset_months": null, 79 | "offset_seconds": null, 80 | "offset_years": null, 81 | "rfc3339": "2024-06-03T15:18:10Z", 82 | "second": 10, 83 | "triggers": null, 84 | "unix": 1717427890, 85 | "year": 2024 86 | }, 87 | "after_unknown": {}, 88 | "before_sensitive": {}, 89 | "after_sensitive": {} 90 | } 91 | } 92 | ], 93 | "prior_state": { 94 | "format_version": "1.0", 95 | "terraform_version": "1.7.2", 96 | "values": { 97 | "root_module": { 98 | "resources": [ 99 | { 100 | "address": "time_offset.example", 101 | "mode": "managed", 102 | "type": "time_offset", 103 | "name": "example", 104 | "provider_name": "registry.terraform.io/hashicorp/time", 105 | "schema_version": 0, 106 | "values": { 107 | "base_rfc3339": "2024-05-26T15:18:10Z", 108 | "day": 2, 109 | "hour": 15, 110 | "id": "2024-05-26T15:18:10Z", 111 | "minute": 18, 112 | "month": 6, 113 | "offset_days": 7, 114 | "offset_hours": null, 115 | "offset_minutes": null, 116 | "offset_months": null, 117 | "offset_seconds": null, 118 | "offset_years": null, 119 | "rfc3339": "2024-06-02T15:18:10Z", 120 | "second": 10, 121 | "triggers": null, 122 | "unix": 1717341490, 123 | "year": 2024 124 | }, 125 | "sensitive_values": {} 126 | } 127 | ] 128 | } 129 | } 130 | }, 131 | "configuration": { 132 | "provider_config": { 133 | "time": { 134 | "name": "time", 135 | "full_name": "registry.terraform.io/hashicorp/time", 136 | "version_constraint": "0.11.1" 137 | } 138 | }, 139 | "root_module": { 140 | "resources": [ 141 | { 142 | "address": "time_offset.example", 143 | "mode": "managed", 144 | "type": "time_offset", 145 | "name": "example", 146 | "provider_config_key": "time", 147 | "expressions": { 148 | "offset_days": { 149 | "constant_value": 8 150 | } 151 | }, 152 | "schema_version": 0 153 | } 154 | ] 155 | } 156 | }, 157 | "timestamp": "2024-05-26T15:18:10Z", 158 | "errored": false 159 | } 160 | -------------------------------------------------------------------------------- /resources/tfreveal.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 209, "height": 51, "timestamp": 1723800851, "idle_time_limit": 3.0, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}, "title": "tfreveal"} 2 | [0.018058, "o", "$ "] 3 | [1.011409, "o", "t"] 4 | [1.210462, "o", "e"] 5 | [1.449761, "o", "r"] 6 | [1.593245, "o", "r"] 7 | [1.784655, "o", "a"] 8 | [2.135778, "o", "f"] 9 | [2.303313, "o", "o"] 10 | [2.488217, "o", "r"] 11 | [2.630827, "o", "m"] 12 | [3.253111, "o", " "] 13 | [3.629153, "o", "p"] 14 | [3.860281, "o", "l"] 15 | [4.020036, "o", "a"] 16 | [4.116258, "o", "n"] 17 | [4.292557, "o", " "] 18 | [4.84028, "o", "-"] 19 | [5.096364, "o", "o"] 20 | [5.249848, "o", "u"] 21 | [5.407821, "o", "t"] 22 | [5.63759, "o", " "] 23 | [6.182006, "o", "p"] 24 | [6.389167, "o", "l"] 25 | [6.571166, "o", "a"] 26 | [6.714918, "o", "n"] 27 | [7.144546, "o", "."] 28 | [7.456428, "o", "o"] 29 | [7.592493, "o", "u"] 30 | [7.736033, "o", "t"] 31 | [8.182719, "o", "\r\n\u001b[?2004l\r"] 32 | [8.28449, "o", "\u001b[0m\u001b[1mnull_resource.sensitive: Refreshing state... [id=779033444032252607]\u001b[0m\r\n"] 33 | [8.293545, "o", "\r\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:\r\n\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m destroy and then create replacement\u001b[0m\r\n\r\nTerraform will perform the following actions:\r\n\r\n\u001b[1m # null_resource.sensitive\u001b[0m must be \u001b[1m\u001b[31mreplaced\u001b[0m\r\n\u001b[0m\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m\u001b[0m resource \"null_resource\" \"sensitive\" {\r\n \u001b[33m~\u001b[0m\u001b[0m id = \"779033444032252607\" -> (known after apply)\r\n \u001b[33m~\u001b[0m\u001b[0m triggers = { \u001b[31m# forces replacement\u001b[0m\u001b[0m\r\n \u001b[33m~\u001b[0m\u001b[0m \"secret\" = (sensitive value)\r\n }\r\n }\r\n\r\n\u001b[1mPlan:\u001b[0m 1 to add, 0 to change, 1 to destroy.\r\n\u001b[0m\r\nChanges to Outputs:\r\n \u001b[33m~\u001b[0m\u001b[0m json_with_nested_sensitive = (sensitive value)\r\n \u001b[33m~\u001b[0m\u001b[0m null_resource = (sensitive value)\r\n \u001b[33m~\u001b[0m\u001b[0m sensitive = (sensitive value)\r\n \u001b[33m~\u001b[0m\u001b[0m sensitive_json = (sensitive value)\r\n\u001b[90m\r\n────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\r\n\r\nSaved the plan to: plan.out\r\n\r\nTo perform exactly these actions, run the following command to apply:\r\n terraform apply \"plan.out\"\r\n"] 34 | [8.299675, "o", "$ "] 35 | [13.701412, "o", "t"] 36 | [14.434745, "o", "e"] 37 | [14.713196, "o", "r"] 38 | [14.848999, "o", "r"] 39 | [15.076409, "o", "a"] 40 | [15.637754, "o", "f"] 41 | [15.787253, "o", "o"] 42 | [15.960045, "o", "r"] 43 | [16.353591, "o", "m"] 44 | [16.545223, "o", " "] 45 | [16.782131, "o", "s"] 46 | [16.878969, "o", "h"] 47 | [17.015434, "o", "o"] 48 | [17.173707, "o", "w"] 49 | [17.750892, "o", " "] 50 | [18.657051, "o", "-"] 51 | [18.970233, "o", "j"] 52 | [19.117325, "o", "s"] 53 | [19.213684, "o", "o"] 54 | [19.410394, "o", "n"] 55 | [19.833309, "o", " "] 56 | [20.256719, "o", "p"] 57 | [20.446091, "o", "l"] 58 | [20.545028, "o", "a"] 59 | [20.644554, "o", "n"] 60 | [20.945021, "o", "."] 61 | [21.261249, "o", "o"] 62 | [21.407778, "o", "u"] 63 | [21.556152, "o", "t"] 64 | [22.145026, "o", " "] 65 | [22.984971, "o", "|"] 66 | [23.22992, "o", " "] 67 | [24.154724, "o", "t"] 68 | [24.645254, "o", "f"] 69 | [24.983468, "o", "r"] 70 | [25.080573, "o", "e"] 71 | [25.413524, "o", "v"] 72 | [25.673612, "o", "e"] 73 | [25.804847, "o", "a"] 74 | [25.941391, "o", "l"] 75 | [26.529072, "o", "\r\n\u001b[?2004l\r"] 76 | [26.623703, "o", "The provided execution plan contains the following changes.\r\n\r\n"] 77 | [26.62414, "o", "Changes to Resources:\r\n\r\n \u001b[97m# null_resource.sensitive\u001b[0m must be \u001b[91mreplaced\u001b[0m\r\n\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m null_resource.sensitive = {\r\n \u001b[33m~\u001b[0m id = \"779033444032252607\" \u001b[33m->\u001b[0m \"(known after apply)\"\r\n triggers = { \u001b[91m# forces replacement\u001b[0m\r\n \u001b[33m~\u001b[0m secret = \"some secret value\" \u001b[33m->\u001b[0m \"new secret value\"\r\n }\r\n }\r\n\r\nChanges to Outputs:\r\n\r\n \u001b[33m~\u001b[0m json_with_nested_sensitive = jsonencode(\r\n {\r\n \u001b[33m~\u001b[0m key = \"value\" \u001b[33m->\u001b[0m \"value2\"\r\n }\r\n )\r\n \u001b[33m~\u001b[0m null_resource = {\r\n \u001b[33m~\u001b[0m id = \"779033444032252607\" \u001b[33m->\u001b[0m \"(known after apply)\"\r\n triggers = {\r\n \u001b[33m~\u001b[0m secret = \"some secret value\" \u001b[33m->\u001b[0m \"new secret value\"\r\n }\r\n }\r\n \u001b[33m~\u001b[0m sensitive = \"some secret value\" \u001b[33m->\u001b[0m \"new secret value\"\r\n \u001b[33m~\u001b[0m sensitive_json = jsonencode(\r\n {\r\n \u001b[33m~\u001b[0m key = \"value\" \u001b[33m->\u001b[0m \"value2\"\r\n }\r\n )\r\n"] 78 | [26.631321, "o", "$ "] 79 | [28.404596, "o", "\u001b[?2004l\r\r\nexit\r\n"] 80 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/ghetzel/go-stockutil/maputil" 10 | tfjson "github.com/hashicorp/terraform-json" 11 | "github.com/mitchellh/colorstring" 12 | "github.com/wI2L/jsondiff" 13 | 14 | "github.com/breml/jsondiffprinter" 15 | ) 16 | 17 | func (a *App) resourceChanges(plan tfjson.Plan) string { 18 | colorize := colorstring.Colorize{ 19 | Colors: colorstring.DefaultColors, 20 | Disable: a.noColor, 21 | } 22 | 23 | buf := strings.Builder{} 24 | 25 | for _, v := range plan.ResourceChanges { 26 | if v.Change.Actions.NoOp() { 27 | continue 28 | } 29 | 30 | diffString := a.diff(v.Change) 31 | 32 | if len(v.Change.ReplacePaths) > 0 { 33 | buf.WriteString(colorize.Color(fmt.Sprintf(" [white]# %s[reset] must be [light_red]replaced[reset]\n", v.Address))) 34 | } 35 | buf.WriteString(fmt.Sprintf("%s %s = %s\n", a.marker(v.Change.Actions), v.Address, indent(diffString, 2))) 36 | } 37 | 38 | if buf.Len() == 0 { 39 | return "" 40 | } 41 | 42 | return fmt.Sprintf("Changes to Resources:\n\n%s", buf.String()) 43 | } 44 | 45 | func (a *App) outputChanges(plan tfjson.Plan) string { 46 | buf := strings.Builder{} 47 | 48 | outputs := make([]string, 0, len(plan.OutputChanges)) 49 | for k := range plan.OutputChanges { 50 | outputs = append(outputs, k) 51 | } 52 | sort.Strings(outputs) 53 | 54 | for _, k := range outputs { 55 | v := plan.OutputChanges[k] 56 | if v.Actions.NoOp() { 57 | continue 58 | } 59 | 60 | diffString := a.diff(v) 61 | 62 | buf.WriteString(fmt.Sprintf("%s %s = %s", a.marker(v.Actions), k, indent(diffString, 2))) 63 | } 64 | 65 | if buf.Len() == 0 { 66 | return "" 67 | } 68 | 69 | return fmt.Sprintf("Changes to Outputs:\n\n%s", buf.String()) 70 | } 71 | 72 | func (a *App) diff(change *tfjson.Change) string { 73 | if change.Actions.NoOp() { 74 | return "" 75 | } 76 | 77 | err := maputil.Walk(change.AfterUnknown, func(value interface{}, path []string, isLeaf bool) error { 78 | if val, _ := value.(bool); val { 79 | maputil.DeepSet(change.After, path, "(known after apply)") 80 | } 81 | return nil 82 | }) 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | patch, err := jsondiff.Compare(change.Before, change.After) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | replacePaths := make([]string, 0, len(change.ReplacePaths)) 93 | for _, replacePath := range change.ReplacePaths { 94 | rp := replacePath.([]any) 95 | var path string 96 | for _, pathSegment := range rp { 97 | path += "/" + fmt.Sprint(pathSegment) 98 | } 99 | replacePaths = append(replacePaths, path) 100 | } 101 | 102 | buf := &strings.Builder{} 103 | options := []jsondiffprinter.Option{ 104 | jsondiffprinter.WithTerraformDefaults(), 105 | jsondiffprinter.WithWriter(buf), 106 | jsondiffprinter.WithIndentation(" "), 107 | jsondiffprinter.WithHideUnchanged(!a.showUnchanged), 108 | jsondiffprinter.WithJSONinJSONCompare(compare), 109 | jsondiffprinter.WithColor(!a.noColor), 110 | jsondiffprinter.WithPatchSeriesPostProcess(func(diff jsondiffprinter.Patch) jsondiffprinter.Patch { 111 | colorize := colorstring.Colorize{ 112 | Colors: colorstring.DefaultColors, 113 | Disable: a.noColor, 114 | } 115 | 116 | outerLoop: 117 | for _, path := range replacePaths { 118 | for i := range diff { 119 | if diff[i].Path.String() == path { 120 | if diff[i].Metadata == nil { 121 | diff[i].Metadata = make(map[string]string, 0) 122 | } 123 | diff[i].Metadata["note"] = colorize.Color(" [light_red]# forces replacement[reset]") 124 | continue outerLoop 125 | } 126 | } 127 | } 128 | return diff 129 | }), 130 | } 131 | err = jsondiffprinter.Format(change.Before, patch, options...) 132 | if err != nil { 133 | panic(err) 134 | } 135 | 136 | return strings.TrimLeft(buf.String(), " ") 137 | } 138 | 139 | func compare(before, after any) ([]byte, error) { 140 | patch, err := jsondiff.Compare(before, after) 141 | if err != nil { 142 | return nil, err 143 | } 144 | return json.Marshal(patch) 145 | } 146 | 147 | func indent(in string, indent int) string { 148 | lines := strings.Split(in, "\n") 149 | for i := range lines[1:] { 150 | lines[i] = strings.Repeat(" ", indent) + lines[i] 151 | } 152 | lines[0] = strings.TrimLeft(lines[0], " ") 153 | return strings.Join(lines, "\n") 154 | } 155 | 156 | func (a *App) marker(actions tfjson.Actions) string { 157 | colorize := colorstring.Colorize{ 158 | Colors: colorstring.DefaultColors, 159 | Disable: a.noColor, 160 | } 161 | switch { 162 | case actions.Create(): 163 | return colorize.Color(" [green]+[reset]") 164 | case actions.CreateBeforeDestroy(): 165 | return colorize.Color("[green]+[reset]/[red]-[reset]") 166 | case actions.Delete(): 167 | return colorize.Color(" [red]-[reset]") 168 | case actions.DestroyBeforeCreate(): 169 | return colorize.Color("[red]-[reset]/[green]+[reset]") 170 | case actions.NoOp(): 171 | return colorize.Color("") 172 | case actions.Read(): 173 | return colorize.Color("") 174 | case actions.Replace(): 175 | return colorize.Color("[red]-[reset]/[green]+[reset]") 176 | case actions.Update(): 177 | return colorize.Color(" [yellow]~[reset]") 178 | default: 179 | return colorize.Color("[red]! [reset]") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /testdata/advanced/output.golden: -------------------------------------------------------------------------------- 1 | The provided execution plan contains the following changes. 2 | 3 | Changes to Resources: 4 | 5 | # local_file.foo must be replaced 6 | -/+ local_file.foo = { 7 | ~ content = jsonencode( 8 | { 9 | array_changed = [ 10 | ~ "foo" -> "foo2" 11 | "bar" 12 | ~ "baz" -> "baz2" 13 | ] 14 | array_item_added = [ 15 | "foo" 16 | "bar" 17 | "baz" 18 | + "biz" 19 | ] 20 | array_item_removed = [ 21 | "foo" 22 | ~ "bar" -> "baz" 23 | - "baz" 24 | ] 25 | + array_new = [ 26 | + "foo" 27 | + "bar" 28 | + "baz" 29 | ] 30 | - array_removed = [ 31 | - "foo" 32 | - "bar" 33 | - "baz" 34 | ] 35 | ~ array_to_object = [ 36 | - "foo" 37 | - "bar" 38 | - "baz" 39 | ] -> { 40 | + 0 = "foo" 41 | + 1 = "bar" 42 | + 2 = "baz" 43 | } 44 | ~ number_changed = 10 -> 14 45 | + number_new = 14 46 | - number_removed = 10 47 | object_changed = { 48 | ~ key = "value" -> "new value" 49 | } 50 | + object_new = { 51 | + key = "value" 52 | } 53 | - object_removed = { 54 | - key = "value" 55 | } 56 | ~ string_changed = "bar" -> "bar changed" 57 | + string_new = "new" 58 | - string_removed = "removed" 59 | # (4 unchanged attribute hidden) 60 | } 61 | ) # forces replacement 62 | ~ content_base64sha256 = "nIMhVlzPvgCIzeahBGJ/jEJLuCBuwIqx78vltuKjcyw=" -> "(known after apply)" 63 | ~ content_base64sha512 = "WDetH1vI/Yy0tu49ZCyW/WySxstlQc3+T7vX0Ei0UaLvLKsd884Djd3raxFJS66NMgFZc1bpSxPKKDXmgz9drA==" -> "(known after apply)" 64 | ~ content_md5 = "6f61fcba1ffd2b366fe358450f93b952" -> "(known after apply)" 65 | ~ content_sha1 = "4dfb08a62fbd3d8737cd6a11c2360df3953dda51" -> "(known after apply)" 66 | ~ content_sha256 = "9c8321565ccfbe0088cde6a104627f8c424bb8206ec08ab1efcbe5b6e2a3732c" -> "(known after apply)" 67 | ~ content_sha512 = "5837ad1f5bc8fd8cb4b6ee3d642c96fd6c92c6cb6541cdfe4fbbd7d048b451a2ef2cab1df3ce038dddeb6b11494bae8d3201597356e94b13ca2835e6833f5dac" -> "(known after apply)" 68 | ~ file_permission = "0777" -> "0660" # forces replacement 69 | ~ id = "4dfb08a62fbd3d8737cd6a11c2360df3953dda51" -> "(known after apply)" 70 | # (5 unchanged attribute hidden) 71 | } 72 | 73 | # local_file.json2string must be replaced 74 | -/+ local_file.json2string = { 75 | ~ content = jsonencode( 76 | { 77 | - key = "some random json" 78 | } 79 | ) -> "some random string" # forces replacement 80 | ~ content_base64sha256 = "HYsNaOjaBgmFTRuYj4T4J4Ve7InaRVmfw2wgDbwr8Cs=" -> "(known after apply)" 81 | ~ content_base64sha512 = "COm1PXnJevPHVeZququDXk5+U5hDeA4dLmTnUpdeMhoF/3+isI46Hgkg5Dln9HCVN7Ythqjq4mrIP4t67bq3Ow==" -> "(known after apply)" 82 | ~ content_md5 = "31dd41a76bb193c928caf767ac8e8340" -> "(known after apply)" 83 | ~ content_sha1 = "72e1eb91ff698efdaa1a82b5977634a04cf2985d" -> "(known after apply)" 84 | ~ content_sha256 = "1d8b0d68e8da0609854d1b988f84f827855eec89da45599fc36c200dbc2bf02b" -> "(known after apply)" 85 | ~ content_sha512 = "08e9b53d79c97af3c755e66abaab835e4e7e539843780e1d2e64e752975e321a05ff7fa2b08e3a1e0920e43967f4709537b62d86a8eae26ac83f8b7aedbab73b" -> "(known after apply)" 86 | ~ id = "72e1eb91ff698efdaa1a82b5977634a04cf2985d" -> "(known after apply)" 87 | # (6 unchanged attribute hidden) 88 | } 89 | 90 | # local_file.string2json must be replaced 91 | -/+ local_file.string2json = { 92 | ~ content = "some random string" -> jsonencode( 93 | { 94 | + key = "some random json" 95 | } 96 | ) # forces replacement 97 | ~ content_base64sha256 = "Q0LgjQHySsRRvDu/9oZAhWO1wm04bQwYh5JE9eeXTX0=" -> "(known after apply)" 98 | ~ content_base64sha512 = "eaxV3784RhIgGecPVkfZ+wZlola4sUqDaG/V8Hr+Y5LKG7a3UJ5UkF9VRA4UYOGmtnlGI0hcm7nVJeq2u1AGFw==" -> "(known after apply)" 99 | ~ content_md5 = "76712b27e483bc0ba2ce8d2109210c22" -> "(known after apply)" 100 | ~ content_sha1 = "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8" -> "(known after apply)" 101 | ~ content_sha256 = "4342e08d01f24ac451bc3bbff686408563b5c26d386d0c18879244f5e7974d7d" -> "(known after apply)" 102 | ~ content_sha512 = "79ac55dfbf3846122019e70f5647d9fb0665a256b8b14a83686fd5f07afe6392ca1bb6b7509e54905f55440e1460e1a6b6794623485c9bb9d525eab6bb500617" -> "(known after apply)" 103 | ~ id = "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8" -> "(known after apply)" 104 | # (6 unchanged attribute hidden) 105 | } 106 | 107 | # null_resource.cluster must be replaced 108 | -/+ null_resource.cluster = { 109 | ~ id = "2582010537129087805" -> "(known after apply)" 110 | triggers = { # forces replacement 111 | ~ secret = "secure" -> "still secure" 112 | } 113 | } 114 | 115 | + null_resource.cluster_new = { 116 | + id = "(known after apply)" 117 | + triggers = { 118 | + secret = "very very secure" 119 | } 120 | } 121 | 122 | - null_resource.cluster_old = { 123 | - id = "6045924575212971352" 124 | - triggers = { 125 | - secret = "very very secure" 126 | } 127 | } 128 | 129 | ~ time_offset.example = { 130 | ~ day = 16 -> 17 131 | ~ offset_days = 7 -> 8 132 | ~ rfc3339 = "2024-08-16T06:48:48Z" -> "2024-08-17T06:48:48Z" 133 | ~ unix = 1723790928 -> 1723877328 134 | # (13 unchanged attribute hidden) 135 | } 136 | 137 | Changes to Outputs: 138 | 139 | ~ string = "some text" -> "some text 1" 140 | -------------------------------------------------------------------------------- /testdata/outputs/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "outputs": { 6 | "array": { 7 | "sensitive": false, 8 | "type": [ 9 | "tuple", 10 | [ 11 | "string", 12 | "string", 13 | "string" 14 | ] 15 | ], 16 | "value": [ 17 | "foo", 18 | "more", 19 | "bar" 20 | ] 21 | }, 22 | "jsonString": { 23 | "sensitive": false, 24 | "type": "string", 25 | "value": "{\"key1\":\"1\",\"key2\":\"2 added\",\"key3\":\"3 changed\"}" 26 | }, 27 | "number_add": { 28 | "sensitive": false, 29 | "type": "number", 30 | "value": 30 31 | }, 32 | "number_change": { 33 | "sensitive": false, 34 | "type": "number", 35 | "value": 12 36 | }, 37 | "number_equal": { 38 | "sensitive": false, 39 | "type": "number", 40 | "value": 5 41 | }, 42 | "object": { 43 | "sensitive": false, 44 | "type": [ 45 | "object", 46 | { 47 | "key1": "string", 48 | "key2": "string", 49 | "key3": "string" 50 | } 51 | ], 52 | "value": { 53 | "key1": "1", 54 | "key2": "2 added", 55 | "key3": "3 changed" 56 | } 57 | }, 58 | "string": { 59 | "sensitive": false, 60 | "type": "string", 61 | "value": "some text 1" 62 | } 63 | }, 64 | "root_module": {} 65 | }, 66 | "output_changes": { 67 | "array": { 68 | "actions": [ 69 | "update" 70 | ], 71 | "before": [ 72 | "foo", 73 | "bar" 74 | ], 75 | "after": [ 76 | "foo", 77 | "more", 78 | "bar" 79 | ], 80 | "after_unknown": false, 81 | "before_sensitive": false, 82 | "after_sensitive": false 83 | }, 84 | "jsonString": { 85 | "actions": [ 86 | "update" 87 | ], 88 | "before": "{\"key1\":\"1\",\"key3\":\"3\",\"key4\":\"4 remove\"}", 89 | "after": "{\"key1\":\"1\",\"key2\":\"2 added\",\"key3\":\"3 changed\"}", 90 | "after_unknown": false, 91 | "before_sensitive": false, 92 | "after_sensitive": false 93 | }, 94 | "number_add": { 95 | "actions": [ 96 | "create" 97 | ], 98 | "before": null, 99 | "after": 30, 100 | "after_unknown": false, 101 | "before_sensitive": false, 102 | "after_sensitive": false 103 | }, 104 | "number_change": { 105 | "actions": [ 106 | "update" 107 | ], 108 | "before": 10, 109 | "after": 12, 110 | "after_unknown": false, 111 | "before_sensitive": false, 112 | "after_sensitive": false 113 | }, 114 | "number_equal": { 115 | "actions": [ 116 | "no-op" 117 | ], 118 | "before": 5, 119 | "after": 5, 120 | "after_unknown": false, 121 | "before_sensitive": false, 122 | "after_sensitive": false 123 | }, 124 | "number_remove": { 125 | "actions": [ 126 | "delete" 127 | ], 128 | "before": 20, 129 | "after": null, 130 | "after_unknown": false, 131 | "before_sensitive": false, 132 | "after_sensitive": false 133 | }, 134 | "object": { 135 | "actions": [ 136 | "update" 137 | ], 138 | "before": { 139 | "key1": "1", 140 | "key3": "3", 141 | "key4": "4 remove" 142 | }, 143 | "after": { 144 | "key1": "1", 145 | "key2": "2 added", 146 | "key3": "3 changed" 147 | }, 148 | "after_unknown": false, 149 | "before_sensitive": false, 150 | "after_sensitive": false 151 | }, 152 | "string": { 153 | "actions": [ 154 | "update" 155 | ], 156 | "before": "some text", 157 | "after": "some text 1", 158 | "after_unknown": false, 159 | "before_sensitive": false, 160 | "after_sensitive": false 161 | } 162 | }, 163 | "prior_state": { 164 | "format_version": "1.0", 165 | "terraform_version": "1.7.2", 166 | "values": { 167 | "outputs": { 168 | "array": { 169 | "sensitive": false, 170 | "value": [ 171 | "foo", 172 | "more", 173 | "bar" 174 | ], 175 | "type": [ 176 | "tuple", 177 | [ 178 | "string", 179 | "string", 180 | "string" 181 | ] 182 | ] 183 | }, 184 | "jsonString": { 185 | "sensitive": false, 186 | "value": "{\"key1\":\"1\",\"key2\":\"2 added\",\"key3\":\"3 changed\"}", 187 | "type": "string" 188 | }, 189 | "number_add": { 190 | "sensitive": false, 191 | "value": 30, 192 | "type": "number" 193 | }, 194 | "number_change": { 195 | "sensitive": false, 196 | "value": 12, 197 | "type": "number" 198 | }, 199 | "number_equal": { 200 | "sensitive": false, 201 | "value": 5, 202 | "type": "number" 203 | }, 204 | "number_remove": { 205 | "sensitive": false, 206 | "value": 20, 207 | "type": "number" 208 | }, 209 | "object": { 210 | "sensitive": false, 211 | "value": { 212 | "key1": "1", 213 | "key2": "2 added", 214 | "key3": "3 changed" 215 | }, 216 | "type": [ 217 | "object", 218 | { 219 | "key1": "string", 220 | "key2": "string", 221 | "key3": "string" 222 | } 223 | ] 224 | }, 225 | "string": { 226 | "sensitive": false, 227 | "value": "some text 1", 228 | "type": "string" 229 | } 230 | }, 231 | "root_module": {} 232 | } 233 | }, 234 | "configuration": { 235 | "root_module": { 236 | "outputs": { 237 | "array": { 238 | "expression": { 239 | "constant_value": [ 240 | "foo", 241 | "more", 242 | "bar" 243 | ] 244 | } 245 | }, 246 | "jsonString": { 247 | "expression": {} 248 | }, 249 | "number_add": { 250 | "expression": { 251 | "constant_value": 30 252 | } 253 | }, 254 | "number_change": { 255 | "expression": { 256 | "constant_value": 12 257 | } 258 | }, 259 | "number_equal": { 260 | "expression": { 261 | "constant_value": 5 262 | } 263 | }, 264 | "object": { 265 | "expression": { 266 | "constant_value": { 267 | "key1": "1", 268 | "key2": "2 added", 269 | "key3": "3 changed" 270 | } 271 | } 272 | }, 273 | "string": { 274 | "expression": { 275 | "constant_value": "some text 1" 276 | } 277 | } 278 | } 279 | } 280 | }, 281 | "timestamp": "2024-05-26T15:18:05Z", 282 | "errored": false 283 | } 284 | -------------------------------------------------------------------------------- /testdata/sensitive/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.2", 4 | "planned_values": { 5 | "outputs": { 6 | "json_with_nested_sensitive": { 7 | "sensitive": false, 8 | "type": "string", 9 | "value": "{\"key\":\"value2\"}" 10 | }, 11 | "null_resource": { 12 | "sensitive": true 13 | }, 14 | "other_value": { 15 | "sensitive": false, 16 | "type": "string", 17 | "value": "{\"key\":\"value\"}" 18 | }, 19 | "sensitive": { 20 | "sensitive": false, 21 | "type": "string", 22 | "value": "new secret value" 23 | }, 24 | "sensitive_json": { 25 | "sensitive": false, 26 | "type": "string", 27 | "value": "{\"key\":\"value2\"}" 28 | } 29 | }, 30 | "root_module": { 31 | "resources": [ 32 | { 33 | "address": "null_resource.sensitive", 34 | "mode": "managed", 35 | "type": "null_resource", 36 | "name": "sensitive", 37 | "provider_name": "registry.terraform.io/hashicorp/null", 38 | "schema_version": 0, 39 | "values": { 40 | "triggers": { 41 | "secret": "new secret value" 42 | } 43 | }, 44 | "sensitive_values": { 45 | "triggers": { 46 | "secret": true 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | }, 53 | "resource_changes": [ 54 | { 55 | "address": "null_resource.sensitive", 56 | "mode": "managed", 57 | "type": "null_resource", 58 | "name": "sensitive", 59 | "provider_name": "registry.terraform.io/hashicorp/null", 60 | "change": { 61 | "actions": [ 62 | "delete", 63 | "create" 64 | ], 65 | "before": { 66 | "id": "4190156157480914441", 67 | "triggers": { 68 | "secret": "some secret value" 69 | } 70 | }, 71 | "after": { 72 | "triggers": { 73 | "secret": "new secret value" 74 | } 75 | }, 76 | "after_unknown": { 77 | "id": true, 78 | "triggers": {} 79 | }, 80 | "before_sensitive": { 81 | "triggers": { 82 | "secret": true 83 | } 84 | }, 85 | "after_sensitive": { 86 | "triggers": { 87 | "secret": true 88 | } 89 | }, 90 | "replace_paths": [ 91 | [ 92 | "triggers" 93 | ] 94 | ] 95 | }, 96 | "action_reason": "replace_because_cannot_update" 97 | } 98 | ], 99 | "output_changes": { 100 | "json_with_nested_sensitive": { 101 | "actions": [ 102 | "update" 103 | ], 104 | "before": "{\"key\":\"value\"}", 105 | "after": "{\"key\":\"value2\"}", 106 | "after_unknown": false, 107 | "before_sensitive": false, 108 | "after_sensitive": false 109 | }, 110 | "null_resource": { 111 | "actions": [ 112 | "update" 113 | ], 114 | "before": { 115 | "id": "4190156157480914441", 116 | "triggers": { 117 | "secret": "some secret value" 118 | } 119 | }, 120 | "after": { 121 | "triggers": { 122 | "secret": "new secret value" 123 | } 124 | }, 125 | "after_unknown": { 126 | "id": true, 127 | "triggers": {} 128 | }, 129 | "before_sensitive": true, 130 | "after_sensitive": true 131 | }, 132 | "other_value": { 133 | "actions": [ 134 | "create" 135 | ], 136 | "before": null, 137 | "after": "{\"key\":\"value\"}", 138 | "after_unknown": false, 139 | "before_sensitive": false, 140 | "after_sensitive": false 141 | }, 142 | "sensitive": { 143 | "actions": [ 144 | "update" 145 | ], 146 | "before": "some secret value", 147 | "after": "new secret value", 148 | "after_unknown": false, 149 | "before_sensitive": false, 150 | "after_sensitive": false 151 | }, 152 | "sensitive_json": { 153 | "actions": [ 154 | "update" 155 | ], 156 | "before": "{\"key\":\"value\"}", 157 | "after": "{\"key\":\"value2\"}", 158 | "after_unknown": false, 159 | "before_sensitive": false, 160 | "after_sensitive": false 161 | } 162 | }, 163 | "prior_state": { 164 | "format_version": "1.0", 165 | "terraform_version": "1.7.2", 166 | "values": { 167 | "outputs": { 168 | "json_with_nested_sensitive": { 169 | "sensitive": false, 170 | "value": "{\"key\":\"value2\"}", 171 | "type": "string" 172 | }, 173 | "null_resource": { 174 | "sensitive": true, 175 | "value": { 176 | "id": "4190156157480914441", 177 | "triggers": { 178 | "secret": "some secret value" 179 | } 180 | }, 181 | "type": [ 182 | "object", 183 | { 184 | "id": "string", 185 | "triggers": [ 186 | "map", 187 | "string" 188 | ] 189 | } 190 | ] 191 | }, 192 | "other_value": { 193 | "sensitive": false, 194 | "value": "{\"key\":\"value\"}", 195 | "type": "string" 196 | }, 197 | "sensitive": { 198 | "sensitive": false, 199 | "value": "new secret value", 200 | "type": "string" 201 | }, 202 | "sensitive_json": { 203 | "sensitive": false, 204 | "value": "{\"key\":\"value2\"}", 205 | "type": "string" 206 | } 207 | }, 208 | "root_module": { 209 | "resources": [ 210 | { 211 | "address": "null_resource.sensitive", 212 | "mode": "managed", 213 | "type": "null_resource", 214 | "name": "sensitive", 215 | "provider_name": "registry.terraform.io/hashicorp/null", 216 | "schema_version": 0, 217 | "values": { 218 | "id": "4190156157480914441", 219 | "triggers": { 220 | "secret": "some secret value" 221 | } 222 | }, 223 | "sensitive_values": { 224 | "triggers": { 225 | "secret": true 226 | } 227 | } 228 | } 229 | ] 230 | } 231 | } 232 | }, 233 | "configuration": { 234 | "provider_config": { 235 | "null": { 236 | "name": "null", 237 | "full_name": "registry.terraform.io/hashicorp/null" 238 | } 239 | }, 240 | "root_module": { 241 | "outputs": { 242 | "json_with_nested_sensitive": { 243 | "expression": { 244 | "references": [ 245 | "local.json_with_nested_sensitive" 246 | ] 247 | } 248 | }, 249 | "null_resource": { 250 | "sensitive": true, 251 | "expression": { 252 | "references": [ 253 | "null_resource.sensitive" 254 | ] 255 | } 256 | }, 257 | "other_value": { 258 | "expression": { 259 | "references": [ 260 | "local.other_value" 261 | ] 262 | } 263 | }, 264 | "sensitive": { 265 | "expression": { 266 | "references": [ 267 | "null_resource.sensitive.triggers.secret", 268 | "null_resource.sensitive.triggers", 269 | "null_resource.sensitive" 270 | ] 271 | } 272 | }, 273 | "sensitive_json": { 274 | "expression": { 275 | "references": [ 276 | "local.sensitive_json" 277 | ] 278 | } 279 | } 280 | }, 281 | "resources": [ 282 | { 283 | "address": "null_resource.sensitive", 284 | "mode": "managed", 285 | "type": "null_resource", 286 | "name": "sensitive", 287 | "provider_config_key": "null", 288 | "expressions": { 289 | "triggers": { 290 | "references": [ 291 | "local.secret_value" 292 | ] 293 | } 294 | }, 295 | "schema_version": 0 296 | } 297 | ] 298 | } 299 | }, 300 | "relevant_attributes": [ 301 | { 302 | "resource": "null_resource.sensitive", 303 | "attribute": [ 304 | "triggers", 305 | "secret" 306 | ] 307 | }, 308 | { 309 | "resource": "null_resource.sensitive", 310 | "attribute": [] 311 | } 312 | ], 313 | "timestamp": "2024-05-26T15:18:12Z", 314 | "errored": false 315 | } 316 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= 2 | github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= 3 | github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= 4 | github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 5 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 6 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 7 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 8 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 9 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 10 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 11 | github.com/breml/jsondiffprinter v0.0.12 h1:ZZRPmqDBtyLIZBQbsqRpPZFue52EWTMXKGaNQbwQlfM= 12 | github.com/breml/jsondiffprinter v0.0.12/go.mod h1:8Tjh6/fPQs5Iu3tyXBeX/zJyHzRHxftxOjxG2of+5IA= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 19 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 20 | github.com/ghetzel/go-stockutil v1.13.0 h1:tA6QkzZ65EBzV5/Cx10Lv2PSw0YSoHhQ+37Imlpcy+E= 21 | github.com/ghetzel/go-stockutil v1.13.0/go.mod h1:9BuwZzs0hZSJGuLzthx4yXjERZHRMkUQXzWVx8uRCVs= 22 | github.com/ghetzel/testify v1.4.1 h1:wpJirdM+znAnxWruGDBdIys5aU+wGJHNUTkgEo4PYwk= 23 | github.com/ghetzel/testify v1.4.1/go.mod h1:FwvFn1OiGEUgzhS3ySCjTBG7/sez0WRvOAxz5uQU8so= 24 | github.com/ghetzel/uuid v0.0.0-20171129191014-dec09d789f3d h1:YVJe7KwVYazt90hCc/q2dYJVS3062AY6QdT6iHd+Kh8= 25 | github.com/ghetzel/uuid v0.0.0-20171129191014-dec09d789f3d/go.mod h1:7CCemW/spiphukVWb/v2WWYeZkydh30TwSRBh48irZQ= 26 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 27 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 28 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 29 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 30 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 31 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 32 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 33 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 34 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 35 | github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= 36 | github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= 37 | github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 h1:4zOlv2my+vf98jT1nQt4bT/yKWUImevYPJ2H344CloE= 38 | github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6/go.mod h1:r/8JmuR0qjuCiEhAolkfvdZgmPiHTnJaG0UXCSeR1Zo= 39 | github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= 40 | github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= 41 | github.com/juliangruber/go-intersect v1.1.0 h1:sc+y5dCjMMx0pAdYk/N6KBm00tD/f3tq+Iox7dYDUrY= 42 | github.com/juliangruber/go-intersect v1.1.0/go.mod h1:WMau+1kAmnlQnKiikekNJbtGtfmILU/mMU6H7AgKbWQ= 43 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 44 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 45 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 46 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 49 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 50 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 51 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 52 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 53 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 54 | github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 55 | github.com/neurosnap/sentences v1.0.6 h1:iBVUivNtlwGkYsJblWV8GGVFmXzZzak907Ci8aA0VTE= 56 | github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 61 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 62 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 63 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 64 | github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 68 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 69 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 70 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 71 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 72 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 73 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 74 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 75 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 76 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 77 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 78 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 79 | github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= 80 | github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= 81 | github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= 82 | github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= 83 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 84 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 85 | github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= 86 | github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 87 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 88 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 89 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 90 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 91 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 92 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 95 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= 97 | gopkg.in/neurosnap/sentences.v1 v1.0.7 h1:gpTUYnqthem4+o8kyTLiYIB05W+IvdQFYR29erfe8uU= 98 | gopkg.in/neurosnap/sentences.v1 v1.0.7/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= 99 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 100 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 105 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 106 | -------------------------------------------------------------------------------- /testdata/advanced/plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.2", 3 | "terraform_version": "1.7.4", 4 | "planned_values": { 5 | "outputs": { 6 | "string": { 7 | "sensitive": false, 8 | "type": "string", 9 | "value": "some text 1" 10 | } 11 | }, 12 | "root_module": { 13 | "resources": [ 14 | { 15 | "address": "local_file.foo", 16 | "mode": "managed", 17 | "type": "local_file", 18 | "name": "foo", 19 | "provider_name": "registry.terraform.io/hashicorp/local", 20 | "schema_version": 0, 21 | "values": { 22 | "content": "{\"array_changed\":[\"foo2\",\"bar\",\"baz2\"],\"array_item_added\":[\"foo\",\"bar\",\"baz\",\"biz\"],\"array_item_removed\":[\"foo\",\"baz\"],\"array_new\":[\"foo\",\"bar\",\"baz\"],\"array_to_object\":{\"0\":\"foo\",\"1\":\"bar\",\"2\":\"baz\"},\"array_unchanged\":[\"foo\",\"bar\",\"baz\"],\"number_changed\":14,\"number_new\":14,\"number_unchanged\":10,\"object_changed\":{\"key\":\"new value\"},\"object_new\":{\"key\":\"value\"},\"object_unchanged\":{\"key\":\"value\"},\"string_changed\":\"bar changed\",\"string_new\":\"new\",\"string_unchanged\":\"foo\"}", 23 | "content_base64": null, 24 | "directory_permission": "0777", 25 | "file_permission": "0660", 26 | "filename": "./foo.bar", 27 | "sensitive_content": null, 28 | "source": null 29 | }, 30 | "sensitive_values": {} 31 | }, 32 | { 33 | "address": "local_file.json2string", 34 | "mode": "managed", 35 | "type": "local_file", 36 | "name": "json2string", 37 | "provider_name": "registry.terraform.io/hashicorp/local", 38 | "schema_version": 0, 39 | "values": { 40 | "content": "some random string", 41 | "content_base64": null, 42 | "directory_permission": "0777", 43 | "file_permission": "0777", 44 | "filename": "./json2string", 45 | "sensitive_content": null, 46 | "source": null 47 | }, 48 | "sensitive_values": {} 49 | }, 50 | { 51 | "address": "local_file.string2json", 52 | "mode": "managed", 53 | "type": "local_file", 54 | "name": "string2json", 55 | "provider_name": "registry.terraform.io/hashicorp/local", 56 | "schema_version": 0, 57 | "values": { 58 | "content": "{\"key\":\"some random json\"}", 59 | "content_base64": null, 60 | "directory_permission": "0777", 61 | "file_permission": "0777", 62 | "filename": "./string2json", 63 | "sensitive_content": null, 64 | "source": null 65 | }, 66 | "sensitive_values": {} 67 | }, 68 | { 69 | "address": "null_resource.cluster", 70 | "mode": "managed", 71 | "type": "null_resource", 72 | "name": "cluster", 73 | "provider_name": "registry.terraform.io/hashicorp/null", 74 | "schema_version": 0, 75 | "values": { 76 | "triggers": { 77 | "secret": "still secure" 78 | } 79 | }, 80 | "sensitive_values": { 81 | "triggers": {} 82 | } 83 | }, 84 | { 85 | "address": "null_resource.cluster_new", 86 | "mode": "managed", 87 | "type": "null_resource", 88 | "name": "cluster_new", 89 | "provider_name": "registry.terraform.io/hashicorp/null", 90 | "schema_version": 0, 91 | "values": { 92 | "triggers": { 93 | "secret": "very very secure" 94 | } 95 | }, 96 | "sensitive_values": { 97 | "triggers": {} 98 | } 99 | }, 100 | { 101 | "address": "time_offset.example", 102 | "mode": "managed", 103 | "type": "time_offset", 104 | "name": "example", 105 | "provider_name": "registry.terraform.io/hashicorp/time", 106 | "schema_version": 0, 107 | "values": { 108 | "base_rfc3339": "2024-08-09T06:48:48Z", 109 | "day": 17, 110 | "hour": 6, 111 | "id": "2024-08-09T06:48:48Z", 112 | "minute": 48, 113 | "month": 8, 114 | "offset_days": 8, 115 | "offset_hours": null, 116 | "offset_minutes": null, 117 | "offset_months": null, 118 | "offset_seconds": null, 119 | "offset_years": null, 120 | "rfc3339": "2024-08-17T06:48:48Z", 121 | "second": 48, 122 | "triggers": null, 123 | "unix": 1723877328, 124 | "year": 2024 125 | }, 126 | "sensitive_values": {} 127 | } 128 | ] 129 | } 130 | }, 131 | "resource_changes": [ 132 | { 133 | "address": "local_file.foo", 134 | "mode": "managed", 135 | "type": "local_file", 136 | "name": "foo", 137 | "provider_name": "registry.terraform.io/hashicorp/local", 138 | "change": { 139 | "actions": [ 140 | "delete", 141 | "create" 142 | ], 143 | "before": { 144 | "content": "{\"array_changed\":[\"foo\",\"bar\",\"baz\"],\"array_item_added\":[\"foo\",\"bar\",\"baz\"],\"array_item_removed\":[\"foo\",\"bar\",\"baz\"],\"array_removed\":[\"foo\",\"bar\",\"baz\"],\"array_to_object\":[\"foo\",\"bar\",\"baz\"],\"array_unchanged\":[\"foo\",\"bar\",\"baz\"],\"number_changed\":10,\"number_removed\":10,\"number_unchanged\":10,\"object_changed\":{\"key\":\"value\"},\"object_removed\":{\"key\":\"value\"},\"object_unchanged\":{\"key\":\"value\"},\"string_changed\":\"bar\",\"string_removed\":\"removed\",\"string_unchanged\":\"foo\"}", 145 | "content_base64": null, 146 | "content_base64sha256": "nIMhVlzPvgCIzeahBGJ/jEJLuCBuwIqx78vltuKjcyw=", 147 | "content_base64sha512": "WDetH1vI/Yy0tu49ZCyW/WySxstlQc3+T7vX0Ei0UaLvLKsd884Djd3raxFJS66NMgFZc1bpSxPKKDXmgz9drA==", 148 | "content_md5": "6f61fcba1ffd2b366fe358450f93b952", 149 | "content_sha1": "4dfb08a62fbd3d8737cd6a11c2360df3953dda51", 150 | "content_sha256": "9c8321565ccfbe0088cde6a104627f8c424bb8206ec08ab1efcbe5b6e2a3732c", 151 | "content_sha512": "5837ad1f5bc8fd8cb4b6ee3d642c96fd6c92c6cb6541cdfe4fbbd7d048b451a2ef2cab1df3ce038dddeb6b11494bae8d3201597356e94b13ca2835e6833f5dac", 152 | "directory_permission": "0777", 153 | "file_permission": "0777", 154 | "filename": "./foo.bar", 155 | "id": "4dfb08a62fbd3d8737cd6a11c2360df3953dda51", 156 | "sensitive_content": null, 157 | "source": null 158 | }, 159 | "after": { 160 | "content": "{\"array_changed\":[\"foo2\",\"bar\",\"baz2\"],\"array_item_added\":[\"foo\",\"bar\",\"baz\",\"biz\"],\"array_item_removed\":[\"foo\",\"baz\"],\"array_new\":[\"foo\",\"bar\",\"baz\"],\"array_to_object\":{\"0\":\"foo\",\"1\":\"bar\",\"2\":\"baz\"},\"array_unchanged\":[\"foo\",\"bar\",\"baz\"],\"number_changed\":14,\"number_new\":14,\"number_unchanged\":10,\"object_changed\":{\"key\":\"new value\"},\"object_new\":{\"key\":\"value\"},\"object_unchanged\":{\"key\":\"value\"},\"string_changed\":\"bar changed\",\"string_new\":\"new\",\"string_unchanged\":\"foo\"}", 161 | "content_base64": null, 162 | "directory_permission": "0777", 163 | "file_permission": "0660", 164 | "filename": "./foo.bar", 165 | "sensitive_content": null, 166 | "source": null 167 | }, 168 | "after_unknown": { 169 | "content_base64sha256": true, 170 | "content_base64sha512": true, 171 | "content_md5": true, 172 | "content_sha1": true, 173 | "content_sha256": true, 174 | "content_sha512": true, 175 | "id": true 176 | }, 177 | "before_sensitive": { 178 | "sensitive_content": true 179 | }, 180 | "after_sensitive": { 181 | "sensitive_content": true 182 | }, 183 | "replace_paths": [ 184 | [ 185 | "file_permission" 186 | ], 187 | [ 188 | "content" 189 | ] 190 | ] 191 | }, 192 | "action_reason": "replace_because_cannot_update" 193 | }, 194 | { 195 | "address": "local_file.json2string", 196 | "mode": "managed", 197 | "type": "local_file", 198 | "name": "json2string", 199 | "provider_name": "registry.terraform.io/hashicorp/local", 200 | "change": { 201 | "actions": [ 202 | "delete", 203 | "create" 204 | ], 205 | "before": { 206 | "content": "{\"key\":\"some random json\"}", 207 | "content_base64": null, 208 | "content_base64sha256": "HYsNaOjaBgmFTRuYj4T4J4Ve7InaRVmfw2wgDbwr8Cs=", 209 | "content_base64sha512": "COm1PXnJevPHVeZququDXk5+U5hDeA4dLmTnUpdeMhoF/3+isI46Hgkg5Dln9HCVN7Ythqjq4mrIP4t67bq3Ow==", 210 | "content_md5": "31dd41a76bb193c928caf767ac8e8340", 211 | "content_sha1": "72e1eb91ff698efdaa1a82b5977634a04cf2985d", 212 | "content_sha256": "1d8b0d68e8da0609854d1b988f84f827855eec89da45599fc36c200dbc2bf02b", 213 | "content_sha512": "08e9b53d79c97af3c755e66abaab835e4e7e539843780e1d2e64e752975e321a05ff7fa2b08e3a1e0920e43967f4709537b62d86a8eae26ac83f8b7aedbab73b", 214 | "directory_permission": "0777", 215 | "file_permission": "0777", 216 | "filename": "./json2string", 217 | "id": "72e1eb91ff698efdaa1a82b5977634a04cf2985d", 218 | "sensitive_content": null, 219 | "source": null 220 | }, 221 | "after": { 222 | "content": "some random string", 223 | "content_base64": null, 224 | "directory_permission": "0777", 225 | "file_permission": "0777", 226 | "filename": "./json2string", 227 | "sensitive_content": null, 228 | "source": null 229 | }, 230 | "after_unknown": { 231 | "content_base64sha256": true, 232 | "content_base64sha512": true, 233 | "content_md5": true, 234 | "content_sha1": true, 235 | "content_sha256": true, 236 | "content_sha512": true, 237 | "id": true 238 | }, 239 | "before_sensitive": { 240 | "sensitive_content": true 241 | }, 242 | "after_sensitive": { 243 | "sensitive_content": true 244 | }, 245 | "replace_paths": [ 246 | [ 247 | "content" 248 | ] 249 | ] 250 | }, 251 | "action_reason": "replace_because_cannot_update" 252 | }, 253 | { 254 | "address": "local_file.string2json", 255 | "mode": "managed", 256 | "type": "local_file", 257 | "name": "string2json", 258 | "provider_name": "registry.terraform.io/hashicorp/local", 259 | "change": { 260 | "actions": [ 261 | "delete", 262 | "create" 263 | ], 264 | "before": { 265 | "content": "some random string", 266 | "content_base64": null, 267 | "content_base64sha256": "Q0LgjQHySsRRvDu/9oZAhWO1wm04bQwYh5JE9eeXTX0=", 268 | "content_base64sha512": "eaxV3784RhIgGecPVkfZ+wZlola4sUqDaG/V8Hr+Y5LKG7a3UJ5UkF9VRA4UYOGmtnlGI0hcm7nVJeq2u1AGFw==", 269 | "content_md5": "76712b27e483bc0ba2ce8d2109210c22", 270 | "content_sha1": "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8", 271 | "content_sha256": "4342e08d01f24ac451bc3bbff686408563b5c26d386d0c18879244f5e7974d7d", 272 | "content_sha512": "79ac55dfbf3846122019e70f5647d9fb0665a256b8b14a83686fd5f07afe6392ca1bb6b7509e54905f55440e1460e1a6b6794623485c9bb9d525eab6bb500617", 273 | "directory_permission": "0777", 274 | "file_permission": "0777", 275 | "filename": "./string2json", 276 | "id": "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8", 277 | "sensitive_content": null, 278 | "source": null 279 | }, 280 | "after": { 281 | "content": "{\"key\":\"some random json\"}", 282 | "content_base64": null, 283 | "directory_permission": "0777", 284 | "file_permission": "0777", 285 | "filename": "./string2json", 286 | "sensitive_content": null, 287 | "source": null 288 | }, 289 | "after_unknown": { 290 | "content_base64sha256": true, 291 | "content_base64sha512": true, 292 | "content_md5": true, 293 | "content_sha1": true, 294 | "content_sha256": true, 295 | "content_sha512": true, 296 | "id": true 297 | }, 298 | "before_sensitive": { 299 | "sensitive_content": true 300 | }, 301 | "after_sensitive": { 302 | "sensitive_content": true 303 | }, 304 | "replace_paths": [ 305 | [ 306 | "content" 307 | ] 308 | ] 309 | }, 310 | "action_reason": "replace_because_cannot_update" 311 | }, 312 | { 313 | "address": "null_resource.cluster", 314 | "mode": "managed", 315 | "type": "null_resource", 316 | "name": "cluster", 317 | "provider_name": "registry.terraform.io/hashicorp/null", 318 | "change": { 319 | "actions": [ 320 | "delete", 321 | "create" 322 | ], 323 | "before": { 324 | "id": "2582010537129087805", 325 | "triggers": { 326 | "secret": "secure" 327 | } 328 | }, 329 | "after": { 330 | "triggers": { 331 | "secret": "still secure" 332 | } 333 | }, 334 | "after_unknown": { 335 | "id": true, 336 | "triggers": {} 337 | }, 338 | "before_sensitive": { 339 | "triggers": {} 340 | }, 341 | "after_sensitive": { 342 | "triggers": {} 343 | }, 344 | "replace_paths": [ 345 | [ 346 | "triggers" 347 | ] 348 | ] 349 | }, 350 | "action_reason": "replace_because_cannot_update" 351 | }, 352 | { 353 | "address": "null_resource.cluster_new", 354 | "mode": "managed", 355 | "type": "null_resource", 356 | "name": "cluster_new", 357 | "provider_name": "registry.terraform.io/hashicorp/null", 358 | "change": { 359 | "actions": [ 360 | "create" 361 | ], 362 | "before": null, 363 | "after": { 364 | "triggers": { 365 | "secret": "very very secure" 366 | } 367 | }, 368 | "after_unknown": { 369 | "id": true, 370 | "triggers": {} 371 | }, 372 | "before_sensitive": false, 373 | "after_sensitive": { 374 | "triggers": {} 375 | } 376 | } 377 | }, 378 | { 379 | "address": "null_resource.cluster_old", 380 | "mode": "managed", 381 | "type": "null_resource", 382 | "name": "cluster_old", 383 | "provider_name": "registry.terraform.io/hashicorp/null", 384 | "change": { 385 | "actions": [ 386 | "delete" 387 | ], 388 | "before": { 389 | "id": "6045924575212971352", 390 | "triggers": { 391 | "secret": "very very secure" 392 | } 393 | }, 394 | "after": null, 395 | "after_unknown": {}, 396 | "before_sensitive": { 397 | "triggers": {} 398 | }, 399 | "after_sensitive": false 400 | }, 401 | "action_reason": "delete_because_no_resource_config" 402 | }, 403 | { 404 | "address": "time_offset.example", 405 | "mode": "managed", 406 | "type": "time_offset", 407 | "name": "example", 408 | "provider_name": "registry.terraform.io/hashicorp/time", 409 | "change": { 410 | "actions": [ 411 | "update" 412 | ], 413 | "before": { 414 | "base_rfc3339": "2024-08-09T06:48:48Z", 415 | "day": 16, 416 | "hour": 6, 417 | "id": "2024-08-09T06:48:48Z", 418 | "minute": 48, 419 | "month": 8, 420 | "offset_days": 7, 421 | "offset_hours": null, 422 | "offset_minutes": null, 423 | "offset_months": null, 424 | "offset_seconds": null, 425 | "offset_years": null, 426 | "rfc3339": "2024-08-16T06:48:48Z", 427 | "second": 48, 428 | "triggers": null, 429 | "unix": 1723790928, 430 | "year": 2024 431 | }, 432 | "after": { 433 | "base_rfc3339": "2024-08-09T06:48:48Z", 434 | "day": 17, 435 | "hour": 6, 436 | "id": "2024-08-09T06:48:48Z", 437 | "minute": 48, 438 | "month": 8, 439 | "offset_days": 8, 440 | "offset_hours": null, 441 | "offset_minutes": null, 442 | "offset_months": null, 443 | "offset_seconds": null, 444 | "offset_years": null, 445 | "rfc3339": "2024-08-17T06:48:48Z", 446 | "second": 48, 447 | "triggers": null, 448 | "unix": 1723877328, 449 | "year": 2024 450 | }, 451 | "after_unknown": {}, 452 | "before_sensitive": {}, 453 | "after_sensitive": {} 454 | } 455 | } 456 | ], 457 | "output_changes": { 458 | "string": { 459 | "actions": [ 460 | "update" 461 | ], 462 | "before": "some text", 463 | "after": "some text 1", 464 | "after_unknown": false, 465 | "before_sensitive": false, 466 | "after_sensitive": false 467 | } 468 | }, 469 | "prior_state": { 470 | "format_version": "1.0", 471 | "terraform_version": "1.7.4", 472 | "values": { 473 | "outputs": { 474 | "string": { 475 | "sensitive": false, 476 | "value": "some text 1", 477 | "type": "string" 478 | } 479 | }, 480 | "root_module": { 481 | "resources": [ 482 | { 483 | "address": "local_file.foo", 484 | "mode": "managed", 485 | "type": "local_file", 486 | "name": "foo", 487 | "provider_name": "registry.terraform.io/hashicorp/local", 488 | "schema_version": 0, 489 | "values": { 490 | "content": "{\"array_changed\":[\"foo\",\"bar\",\"baz\"],\"array_item_added\":[\"foo\",\"bar\",\"baz\"],\"array_item_removed\":[\"foo\",\"bar\",\"baz\"],\"array_removed\":[\"foo\",\"bar\",\"baz\"],\"array_to_object\":[\"foo\",\"bar\",\"baz\"],\"array_unchanged\":[\"foo\",\"bar\",\"baz\"],\"number_changed\":10,\"number_removed\":10,\"number_unchanged\":10,\"object_changed\":{\"key\":\"value\"},\"object_removed\":{\"key\":\"value\"},\"object_unchanged\":{\"key\":\"value\"},\"string_changed\":\"bar\",\"string_removed\":\"removed\",\"string_unchanged\":\"foo\"}", 491 | "content_base64": null, 492 | "content_base64sha256": "nIMhVlzPvgCIzeahBGJ/jEJLuCBuwIqx78vltuKjcyw=", 493 | "content_base64sha512": "WDetH1vI/Yy0tu49ZCyW/WySxstlQc3+T7vX0Ei0UaLvLKsd884Djd3raxFJS66NMgFZc1bpSxPKKDXmgz9drA==", 494 | "content_md5": "6f61fcba1ffd2b366fe358450f93b952", 495 | "content_sha1": "4dfb08a62fbd3d8737cd6a11c2360df3953dda51", 496 | "content_sha256": "9c8321565ccfbe0088cde6a104627f8c424bb8206ec08ab1efcbe5b6e2a3732c", 497 | "content_sha512": "5837ad1f5bc8fd8cb4b6ee3d642c96fd6c92c6cb6541cdfe4fbbd7d048b451a2ef2cab1df3ce038dddeb6b11494bae8d3201597356e94b13ca2835e6833f5dac", 498 | "directory_permission": "0777", 499 | "file_permission": "0777", 500 | "filename": "./foo.bar", 501 | "id": "4dfb08a62fbd3d8737cd6a11c2360df3953dda51", 502 | "sensitive_content": null, 503 | "source": null 504 | }, 505 | "sensitive_values": { 506 | "sensitive_content": true 507 | } 508 | }, 509 | { 510 | "address": "local_file.json2string", 511 | "mode": "managed", 512 | "type": "local_file", 513 | "name": "json2string", 514 | "provider_name": "registry.terraform.io/hashicorp/local", 515 | "schema_version": 0, 516 | "values": { 517 | "content": "{\"key\":\"some random json\"}", 518 | "content_base64": null, 519 | "content_base64sha256": "HYsNaOjaBgmFTRuYj4T4J4Ve7InaRVmfw2wgDbwr8Cs=", 520 | "content_base64sha512": "COm1PXnJevPHVeZququDXk5+U5hDeA4dLmTnUpdeMhoF/3+isI46Hgkg5Dln9HCVN7Ythqjq4mrIP4t67bq3Ow==", 521 | "content_md5": "31dd41a76bb193c928caf767ac8e8340", 522 | "content_sha1": "72e1eb91ff698efdaa1a82b5977634a04cf2985d", 523 | "content_sha256": "1d8b0d68e8da0609854d1b988f84f827855eec89da45599fc36c200dbc2bf02b", 524 | "content_sha512": "08e9b53d79c97af3c755e66abaab835e4e7e539843780e1d2e64e752975e321a05ff7fa2b08e3a1e0920e43967f4709537b62d86a8eae26ac83f8b7aedbab73b", 525 | "directory_permission": "0777", 526 | "file_permission": "0777", 527 | "filename": "./json2string", 528 | "id": "72e1eb91ff698efdaa1a82b5977634a04cf2985d", 529 | "sensitive_content": null, 530 | "source": null 531 | }, 532 | "sensitive_values": { 533 | "sensitive_content": true 534 | } 535 | }, 536 | { 537 | "address": "local_file.string2json", 538 | "mode": "managed", 539 | "type": "local_file", 540 | "name": "string2json", 541 | "provider_name": "registry.terraform.io/hashicorp/local", 542 | "schema_version": 0, 543 | "values": { 544 | "content": "some random string", 545 | "content_base64": null, 546 | "content_base64sha256": "Q0LgjQHySsRRvDu/9oZAhWO1wm04bQwYh5JE9eeXTX0=", 547 | "content_base64sha512": "eaxV3784RhIgGecPVkfZ+wZlola4sUqDaG/V8Hr+Y5LKG7a3UJ5UkF9VRA4UYOGmtnlGI0hcm7nVJeq2u1AGFw==", 548 | "content_md5": "76712b27e483bc0ba2ce8d2109210c22", 549 | "content_sha1": "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8", 550 | "content_sha256": "4342e08d01f24ac451bc3bbff686408563b5c26d386d0c18879244f5e7974d7d", 551 | "content_sha512": "79ac55dfbf3846122019e70f5647d9fb0665a256b8b14a83686fd5f07afe6392ca1bb6b7509e54905f55440e1460e1a6b6794623485c9bb9d525eab6bb500617", 552 | "directory_permission": "0777", 553 | "file_permission": "0777", 554 | "filename": "./string2json", 555 | "id": "8d7219cf3e6259b90b70d5b5e2d39b27f946e4b8", 556 | "sensitive_content": null, 557 | "source": null 558 | }, 559 | "sensitive_values": { 560 | "sensitive_content": true 561 | } 562 | }, 563 | { 564 | "address": "null_resource.cluster", 565 | "mode": "managed", 566 | "type": "null_resource", 567 | "name": "cluster", 568 | "provider_name": "registry.terraform.io/hashicorp/null", 569 | "schema_version": 0, 570 | "values": { 571 | "id": "2582010537129087805", 572 | "triggers": { 573 | "secret": "secure" 574 | } 575 | }, 576 | "sensitive_values": { 577 | "triggers": {} 578 | } 579 | }, 580 | { 581 | "address": "null_resource.cluster_old", 582 | "mode": "managed", 583 | "type": "null_resource", 584 | "name": "cluster_old", 585 | "provider_name": "registry.terraform.io/hashicorp/null", 586 | "schema_version": 0, 587 | "values": { 588 | "id": "6045924575212971352", 589 | "triggers": { 590 | "secret": "very very secure" 591 | } 592 | }, 593 | "sensitive_values": { 594 | "triggers": {} 595 | } 596 | }, 597 | { 598 | "address": "time_offset.example", 599 | "mode": "managed", 600 | "type": "time_offset", 601 | "name": "example", 602 | "provider_name": "registry.terraform.io/hashicorp/time", 603 | "schema_version": 0, 604 | "values": { 605 | "base_rfc3339": "2024-08-09T06:48:48Z", 606 | "day": 16, 607 | "hour": 6, 608 | "id": "2024-08-09T06:48:48Z", 609 | "minute": 48, 610 | "month": 8, 611 | "offset_days": 7, 612 | "offset_hours": null, 613 | "offset_minutes": null, 614 | "offset_months": null, 615 | "offset_seconds": null, 616 | "offset_years": null, 617 | "rfc3339": "2024-08-16T06:48:48Z", 618 | "second": 48, 619 | "triggers": null, 620 | "unix": 1723790928, 621 | "year": 2024 622 | }, 623 | "sensitive_values": {} 624 | } 625 | ] 626 | } 627 | } 628 | }, 629 | "configuration": { 630 | "provider_config": { 631 | "local": { 632 | "name": "local", 633 | "full_name": "registry.terraform.io/hashicorp/local", 634 | "version_constraint": "2.5.1" 635 | }, 636 | "null": { 637 | "name": "null", 638 | "full_name": "registry.terraform.io/hashicorp/null", 639 | "version_constraint": "3.2.2" 640 | }, 641 | "time": { 642 | "name": "time", 643 | "full_name": "registry.terraform.io/hashicorp/time", 644 | "version_constraint": "0.11.1" 645 | } 646 | }, 647 | "root_module": { 648 | "outputs": { 649 | "string": { 650 | "expression": { 651 | "constant_value": "some text 1" 652 | } 653 | } 654 | }, 655 | "resources": [ 656 | { 657 | "address": "local_file.foo", 658 | "mode": "managed", 659 | "type": "local_file", 660 | "name": "foo", 661 | "provider_config_key": "local", 662 | "expressions": { 663 | "content": {}, 664 | "file_permission": { 665 | "constant_value": "0660" 666 | }, 667 | "filename": { 668 | "references": [ 669 | "path.module" 670 | ] 671 | } 672 | }, 673 | "schema_version": 0 674 | }, 675 | { 676 | "address": "local_file.json2string", 677 | "mode": "managed", 678 | "type": "local_file", 679 | "name": "json2string", 680 | "provider_config_key": "local", 681 | "expressions": { 682 | "content": { 683 | "constant_value": "some random string" 684 | }, 685 | "filename": { 686 | "references": [ 687 | "path.module" 688 | ] 689 | } 690 | }, 691 | "schema_version": 0 692 | }, 693 | { 694 | "address": "local_file.string2json", 695 | "mode": "managed", 696 | "type": "local_file", 697 | "name": "string2json", 698 | "provider_config_key": "local", 699 | "expressions": { 700 | "content": {}, 701 | "filename": { 702 | "references": [ 703 | "path.module" 704 | ] 705 | } 706 | }, 707 | "schema_version": 0 708 | }, 709 | { 710 | "address": "null_resource.cluster", 711 | "mode": "managed", 712 | "type": "null_resource", 713 | "name": "cluster", 714 | "provider_config_key": "null", 715 | "expressions": { 716 | "triggers": { 717 | "constant_value": { 718 | "secret": "still secure" 719 | } 720 | } 721 | }, 722 | "schema_version": 0 723 | }, 724 | { 725 | "address": "null_resource.cluster_new", 726 | "mode": "managed", 727 | "type": "null_resource", 728 | "name": "cluster_new", 729 | "provider_config_key": "null", 730 | "expressions": { 731 | "triggers": { 732 | "constant_value": { 733 | "secret": "very very secure" 734 | } 735 | } 736 | }, 737 | "schema_version": 0 738 | }, 739 | { 740 | "address": "time_offset.example", 741 | "mode": "managed", 742 | "type": "time_offset", 743 | "name": "example", 744 | "provider_config_key": "time", 745 | "expressions": { 746 | "offset_days": { 747 | "constant_value": 8 748 | } 749 | }, 750 | "schema_version": 0 751 | } 752 | ] 753 | } 754 | }, 755 | "timestamp": "2024-08-09T06:48:48Z", 756 | "errored": false 757 | } 758 | --------------------------------------------------------------------------------