diff --git a/CHANGELOG.md b/CHANGELOG.md index d716cdfdabe2..4c8968107199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ENHANCEMENTS: If an entered line contains opening paretheses/etc that are not closed, Terraform will await another line of input to complete the expression. This initial implementation is primarily intended to support pasting in multi-line expressions from elsewhere, rather than for manual multi-line editing, so the interactive editing support is currently limited. * `cli`: Updates the Terraform CLI output to show logical separation between OPA and Sentinel policy evaluations +* `terraform init` now accepts a `-json` option. If specified, enables the machine readable JSON output. ([#34886](https://github.com/hashicorp/terraform/pull/34886)) +* `terraform test:` The test framework will now maintain sensitive metadata between run blocks. ([#35021](https://github.com/hashicorp/terraform/pull/35021)) BUG FIXES: @@ -19,7 +21,7 @@ Experiments are only enabled in alpha releases of Terraform CLI. The following f * `variable_validation_crossref`: This [language experiment](https://developer.hashicorp.com/terraform/language/settings#experimental-language-features) allows `validation` blocks inside input variable declarations to refer to other objects inside the module where the variable is declared, including to the values of other input variables in the same module. * `terraform test` accepts a new option `-junit-xml=FILENAME`. If specified, and if the test configuration is valid enough to begin executing, then Terraform writes a JUnit XML test result report to the given filename, describing similar information as included in the normal test output. ([#34291](https://github.com/hashicorp/terraform/issues/34291)) * The new command `terraform rpcapi` exposes some Terraform Core functionality through an RPC interface compatible with [`go-plugin`](https://github.com/hashicorp/go-plugin). The exact RPC API exposed here is currently subject to change at any time, because it's here primarily as a vehicle to support the [Terraform Stacks](https://www.hashicorp.com/blog/terraform-stacks-explained) private preview and so will be broken if necessary to respond to feedback from private preview participants, or possibly for other reasons. Do not use this mechanism yet outside of Terraform Stacks private preview. -* The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment. +* The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment. ## Previous Releases diff --git a/go.mod b/go.mod index 876f395fe3b1..e871a273d7bb 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/xanzy/ssh-agent v0.3.3 github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 github.com/zclconf/go-cty v1.14.3 - github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be + github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a github.com/zclconf/go-cty-yaml v1.0.3 go.opentelemetry.io/contrib/exporters/autoexport v0.45.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 diff --git a/go.sum b/go.sum index 846610507fd5..c1e5ccadfe76 100644 --- a/go.sum +++ b/go.sum @@ -1040,8 +1040,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.0.3 h1:og/eOQ7lvA/WWhHGFETVWNduJM7Rjsv2RRpx1sdFMLc= github.com/zclconf/go-cty-yaml v1.0.3/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index 24828a3327f1..31d3a359d36e 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -20,7 +20,6 @@ import ( "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/moduletest" configtest "github.com/hashicorp/terraform/internal/moduletest/config" @@ -1239,36 +1238,13 @@ func (runner *TestFileRunner) FilterVariablesToModule(config *configs.Config, va moduleVars = make(terraform.InputValues) testOnlyVars = make(terraform.InputValues) for name, value := range values { - variableConfig, exists := config.Module.Variables[name] + _, exists := config.Module.Variables[name] if !exists { // If it's not in the configuration then it's a test-only variable. testOnlyVars[name] = value continue } - if marks.Has(value.Value, marks.Sensitive) { - unmarkedValue, _ := value.Value.Unmark() - if !variableConfig.Sensitive { - // Then we are passing a sensitive value into a non-sensitive - // variable. Let's add a warning and tell the user they should - // mark the config as sensitive as well. If the config variable - // is sensitive, then we don't need to worry. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Sensitive metadata on variable lost", - Detail: fmt.Sprintf("The input variable is marked as sensitive, while the receiving configuration is not. The underlying sensitive information may be exposed when var.%s is referenced. Mark the variable block in the configuration as sensitive to resolve this warning.", variableConfig.Name), - Subject: value.SourceRange.ToHCL().Ptr(), - }) - } - - // Set the unmarked value into the input value. - value = &terraform.InputValue{ - Value: unmarkedValue, - SourceType: value.SourceType, - SourceRange: value.SourceRange, - } - } - moduleVars[name] = value } return moduleVars, testOnlyVars, diags diff --git a/internal/backend/remote-state/azure/go.sum b/internal/backend/remote-state/azure/go.sum index c479cde1ef0b..18a9d58c8a22 100644 --- a/internal/backend/remote-state/azure/go.sum +++ b/internal/backend/remote-state/azure/go.sum @@ -323,8 +323,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/consul/go.sum b/internal/backend/remote-state/consul/go.sum index f6df4fcd603e..7ee0eaf91b6d 100644 --- a/internal/backend/remote-state/consul/go.sum +++ b/internal/backend/remote-state/consul/go.sum @@ -323,8 +323,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/cos/go.sum b/internal/backend/remote-state/cos/go.sum index 6cfeeb0e1e16..cd3723de06b8 100644 --- a/internal/backend/remote-state/cos/go.sum +++ b/internal/backend/remote-state/cos/go.sum @@ -264,8 +264,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/gcs/go.sum b/internal/backend/remote-state/gcs/go.sum index 3f5724bf62e3..09a2117e7cd7 100644 --- a/internal/backend/remote-state/gcs/go.sum +++ b/internal/backend/remote-state/gcs/go.sum @@ -267,8 +267,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum index a93de7d755ab..cf89b30e2273 100644 --- a/internal/backend/remote-state/kubernetes/go.sum +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -325,8 +325,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/oss/go.sum b/internal/backend/remote-state/oss/go.sum index 2cbdb9d71fd6..d3a34f6ee6da 100644 --- a/internal/backend/remote-state/oss/go.sum +++ b/internal/backend/remote-state/oss/go.sum @@ -273,8 +273,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/pg/go.sum b/internal/backend/remote-state/pg/go.sum index 9e3c308b38b8..df16491f8d15 100644 --- a/internal/backend/remote-state/pg/go.sum +++ b/internal/backend/remote-state/pg/go.sum @@ -244,8 +244,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/backend/remote-state/s3/go.sum b/internal/backend/remote-state/s3/go.sum index 71ae992de5cd..209ac47f34de 100644 --- a/internal/backend/remote-state/s3/go.sum +++ b/internal/backend/remote-state/s3/go.sum @@ -306,8 +306,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/command/apply.go b/internal/command/apply.go index e332f94b47e7..1223e3d3b493 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -319,12 +319,12 @@ func (c *ApplyCommand) GatherVariables(opReq *backendrun.Operation, args *argume // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/arguments/extended.go b/internal/command/arguments/extended.go index 6e9aadea2a25..3b3c7cc759f9 100644 --- a/internal/command/arguments/extended.go +++ b/internal/command/arguments/extended.go @@ -192,13 +192,13 @@ func (o *Operation) Parse() tfdiags.Diagnostics { } // Vars describes arguments which specify non-default variable values. This -// interfce is unfortunately obscure, because the order of the CLI arguments +// interface is unfortunately obscure, because the order of the CLI arguments // determines the final value of the gathered variables. In future it might be // desirable for the arguments package to handle the gathering of variables // directly, returning a map of variable values. type Vars struct { - vars *flagNameValueSlice - varFiles *flagNameValueSlice + vars *FlagNameValueSlice + varFiles *FlagNameValueSlice } func (v *Vars) All() []FlagNameValue { @@ -239,14 +239,14 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars f.BoolVar(&operation.Refresh, "refresh", true, "refresh") f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy") f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only") - f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target") - f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") + f.Var((*FlagStringSlice)(&operation.targetsRaw), "target", "target") + f.Var((*FlagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") } // Gather all -var and -var-file arguments into one heterogenous structure // to preserve the overall order. if vars != nil { - varsFlags := newFlagNameValueSlice("-var") + varsFlags := NewFlagNameValueSlice("-var") varFilesFlags := varsFlags.Alias("-var-file") vars.vars = &varsFlags vars.varFiles = &varFilesFlags diff --git a/internal/command/arguments/flags.go b/internal/command/arguments/flags.go index 7a19a544eed9..64bf18ddd900 100644 --- a/internal/command/arguments/flags.go +++ b/internal/command/arguments/flags.go @@ -8,72 +8,68 @@ import ( "fmt" ) -// flagStringSlice is a flag.Value implementation which allows collecting +// FlagStringSlice is a flag.Value implementation which allows collecting // multiple instances of a single flag into a slice. This is used for flags // such as -target=aws_instance.foo and -var x=y. -type flagStringSlice []string +type FlagStringSlice []string -var _ flag.Value = (*flagStringSlice)(nil) +var _ flag.Value = (*FlagStringSlice)(nil) -func (v *flagStringSlice) String() string { +func (v *FlagStringSlice) String() string { return "" } -func (v *flagStringSlice) Set(raw string) error { +func (v *FlagStringSlice) Set(raw string) error { *v = append(*v, raw) return nil } -// flagNameValueSlice is a flag.Value implementation that appends raw flag +// FlagNameValueSlice is a flag.Value implementation that appends raw flag // names and values to a slice. This is used to collect a sequence of flags // with possibly different names, preserving the overall order. -// -// FIXME: this is a copy of rawFlags from command/meta_config.go, with the -// eventual aim of replacing it altogether by gathering variables in the -// arguments package. -type flagNameValueSlice struct { - flagName string - items *[]FlagNameValue +type FlagNameValueSlice struct { + FlagName string + Items *[]FlagNameValue } -var _ flag.Value = flagNameValueSlice{} +var _ flag.Value = FlagNameValueSlice{} -func newFlagNameValueSlice(flagName string) flagNameValueSlice { +func NewFlagNameValueSlice(flagName string) FlagNameValueSlice { var items []FlagNameValue - return flagNameValueSlice{ - flagName: flagName, - items: &items, + return FlagNameValueSlice{ + FlagName: flagName, + Items: &items, } } -func (f flagNameValueSlice) Empty() bool { - if f.items == nil { +func (f FlagNameValueSlice) Empty() bool { + if f.Items == nil { return true } - return len(*f.items) == 0 + return len(*f.Items) == 0 } -func (f flagNameValueSlice) AllItems() []FlagNameValue { - if f.items == nil { +func (f FlagNameValueSlice) AllItems() []FlagNameValue { + if f.Items == nil { return nil } - return *f.items + return *f.Items } -func (f flagNameValueSlice) Alias(flagName string) flagNameValueSlice { - return flagNameValueSlice{ - flagName: flagName, - items: f.items, +func (f FlagNameValueSlice) Alias(flagName string) FlagNameValueSlice { + return FlagNameValueSlice{ + FlagName: flagName, + Items: f.Items, } } -func (f flagNameValueSlice) String() string { +func (f FlagNameValueSlice) String() string { return "" } -func (f flagNameValueSlice) Set(str string) error { - *f.items = append(*f.items, FlagNameValue{ - Name: f.flagName, +func (f FlagNameValueSlice) Set(str string) error { + *f.Items = append(*f.Items, FlagNameValue{ + Name: f.FlagName, Value: str, }) return nil diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go new file mode 100644 index 000000000000..b6ae8f98c2aa --- /dev/null +++ b/internal/command/arguments/init.go @@ -0,0 +1,159 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Init represents the command-line arguments for the init command. +type Init struct { + // FromModule identifies the module to copy into the target directory before init. + FromModule string + + // Lockfile specifies a dependency lockfile mode. + Lockfile string + + // TestDirectory is the directory containing any test files that should be + // validated alongside the main configuration. Should be relative to the + // Path. + TestsDirectory string + + // ViewType specifies which init format to use: human or JSON. + ViewType ViewType + + // Backend specifies whether to disable backend or Terraform Cloud initialization. + Backend bool + + // Cloud specifies whether to disable backend or Terraform Cloud initialization. + Cloud bool + + // Get specifies whether to disable downloading modules for this configuration + Get bool + + // ForceInitCopy specifies whether to suppress prompts about copying state data. + ForceInitCopy bool + + // StateLock specifies whether hold a state lock during backend migration. + StateLock bool + + // StateLockTimeout specifies the duration to wait for a state lock. + StateLockTimeout time.Duration + + // Reconfigure specifies whether to disregard any existing configuration, preventing migration of any existing state + Reconfigure bool + + // MigrateState specifies whether to attempt to copy existing state to the new backend + MigrateState bool + + // Upgrade specifies whether to upgrade modules and plugins as part of their respective installation steps + Upgrade bool + + // Json specifies whether to output in JSON format + Json bool + + // IgnoreRemoteVersion specifies whether to ignore remote and local Terraform versions compatibility + IgnoreRemoteVersion bool + + BackendConfig FlagNameValueSlice + + Vars *Vars + + // InputEnabled is used to disable interactive input for unspecified + // variable and backend config values. Default is true. + InputEnabled bool + + TargetFlags []string + + CompactWarnings bool + + PluginPath FlagStringSlice + + Args []string +} + +// ParseInit processes CLI arguments, returning an Init value and errors. +// If errors are encountered, an Init value is still returned representing +// the best effort interpretation of the arguments. +func ParseInit(args []string) (*Init, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + init := &Init{ + Vars: &Vars{}, + } + init.BackendConfig = NewFlagNameValueSlice("-backend-config") + + cmdFlags := extendedFlagSet("init", nil, nil, init.Vars) + + cmdFlags.Var((*FlagStringSlice)(&init.TargetFlags), "target", "resource to target") + cmdFlags.BoolVar(&init.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&init.CompactWarnings, "compact-warnings", false, "use compact warnings") + cmdFlags.BoolVar(&init.Backend, "backend", true, "") + cmdFlags.BoolVar(&init.Cloud, "cloud", true, "") + cmdFlags.StringVar(&init.FromModule, "from-module", "", "copy the source of the given module into the directory before init") + cmdFlags.BoolVar(&init.Get, "get", true, "") + cmdFlags.BoolVar(&init.ForceInitCopy, "force-copy", false, "suppress prompts about copying state data") + cmdFlags.BoolVar(&init.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&init.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.BoolVar(&init.Reconfigure, "reconfigure", false, "reconfigure") + cmdFlags.BoolVar(&init.MigrateState, "migrate-state", false, "migrate state") + cmdFlags.BoolVar(&init.Upgrade, "upgrade", false, "") + cmdFlags.StringVar(&init.Lockfile, "lockfile", "", "Set a dependency lockfile mode") + cmdFlags.BoolVar(&init.IgnoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") + cmdFlags.StringVar(&init.TestsDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&init.Json, "json", false, "json") + cmdFlags.Var(&init.BackendConfig, "backend-config", "") + cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + if init.MigrateState && init.Json { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -migrate-state and -json options are mutually-exclusive", + "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + )) + } + + if init.MigrateState && init.Reconfigure { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -migrate-state and -reconfigure options are mutually-exclusive.", + )) + } + + init.Args = cmdFlags.Args() + + backendFlagSet := FlagIsSet(cmdFlags, "backend") + cloudFlagSet := FlagIsSet(cmdFlags, "cloud") + + if backendFlagSet && cloudFlagSet { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + )) + } else if backendFlagSet { + init.Cloud = init.Backend + } else if cloudFlagSet { + init.Backend = init.Cloud + } + + switch { + case init.Json: + init.ViewType = ViewJSON + default: + init.ViewType = ViewHuman + } + + return init, diags +} diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go new file mode 100644 index 000000000000..93e13b7b6281 --- /dev/null +++ b/internal/command/arguments/init_test.go @@ -0,0 +1,222 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestParseInit_basicValid(t *testing.T) { + var flagNameValue []FlagNameValue + testCases := map[string]struct { + args []string + want *Init + }{ + "with default options": { + nil, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: true, + Cloud: true, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: false, + TargetFlags: nil, + }, + }, + "setting multiple options": { + []string{"-backend=false", "-force-copy=true", + "-from-module=./main-dir", "-json", "-get=false", + "-lock=false", "-lock-timeout=10s", "-reconfigure=true", + "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", + "-ignore-remote-version=true", "-test-directory=./test-dir"}, + &Init{ + FromModule: "./main-dir", + Lockfile: "readonly", + TestsDirectory: "./test-dir", + ViewType: ViewJSON, + Backend: false, + Cloud: false, + Get: false, + ForceInitCopy: true, + StateLock: false, + StateLockTimeout: time.Duration(10) * time.Second, + Reconfigure: true, + MigrateState: false, + Upgrade: true, + Json: true, + IgnoreRemoteVersion: true, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + Args: []string{}, + CompactWarnings: true, + TargetFlags: nil, + }, + }, + "with cloud option": { + []string{"-cloud=false", "-input=false", "-target=foo_bar.baz", "-backend-config", "backend.config"}, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: false, + Cloud: false, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}}, + }, + Vars: &Vars{}, + InputEnabled: false, + Args: []string{}, + CompactWarnings: false, + TargetFlags: []string{"foo_bar.baz"}, + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseInit_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + wantErr string + wantViewType ViewType + }{ + "with unsupported options": { + args: []string{"-raw"}, + wantErr: "flag provided but not defined", + wantViewType: ViewHuman, + }, + "with both -backend and -cloud options set": { + args: []string{"-backend=false", "-cloud=false"}, + wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + wantViewType: ViewHuman, + }, + "with both -migrate-state and -json options set": { + args: []string{"-migrate-state", "-json"}, + wantErr: "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + wantViewType: ViewJSON, + }, + "with both -migrate-state and -reconfigure options set": { + args: []string{"-migrate-state", "-reconfigure"}, + wantErr: "The -migrate-state and -reconfigure options are mutually-exclusive.", + wantViewType: ViewHuman, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) == 0 { + t.Fatal("expected diags but got none") + } + if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) { + t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want) + } + if got.ViewType != tc.wantViewType { + t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman) + } + }) + } +} + +func TestParseInit_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "no var flags by default": { + args: nil, + want: nil, + }, + "one var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want { + t.Fatalf("expected Empty() to return %t, but was %t", want, got) + } + }) + } +} diff --git a/internal/command/arguments/test.go b/internal/command/arguments/test.go index 51e2ab604f3b..e9168aff11e9 100644 --- a/internal/command/arguments/test.go +++ b/internal/command/arguments/test.go @@ -50,7 +50,7 @@ func ParseTest(args []string) (*Test, tfdiags.Diagnostics) { var jsonOutput bool cmdFlags := extendedFlagSet("test", nil, nil, test.Vars) - cmdFlags.Var((*flagStringSlice)(&test.Filter), "filter", "filter") + cmdFlags.Var((*FlagStringSlice)(&test.Filter), "filter", "filter") cmdFlags.StringVar(&test.TestDirectory, "test-directory", configs.DefaultTestDirectory, "test-directory") cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&test.JUnitXMLFile, "junit-xml", "", "junit-xml") diff --git a/internal/command/e2etest/init_test.go b/internal/command/e2etest/init_test.go index e1520c8cd966..abe2fb111db8 100644 --- a/internal/command/e2etest/init_test.go +++ b/internal/command/e2etest/init_test.go @@ -374,14 +374,13 @@ func TestInitProviderNotFound(t *testing.T) { │ Could not retrieve the list of available versions for provider │ hashicorp/nonexist: provider registry registry.terraform.io does not have a │ provider named registry.terraform.io/hashicorp/nonexist -│ +│` + ` ` + ` │ All modules should specify their required_providers so that external │ consumers will get the correct providers when using a module. To see which │ modules are currently depending on hashicorp/nonexist, run the following │ command: │ terraform providers ╵ - ` if stripAnsi(stderr) != expectedErr { t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr)) diff --git a/internal/command/flag_kv.go b/internal/command/flag_kv.go index cf351d2f4a08..2d16ca5505ce 100644 --- a/internal/command/flag_kv.go +++ b/internal/command/flag_kv.go @@ -31,16 +31,3 @@ func (v *FlagStringKV) Set(raw string) error { (*v)[key] = value return nil } - -// FlagStringSlice is a flag.Value implementation for parsing targets from the -// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar -type FlagStringSlice []string - -func (v *FlagStringSlice) String() string { - return "" -} -func (v *FlagStringSlice) Set(raw string) error { - *v = append(*v, raw) - - return nil -} diff --git a/internal/command/hook_module_install.go b/internal/command/hook_module_install.go index a795ac63805b..9971cfbd32e8 100644 --- a/internal/command/hook_module_install.go +++ b/internal/command/hook_module_install.go @@ -11,26 +11,39 @@ import ( "github.com/hashicorp/terraform/internal/initwd" ) +type view interface { + Log(message string, params ...any) +} type uiModuleInstallHooks struct { initwd.ModuleInstallHooksImpl Ui cli.Ui ShowLocalPaths bool + View view } var _ initwd.ModuleInstallHooks = uiModuleInstallHooks{} func (h uiModuleInstallHooks) Download(modulePath, packageAddr string, v *version.Version) { if v != nil { - h.Ui.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) + h.log(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) } else { - h.Ui.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) + h.log(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) } } func (h uiModuleInstallHooks) Install(modulePath string, v *version.Version, localDir string) { if h.ShowLocalPaths { - h.Ui.Info(fmt.Sprintf("- %s in %s", modulePath, localDir)) + h.log(fmt.Sprintf("- %s in %s", modulePath, localDir)) } else { - h.Ui.Info(fmt.Sprintf("- %s", modulePath)) + h.log(fmt.Sprintf("- %s", modulePath)) + } +} + +func (h uiModuleInstallHooks) log(message string) { + switch h.View.(type) { + case view: + h.View.Log(message) + default: + h.Ui.Info(message) } } diff --git a/internal/command/init.go b/internal/command/init.go index 8900d6a3df48..c2b7bb4cc7a7 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -5,6 +5,7 @@ package command import ( "context" + "errors" "fmt" "log" "reflect" @@ -24,6 +25,7 @@ import ( backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/getproviders" @@ -41,50 +43,35 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagFromModule, flagLockfile, testsDirectory string - var flagBackend, flagCloud, flagGet, flagUpgrade bool - var flagPluginPath FlagStringSlice - flagConfigExtra := newRawFlags("-backend-config") - + var diags tfdiags.Diagnostics args = c.Meta.process(args) - cmdFlags := c.Meta.extendedFlagSet("init") - cmdFlags.BoolVar(&flagBackend, "backend", true, "") - cmdFlags.BoolVar(&flagCloud, "cloud", true, "") - cmdFlags.Var(flagConfigExtra, "backend-config", "") - cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init") - cmdFlags.BoolVar(&flagGet, "get", true, "") - cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") - cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state") - cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") - cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") - cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode") - cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") - cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } + initArgs, initDiags := arguments.ParseInit(args) - backendFlagSet := arguments.FlagIsSet(cmdFlags, "backend") - cloudFlagSet := arguments.FlagIsSet(cmdFlags, "cloud") + view := views.NewInit(initArgs.ViewType, c.View) - switch { - case backendFlagSet && cloudFlagSet: - c.Ui.Error("The -backend and -cloud options are aliases of one another and mutually-exclusive in their use") + if initDiags.HasErrors() { + diags = diags.Append(initDiags) + view.Diagnostics(diags) return 1 - case backendFlagSet: - flagCloud = flagBackend - case cloudFlagSet: - flagBackend = flagCloud } - if c.migrateState && c.reconfigure { - c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive") - return 1 + c.forceInitCopy = initArgs.ForceInitCopy + c.Meta.stateLock = initArgs.StateLock + c.Meta.stateLockTimeout = initArgs.StateLockTimeout + c.reconfigure = initArgs.Reconfigure + c.migrateState = initArgs.MigrateState + c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion + c.Meta.input = initArgs.InputEnabled + c.Meta.targetFlags = initArgs.TargetFlags + c.Meta.compactWarnings = initArgs.CompactWarnings + + varArgs := initArgs.Vars.All() + items := make([]arguments.FlagNameValue, len(varArgs)) + for i := range varArgs { + items[i].Name = varArgs[i].Name + items[i].Value = varArgs[i].Value } + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} // Copying the state only happens during backend migration, so setting // -force-copy implies -migrate-state @@ -92,22 +79,21 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - var diags tfdiags.Diagnostics - - if len(flagPluginPath) > 0 { - c.pluginPath = flagPluginPath + if len(initArgs.PluginPath) > 0 { + c.pluginPath = initArgs.PluginPath } // Validate the arg count and get the working directory - args = cmdFlags.Args() - path, err := ModulePath(args) + path, err := ModulePath(initArgs.Args) if err != nil { - c.Ui.Error(err.Error()) + diags = diags.Append(err) + view.Diagnostics(diags) return 1 } if err := c.storePluginPath(c.pluginPath); err != nil { - c.Ui.Error(fmt.Sprintf("Error saving -plugin-path values: %s", err)) + diags = diags.Append(fmt.Errorf("Error saving -plugin-path values: %s", err)) + view.Diagnostics(diags) return 1 } @@ -119,27 +105,28 @@ func (c *InitCommand) Run(args []string) int { // to output a newline before the success message var header bool - if flagFromModule != "" { - src := flagFromModule + if initArgs.FromModule != "" { + src := initArgs.FromModule empty, err := configs.IsEmptyDir(path) if err != nil { - c.Ui.Error(fmt.Sprintf("Error validating destination directory: %s", err)) + diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) + view.Diagnostics(diags) return 1 } if !empty { - c.Ui.Error(strings.TrimSpace(errInitCopyNotEmpty)) + diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))) + view.Diagnostics(diags) return 1 } - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Copying configuration[reset] from %q...", src, - ))) + view.Output(views.CopyingConfigurationMessage, src) header = true hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: false, // since they are in a weird location for init + View: view, } ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes( @@ -149,14 +136,14 @@ func (c *InitCommand) Run(args []string) int { initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) diags = diags.Append(initDirFromModuleDiags) if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) span.SetStatus(codes.Error, "module installation failed") span.End() return 1 } span.End() - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If our directory is empty, then we're done. We can't get or set up @@ -164,25 +151,24 @@ func (c *InitCommand) Run(args []string) int { empty, err := configs.IsEmptyDir(path) if err != nil { diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if empty { - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty))) + view.Output(views.OutputInitEmptyMessage) return 0 } // Load just the root module to begin backend and module initialization - rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, testsDirectory) + rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) // There may be parsing errors in config loading but these will be shown later _after_ // checking for core version requirement errors. Not meeting the version requirement should // be the first error displayed if that is an issue, but other operations are required // before being able to check core version requirements. if rootModEarly == nil { - c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) - diags = diags.Append(earlyConfDiags) - c.showDiagnostics(diags) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) + view.Diagnostics(diags) return 1 } @@ -195,10 +181,10 @@ func (c *InitCommand) Run(args []string) int { var backendOutput bool switch { - case flagCloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra) - case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra) + case initArgs.Cloud && rootModEarly.CloudConfig != nil: + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) + case initArgs.Backend: + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) default: // load the previously-stored backend config back, backDiags = c.Meta.backendFromState(ctx) @@ -216,28 +202,31 @@ func (c *InitCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(back) workspace, err := c.Workspace() if err != nil { - c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err)) + view.Diagnostics(diags) return 1 } sMgr, err := back.StateMgr(workspace) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) + diags = diags.Append(fmt.Errorf("Error loading state: %s", err)) + view.Diagnostics(diags) return 1 } if err := sMgr.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) + diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err)) + view.Diagnostics(diags) return 1 } state = sMgr.State() } - if flagGet { - modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade) + if initArgs.Get { + modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view) diags = diags.Append(modsDiags) if modsAbort || modsDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if modsOutput { @@ -247,7 +236,7 @@ func (c *InitCommand) Run(args []string) int { // With all of the modules (hopefully) installed, we can now try to load the // whole configuration tree. - config, confDiags := c.loadConfigWithTests(path, testsDirectory) + config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) // configDiags will be handled after the version constraint check, since an // incorrect version of terraform may be producing errors for configuration // constructs added in later versions. @@ -258,7 +247,7 @@ func (c *InitCommand) Run(args []string) int { // potentially-confusing downstream errors. versionDiags := terraform.CheckCoreVersionRequirements(config) if versionDiags.HasErrors() { - c.showDiagnostics(versionDiags) + view.Diagnostics(versionDiags) return 1 } @@ -270,16 +259,16 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(earlyConfDiags) diags = diags.Append(backDiags) if earlyConfDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) return 1 } // Now, we can show any errors from initializing the backend, but we won't - // show the errInitConfigError preamble as we didn't detect problems with + // show the InitConfigError preamble as we didn't detect problems with // the early configuration. if backDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -287,8 +276,8 @@ func (c *InitCommand) Run(args []string) int { // show other errors from loading the full configuration tree. diags = diags.Append(confDiags) if confDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) return 1 } @@ -296,17 +285,17 @@ func (c *InitCommand) Run(args []string) int { if c.RunningInAutomation { if err := cb.AssertImportCompatible(config); err != nil { diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } } } // Now that we have loaded all modules, check the module tree for missing providers. - providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, flagUpgrade, flagPluginPath, flagLockfile) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if providersOutput { @@ -316,35 +305,35 @@ func (c *InitCommand) Run(args []string) int { // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. if header { - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If we accumulated any warnings along the way that weren't accompanied // by errors then we'll output them here so that the success message is // still the final thing shown. - c.showDiagnostics(diags) + view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) - output := outputInitSuccess + output := views.OutputInitSuccessMessage if cloud { - output = outputInitSuccessCloud + output = views.OutputInitSuccessCloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + view.Output(output) if !c.RunningInAutomation { // If we're not running in an automation wrapper, give the user // some more detailed next steps that are appropriate for interactive // shell usage. - output = outputInitSuccessCLI + output = views.OutputInitSuccessCLIMessage if cloud { - output = outputInitSuccessCLICloud + output = views.OutputInitSuccessCLICloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + view.Output(output) } return 0 } -func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool, view views.Init) (output bool, abort bool, diags tfdiags.Diagnostics) { testModules := false // We can also have modules buried in test files. for _, file := range earlyRoot.Tests { for _, run := range file.Runs { @@ -365,14 +354,15 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear defer span.End() if upgrade { - c.Ui.Output(c.Colorize().Color("[reset][bold]Upgrading modules...")) + view.Output(views.UpgradingModulesMessage) } else { - c.Ui.Output(c.Colorize().Color("[reset][bold]Initializing modules...")) + view.Output(views.InitializingModulesMessage) } hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: true, + View: view, } installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, false, hooks) @@ -398,12 +388,12 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear return true, installAbort, diags } -func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize Terraform Cloud") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing Terraform Cloud...")) + view.Output(views.InitializingTerraformCloudMessage) if len(extraConfig.AllItems()) != 0 { diags = diags.Append(tfdiags.Sourceless( @@ -417,8 +407,9 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra backendConfig := root.CloudConfig.ToBackendConfig() opts := &BackendOpts{ - Config: &backendConfig, - Init: true, + Config: &backendConfig, + Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -426,12 +417,12 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra return back, true, diags } -func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize backend") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing the backend...")) + view.Output(views.InitializingBackendMessage) var backendConfig *configs.Backend var backendConfigOverride hcl.Body @@ -502,6 +493,7 @@ the backend configuration is present and valid. Config: backendConfig, ConfigOverride: backendConfigOverride, Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -511,7 +503,7 @@ the backend configuration is present and valid. // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. -func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output, abort bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "install providers") defer span.End() @@ -584,15 +576,13 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // are shimming our vt100 output to the legacy console API on Windows. evts := &providercache.InstallerEvents{ PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { - c.Ui.Output(c.Colorize().Color( - "\n[reset][bold]Initializing provider plugins...", - )) + view.Output(views.InitializingProviderPluginMessage) }, ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { - c.Ui.Info(fmt.Sprintf("- Using previously-installed %s v%s", provider.ForDisplay(), selectedVersion)) + view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - c.Ui.Info(fmt.Sprintf("- %s is built in to Terraform", provider.ForDisplay())) + view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -603,20 +593,20 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, }, QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { if locked { - c.Ui.Info(fmt.Sprintf("- Reusing previous version of %s from the dependency lock file", provider.ForDisplay())) + view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay()) } else { if len(versionConstraints) > 0 { - c.Ui.Info(fmt.Sprintf("- Finding %s versions matching %q...", provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))) + view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - c.Ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay())) + view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay()) } } }, LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { - c.Ui.Info(fmt.Sprintf("- Using %s v%s from the shared cache directory", provider.ForDisplay(), version)) + view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) }, FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { - c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version)) + view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -813,10 +803,10 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, keyID = authResult.KeyID } if keyID != "" { - keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID)) + keyID = view.PrepareMessage(views.KeyID, keyID) } - c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID)) + view.LogInitMessage(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID) }, ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) { // We're going to use this opportunity to track if we have any @@ -862,9 +852,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } } if thirdPartySigned { - c.Ui.Info(fmt.Sprintf("\nPartner and community providers are signed by their developers.\n" + - "If you'd like to know more about provider signing, you can read about it here:\n" + - "https://www.terraform.io/docs/cli/plugins/signing.html")) + view.LogInitMessage(views.PartnerAndCommunityProvidersMessage) } }, } @@ -873,7 +861,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, mode := providercache.InstallNewProvidersOnly if upgrade { if flagLockfile == "readonly" { - c.Ui.Error("The -upgrade flag conflicts with -lockfile=readonly.") + diags = diags.Append(fmt.Errorf("The -upgrade flag conflicts with -lockfile=readonly.")) + view.Diagnostics(diags) return true, true, diags } @@ -881,8 +870,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) if ctx.Err() == context.Canceled { - c.showDiagnostics(diags) - c.Ui.Error("Provider installation was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) + view.Diagnostics(diags) return true, true, diags } if err != nil { @@ -949,16 +938,9 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // say a little about what the dependency lock file is, for new // users or those who are upgrading from a previous Terraform // version that didn't have dependency lock files. - c.Ui.Output(c.Colorize().Color(` -Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider -selections it made above. Include this file in your version control repository -so that Terraform can guarantee to make the same selections by default when -you run "terraform init" in the future.`)) + view.Output(views.LockInfo) } else { - c.Ui.Output(c.Colorize().Color(` -Terraform has made some changes to the provider dependency selections recorded -in the .terraform.lock.hcl file. Review those changes and commit them to your -version control system if they represent changes you intended to make.`)) + view.Output(views.DependenciesLockChangesInfo) } moreDiags = c.replaceLockedDependencies(newLocks) @@ -976,7 +958,7 @@ version control system if they represent changes you intended to make.`)) // // If the returned diagnostics contains errors then the returned body may be // incomplete or invalid. -func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { +func (c *InitCommand) backendConfigOverrideBody(flags arguments.FlagNameValueSlice, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { items := flags.AllItems() if len(items) == 0 { return nil, nil @@ -1089,6 +1071,7 @@ func (c *InitCommand) AutocompleteFlags() complete.Flags { "-lock": completePredictBoolean, "-lock-timeout": complete.PredictAnything, "-no-color": complete.PredictNothing, + "-json": complete.PredictNothing, "-plugin-dir": complete.PredictDirs(""), "-reconfigure": complete.PredictNothing, "-migrate-state": complete.PredictNothing, @@ -1151,6 +1134,9 @@ Options: -no-color If specified, output won't contain any color. + -json If specified, machine readable output will be + printed in JSON format. + -plugin-dir Directory containing plugin binaries. This overrides all default search paths for plugins, and prevents the automatic installation of plugins. This flag can be used @@ -1187,14 +1173,6 @@ func (c *InitCommand) Synopsis() string { return "Prepare your working directory for other commands" } -const errInitConfigError = ` -[reset]Terraform encountered problems during initialisation, including problems -with the configuration, described below. - -The Terraform configuration must be valid before initialization so that -Terraform can determine which modules and providers need to be installed. -` - const errInitCopyNotEmpty = ` The working directory already contains files. The -from-module option requires an empty directory into which a copy of the referenced module will be placed. @@ -1203,39 +1181,6 @@ To initialize the configuration already in this working directory, omit the -from-module option. ` -const outputInitEmpty = ` -[reset][bold]Terraform initialized in an empty directory![reset] - -The directory has no Terraform configuration files. You may begin working -with Terraform immediately by creating Terraform configuration files. -` - -const outputInitSuccess = ` -[reset][bold][green]Terraform has been successfully initialized![reset][green] -` - -const outputInitSuccessCloud = ` -[reset][bold][green]Terraform Cloud has been successfully initialized![reset][green] -` - -const outputInitSuccessCLI = `[reset][green] -You may now begin working with Terraform. Try running "terraform plan" to see -any changes that are required for your infrastructure. All Terraform commands -should now work. - -If you ever set or change modules or backend configuration for Terraform, -rerun this command to reinitialize your working directory. If you forget, other -commands will detect it and remind you to do so if necessary. -` - -const outputInitSuccessCLICloud = `[reset][green] -You may now begin working with Terraform Cloud. Try running "terraform plan" to -see any changes that are required for your infrastructure. - -If you ever set or change modules or Terraform Settings, run "terraform init" -again to reinitialize your working directory. -` - // providerProtocolTooOld is a message sent to the CLI UI if the provider's // supported protocol versions are too old for the user's version of terraform, // but a newer version of the provider is compatible. diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 2422b319682f..feb884979345 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -15,11 +15,11 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/hashicorp/cli" + version "github.com/hashicorp/go-version" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" @@ -37,7 +37,7 @@ func TestInit_empty(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -48,7 +48,7 @@ func TestInit_empty(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } @@ -59,7 +59,7 @@ func TestInit_multipleArgs(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -73,10 +73,40 @@ func TestInit_multipleArgs(t *testing.T) { "bad", } if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } +func TestInit_migrateStateAndJSON(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{ + "-migrate-state=true", + "-json=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("error, -migrate-state and -json should be exclusive: \n%s", testOutput.All()) + } + + // Check output + checkGoldenReference(t, testOutput, "init-migrate-state-with-json") +} + func TestInit_fromModule_cwdDest(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -84,7 +114,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -97,7 +127,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { "-from-module=" + testFixturePath("init"), } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { @@ -135,7 +165,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -148,7 +178,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { "-from-module=./..", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { @@ -163,7 +193,7 @@ func TestInit_get(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -174,16 +204,42 @@ func TestInit_get(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Check output - output := ui.OutputWriter.String() + output := done(t).Stdout() if !strings.Contains(output, "foo in foo") { t.Fatalf("doesn't look like we installed module 'foo': %s", output) } } +func TestInit_json(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-get"), td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).All()) + } + + // Check output + output := done(t) + checkGoldenReference(t, output, "init-get") +} + func TestInit_getUpgradeModules(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -191,7 +247,7 @@ func TestInit_getUpgradeModules(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -204,14 +260,15 @@ func TestInit_getUpgradeModules(t *testing.T) { "-get=true", "-upgrade", } - if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("command did not complete successfully:\n%s", testOutput.Stderr()) } // Check output - output := ui.OutputWriter.String() - if !strings.Contains(output, "Upgrading modules...") { - t.Fatalf("doesn't look like get upgrade: %s", output) + if !strings.Contains(testOutput.Stdout(), "Upgrading modules...") { + t.Fatalf("doesn't look like get upgrade: %s", testOutput.Stdout()) } } @@ -222,7 +279,7 @@ func TestInit_backend(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -233,7 +290,7 @@ func TestInit_backend(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { @@ -251,7 +308,7 @@ func TestInit_backendUnset(t *testing.T) { log.Printf("[TRACE] TestInit_backendUnset: beginning first init") ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -262,12 +319,14 @@ func TestInit_backendUnset(t *testing.T) { // Init args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: first init complete") - t.Logf("First run output:\n%s", ui.OutputWriter.String()) - t.Logf("First run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { t.Fatalf("err: %s", err) @@ -283,7 +342,7 @@ func TestInit_backendUnset(t *testing.T) { } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -293,12 +352,14 @@ func TestInit_backendUnset(t *testing.T) { } args := []string{"-force-copy"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: second init complete") - t.Logf("Second run output:\n%s", ui.OutputWriter.String()) - t.Logf("Second run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if !s.Backend.Empty() { @@ -315,7 +376,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Run("good-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -325,7 +386,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config", "input.config"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify we have our settings @@ -338,7 +399,7 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must not be a full terraform block t.Run("full-backend-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -350,15 +411,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported block type") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Unsupported block type") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // the backend config file must match the schema for the backend t.Run("invalid-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -370,15 +431,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported argument") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Unsupported argument") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // missing file is an error t.Run("missing-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -390,15 +451,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Failed to read file") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Failed to read file") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // blank filename clears the backend config t.Run("blank-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -408,7 +469,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config=", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify the backend config is empty @@ -434,7 +495,7 @@ func TestInit_backendConfigFile(t *testing.T) { }, }, } - flagConfigExtra := newRawFlags("-backend-config") + flagConfigExtra := arguments.NewFlagNameValueSlice("-backend-config") flagConfigExtra.Set("input.config") _, diags := c.backendConfigOverrideBody(flagConfigExtra, schema) if len(diags) != 0 { @@ -450,7 +511,7 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -468,12 +529,13 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { // result in an early exit with a diagnostic that the provided // configuration file is not a diretory. args := []string{"-backend-config=", "./input.config"} - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + output := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) } - output := ui.ErrorWriter.String() - if got, want := output, `Too many command line arguments`; !strings.Contains(got, want) { + if got, want := output.Stderr(), `Too many command line arguments`; !strings.Contains(got, want) { t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) } } @@ -490,7 +552,7 @@ func TestInit_backendReconfigure(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -513,7 +575,7 @@ func TestInit_backendReconfigure(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // now run init again, changing the path. @@ -521,7 +583,7 @@ func TestInit_backendReconfigure(t *testing.T) { // Without -reconfigure, the test fails since the backend asks for input on migrating state args = []string{"-reconfigure", "-backend-config", "path=changed"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } } @@ -532,7 +594,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -543,7 +605,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { args := []string{"-backend-config", "input.config", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -565,7 +627,7 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -596,13 +658,13 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { // Attempt to migrate args := []string{"-backend-config", "input.config", "-migrate-state", "-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("expected nonzero exit code: %s", ui.OutputWriter.String()) + t.Fatalf("expected nonzero exit code: %s", done(t).Stdout()) } // Disabling locking should work args = []string{"-backend-config", "input.config", "-migrate-state", "-force-copy", "-lock=false"} if code := c.Run(args); code != 0 { - t.Fatalf("expected zero exit code, got %d: %s", code, ui.ErrorWriter.String()) + t.Fatalf("expected zero exit code, got %d: %s", code, done(t).Stderr()) } } @@ -613,10 +675,13 @@ func TestInit_backendConfigFileChangeWithExistingState(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) + c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -647,7 +712,7 @@ func TestInit_backendConfigKV(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -658,7 +723,7 @@ func TestInit_backendConfigKV(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -675,7 +740,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -686,7 +751,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -701,7 +766,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // a second init should require no changes, nor should it change the backend. args = []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -717,7 +782,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // override the -backend-config options by settings args = []string{"-input=false", "-backend-config", "", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -738,7 +803,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -749,7 +814,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { args := []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -765,7 +830,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { // should it change the backend. args = []string{"-input=false", "-backend-config", "path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -786,7 +851,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -797,10 +862,10 @@ func TestInit_backendCli_no_config_block(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "Warning: Missing backend configuration") { t.Fatal("expected missing backend block warning, got", errMsg) } @@ -825,7 +890,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -836,7 +901,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -851,7 +916,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { // init again and make sure nothing changes if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { @@ -869,7 +934,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -879,7 +944,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { } if code := c.Run([]string{"-input=false"}); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -908,7 +973,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"foo","workspace_dir":null}`; got != want { @@ -978,7 +1043,7 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { // configuration is only about which workspaces we'll be working // with. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -987,10 +1052,10 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { } args := []string{"-backend-config=anything"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -999,7 +1064,6 @@ is not applicable to Terraform Cloud-based configurations. To change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1017,7 +1081,7 @@ Cloud configuration block in the root module. // -reconfigure doesn't really make sense in that context, particularly // with its design bug with the handling of the implicit local backend. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1026,10 +1090,10 @@ Cloud configuration block in the root module. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1038,7 +1102,6 @@ only, and is not needed when changing Terraform Cloud settings. When using Terraform Cloud, initialization automatically activates any new Cloud configuration settings. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1056,7 +1119,7 @@ Cloud configuration settings. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1065,16 +1128,15 @@ Cloud configuration settings. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -reconfigure option is unsupported when migrating to Terraform Cloud, because activating Terraform Cloud involves some additional steps. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1087,7 +1149,7 @@ because activating Terraform Cloud involves some additional steps. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1096,10 +1158,10 @@ because activating Terraform Cloud involves some additional steps. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1108,7 +1170,6 @@ is not applicable when using Terraform Cloud. State storage is handled automatically by Terraform Cloud and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1126,7 +1187,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1135,10 +1196,10 @@ storage location is not configurable. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1147,7 +1208,6 @@ is not applicable when using Terraform Cloud. Terraform Cloud migration has additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1160,7 +1220,7 @@ prompts. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1169,10 +1229,10 @@ prompts. } args := []string{"-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1181,7 +1241,6 @@ not applicable when using Terraform Cloud. State storage is handled automatically by Terraform Cloud and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1199,7 +1258,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1207,11 +1266,13 @@ storage location is not configurable. }, } args := []string{"-force-copy"} - if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("unexpected success\n%s", testOutput.Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := testOutput.Stderr() wantStderr := ` Error: Invalid command-line option @@ -1220,7 +1281,6 @@ not applicable when using Terraform Cloud. Terraform Cloud migration has additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1236,7 +1296,7 @@ func TestInit_inputFalse(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -1247,7 +1307,7 @@ func TestInit_inputFalse(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // write different states for foo and bar @@ -1283,10 +1343,10 @@ func TestInit_inputFalse(t *testing.T) { args = []string{"-input=false", "-backend-config=path=bar", "-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "interactive input is disabled") { t.Fatal("expected input disabled error, got", errMsg) } @@ -1303,7 +1363,7 @@ func TestInit_inputFalse(t *testing.T) { // A missing input=false should abort rather than loop infinitely args = []string{"-backend-config=path=baz"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } } @@ -1315,7 +1375,7 @@ func TestInit_getProvider(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "exact": {"1.2.3"}, @@ -1340,7 +1400,7 @@ func TestInit_getProvider(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1394,18 +1454,20 @@ func TestInit_getProvider(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m.Ui = ui m.View = view c := &InitCommand{ Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatal("expected error, got:", ui.OutputWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatal("expected error, got:", testOutput.Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := testOutput.Stderr() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1420,7 +1482,7 @@ func TestInit_getProviderSource(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "acme/alpha": {"1.2.3"}, @@ -1444,7 +1506,7 @@ func TestInit_getProviderSource(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1470,7 +1532,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "acme/alpha": {"1.2.3"}, }) @@ -1485,9 +1547,10 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { c := &InitCommand{ Meta: m, } - - if code := c.Run(nil); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(nil) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // Expect this diagnostic output @@ -1495,7 +1558,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { "Invalid legacy provider address", "You must complete the Terraform 0.13 upgrade process", } - got := ui.ErrorWriter.String() + got := testOutput.All() for _, want := range wants { if !strings.Contains(got, want) { t.Fatalf("expected output to contain %q, got:\n\n%s", want, got) @@ -1511,7 +1574,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) // create a provider source which allows installing an invalid package addr := addrs.MustParseProviderSourceString("invalid/package") @@ -1543,8 +1606,10 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // invalid provider should be installed @@ -1557,7 +1622,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { "Failed to install provider", "could not find executable file starting with terraform-provider-package", } - got := ui.ErrorWriter.String() + got := testOutput.All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1588,7 +1653,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -1602,8 +1667,10 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } // foo should be installed @@ -1618,7 +1685,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := ui.ErrorWriter.String() + errOutput := testOutput.All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1646,7 +1713,7 @@ func TestInit_providerSource(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1659,11 +1726,12 @@ func TestInit_providerSource(t *testing.T) { } args := []string{} - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } - if strings.Contains(ui.OutputWriter.String(), "Terraform has initialized, but configuration upgrades may be needed") { + if strings.Contains(testOutput.Stdout(), "Terraform has initialized, but configuration upgrades may be needed") { t.Fatalf("unexpected \"configuration upgrade\" warning in output") } @@ -1732,10 +1800,10 @@ func TestInit_providerSource(t *testing.T) { t.Errorf("wrong version selections after upgrade\n%s", diff) } - if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { + if got, want := testOutput.Stdout(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { t.Fatalf("unexpected output: %s\nexpected to include %q", got, want) } - if got, want := ui.ErrorWriter.String(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { + if got, want := testOutput.All(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1754,7 +1822,7 @@ func TestInit_cancelModules(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1767,12 +1835,13 @@ func TestInit_cancelModules(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.Stdout()) } - if got, want := ui.ErrorWriter.String(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1796,7 +1865,7 @@ func TestInit_cancelProviders(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1810,15 +1879,16 @@ func TestInit_cancelProviders(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.All()) } // Currently the first operation that is cancelable is provider // installation, so our error message comes from there. If we // make the earlier steps cancelable in future then it'd be // expected for this particular message to change. - if got, want := ui.ErrorWriter.String(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1840,7 +1910,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1861,7 +1931,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { "-upgrade=true", } if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + t.Fatalf("command did not complete successfully:\n%s", done(t).All()) } cacheDir := m.providerLocalCacheDir() @@ -1965,7 +2035,7 @@ func TestInit_getProviderMissing(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1978,12 +2048,14 @@ func TestInit_getProviderMissing(t *testing.T) { } args := []string{} - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } - if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") { - t.Fatalf("unexpected error output: %s", ui.ErrorWriter) + if !strings.Contains(testOutput.All(), "no available releases match") { + t.Fatalf("unexpected error output: %s", testOutput.Stderr()) } } @@ -1994,7 +2066,7 @@ func TestInit_checkRequiredVersion(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2005,9 +2077,9 @@ func TestInit_checkRequiredVersion(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2025,7 +2097,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2036,9 +2108,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2049,7 +2121,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2060,9 +2132,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2083,7 +2155,7 @@ func TestInit_providerLockFile(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2097,7 +2169,7 @@ func TestInit_providerLockFile(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } lockFile := ".terraform.lock.hcl" @@ -2128,7 +2200,7 @@ provider "registry.terraform.io/hashicorp/test" { // succeeds, to ensure that we don't try to rewrite an unchanged lock file os.Chmod(".", 0555) if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } @@ -2268,9 +2340,11 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -2286,10 +2360,10 @@ provider "registry.terraform.io/hashicorp/test" { code := c.Run(tc.args) if tc.ok && code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } if !tc.ok && code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + t.Fatalf("expected error, got output: \n%s", done(t).Stdout()) } buf, err := ioutil.ReadFile(lockFile) @@ -2314,7 +2388,7 @@ func TestInit_pluginDirReset(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2335,7 +2409,7 @@ func TestInit_pluginDirReset(t *testing.T) { // run once and save the -plugin-dir args := []string{"-plugin-dir", "a"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err := c.loadPluginPath() @@ -2360,7 +2434,7 @@ func TestInit_pluginDirReset(t *testing.T) { // make sure we remove the plugin-dir record args = []string{"-plugin-dir="} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err = c.loadPluginPath() @@ -2384,7 +2458,7 @@ func TestInit_pluginDirProviders(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2425,7 +2499,7 @@ func TestInit_pluginDirProviders(t *testing.T) { "-plugin-dir", "c", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } locks, err := m.lockedDependencies() @@ -2485,7 +2559,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2523,15 +2597,17 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { "-plugin-dir", "a", "-plugin-dir", "b", } - if code := c.Run(args); code == 0 { + code := c.Run(args) + testOutput := done(t) + if code == 0 { // should have been an error - t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", ui.OutputWriter, ui.ErrorWriter) + t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", testOutput.Stdout(), testOutput.Stderr()) } // The error output should mention the "between" provider but should not // mention either the "exact" or "greater-than" provider, because the // latter two are available via the -plugin-dir directories. - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "hashicorp/between"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the 'between' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2558,7 +2634,7 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2571,11 +2647,13 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { } args := []string{"-plugin-dir", "./"} - if code := c.Run(args); code != 0 { - t.Fatalf("error: %s", ui.ErrorWriter) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("error: %s", testOutput.Stderr()) } - outputStr := ui.OutputWriter.String() + outputStr := testOutput.Stdout() if subStr := "terraform.io/builtin/terraform is built in to Terraform"; !strings.Contains(outputStr, subStr) { t.Errorf("output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, outputStr) } @@ -2596,7 +2674,7 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2608,11 +2686,13 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "Cannot use terraform.io/builtin/terraform: built-in"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2627,7 +2707,7 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2637,11 +2717,13 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2656,7 +2738,7 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2666,11 +2748,13 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2685,7 +2769,7 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2695,11 +2779,13 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2717,7 +2803,7 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2727,11 +2813,13 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.All() if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2757,7 +2845,7 @@ func TestInit_tests(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2769,7 +2857,7 @@ func TestInit_tests(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } } @@ -2787,7 +2875,7 @@ func TestInit_testsWithProvider(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2798,18 +2886,19 @@ func TestInit_testsWithProvider(t *testing.T) { } args := []string{} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected failure but got: \n%s", testOutput.All()) } - got := ui.ErrorWriter.String() + got := testOutput.Stderr() want := ` Error: Failed to query available provider packages Could not retrieve the list of available versions for provider hashicorp/test: no available releases match the given constraints 1.0.1, 1.0.2 - ` if diff := cmp.Diff(got, want); len(diff) > 0 { t.Fatalf("wrong error message: \ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) @@ -2830,7 +2919,7 @@ func TestInit_testsWithModule(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2841,12 +2930,14 @@ func TestInit_testsWithModule(t *testing.T) { } args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } // Check output - output := ui.OutputWriter.String() + output := testOutput.Stdout() if !strings.Contains(output, "test.main.setup in setup") { t.Fatalf("doesn't look like we installed the test module': %s", output) } diff --git a/internal/command/jsonformat/plan_test.go b/internal/command/jsonformat/plan_test.go index 754db9156d85..ddce6344992f 100644 --- a/internal/command/jsonformat/plan_test.go +++ b/internal/command/jsonformat/plan_test.go @@ -19,7 +19,6 @@ import ( "github.com/hashicorp/terraform/internal/command/jsonplan" "github.com/hashicorp/terraform/internal/command/jsonprovider" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" @@ -3503,11 +3502,8 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "disks"}}, }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -3597,11 +3593,8 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -3647,11 +3640,8 @@ func TestResourceChange_nestedSet(t *testing.T) { "volume_type": cty.String, })), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -4397,14 +4387,8 @@ func TestResourceChange_nestedMap(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}, - cty.IndexStep{Key: cty.StringVal("disk_a")}, - cty.GetAttrStep{Name: "mount_point"}, - }, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks").IndexString("disk_a").GetAttr("mount_point"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchemaPlus(configschema.NestingMap), @@ -6067,32 +6051,14 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - // Nested blocks/sets will mark the whole set/block as sensitive - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + // Nested blocks/sets will mark the whole set/block as sensitive + cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6211,39 +6177,15 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "special"}}, + cty.Path{cty.GetAttrStep{Name: "some_number"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6359,27 +6301,12 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { "an_attr": cty.StringVal("changed"), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6472,49 +6399,19 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6617,39 +6514,15 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "special"}}, + cty.Path{cty.GetAttrStep{Name: "some_number"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6757,31 +6630,13 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), After: cty.NullVal(cty.EmptyObject), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -6845,25 +6700,13 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.GetAttrPath("ami"), + cty.GetAttrPath("nested_block_set"), }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("ami"), + cty.GetAttrPath("nested_block_set"), }, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -7046,20 +6889,20 @@ func TestResourceChange_moved(t *testing.T) { } type testCase struct { - Action plans.Action - ActionReason plans.ResourceInstanceChangeActionReason - ModuleInst addrs.ModuleInstance - Mode addrs.ResourceMode - InstanceKey addrs.InstanceKey - DeposedKey states.DeposedKey - Before cty.Value - BeforeValMarks []cty.PathValueMarks - AfterValMarks []cty.PathValueMarks - After cty.Value - Schema *configschema.Block - RequiredReplace cty.PathSet - ExpectedOutput string - PrevRunAddr addrs.AbsResourceInstance + Action plans.Action + ActionReason plans.ResourceInstanceChangeActionReason + ModuleInst addrs.ModuleInstance + Mode addrs.ResourceMode + InstanceKey addrs.InstanceKey + DeposedKey states.DeposedKey + Before cty.Value + BeforeSensitivePaths []cty.Path + After cty.Value + AfterSensitivePaths []cty.Path + Schema *configschema.Block + RequiredReplace cty.PathSet + ExpectedOutput string + PrevRunAddr addrs.AbsResourceInstance } func runTestCases(t *testing.T, testCases map[string]testCase) { @@ -7110,11 +6953,11 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { src := &plans.ResourceInstanceChangeSrc{ ChangeSrc: plans.ChangeSrc{ - Action: tc.Action, - Before: beforeDynamicValue, - BeforeValMarks: tc.BeforeValMarks, - After: afterDynamicValue, - AfterValMarks: tc.AfterValMarks, + Action: tc.Action, + Before: beforeDynamicValue, + BeforeSensitivePaths: tc.BeforeSensitivePaths, + After: afterDynamicValue, + AfterSensitivePaths: tc.AfterSensitivePaths, }, Addr: addr, diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 35a2a01176f4..46b4ed8b4770 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform/internal/command/jsonconfig" "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" @@ -418,11 +419,9 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema if err != nil { return nil, err } - marks := rc.BeforeValMarks - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(changeV.Before, nil)...) - } - bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks)) + sensitivePaths := rc.BeforeSensitivePaths + sensitivePaths = append(sensitivePaths, schema.SensitivePaths(changeV.Before, nil)...) + bs := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.Before, marks.Sensitive, sensitivePaths)) beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) if err != nil { return nil, err @@ -447,11 +446,9 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema } afterUnknown = unknownAsBool(changeV.After) } - marks := rc.AfterValMarks - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(changeV.After, nil)...) - } - as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks)) + sensitivePaths := rc.AfterSensitivePaths + sensitivePaths = append(sensitivePaths, schema.SensitivePaths(changeV.After, nil)...) + as := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.After, marks.Sensitive, sensitivePaths)) afterSensitive, err = ctyjson.Marshal(as, as.Type()) if err != nil { return nil, err diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index e7b1d07de53d..f77da0e036c9 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) const ( @@ -115,12 +116,15 @@ type Resource struct { // resource, whose structure depends on the resource type schema. type AttributeValues map[string]json.RawMessage -func marshalAttributeValues(value cty.Value) AttributeValues { +func marshalAttributeValues(value cty.Value) (unmarkedVal cty.Value, marshalledVals AttributeValues, sensitivePaths []cty.Path, err error) { // unmark our value to show all values - value, _ = value.UnmarkDeep() + value, sensitivePaths, err = unmarkValueForMarshaling(value) + if err != nil { + return cty.NilVal, nil, nil, err + } if value == cty.NilVal || value.IsNull() { - return nil + return value, nil, nil, nil } ret := make(AttributeValues) @@ -131,7 +135,7 @@ func marshalAttributeValues(value cty.Value) AttributeValues { vJSON, _ := ctyjson.Marshal(v, v.Type()) ret[k.AsString()] = json.RawMessage(vJSON) } - return ret + return value, ret, sensitivePaths, nil } // newState() returns a minimally-initialized state @@ -397,13 +401,14 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module return nil, err } - current.AttributeValues = marshalAttributeValues(riObj.Value) - - value, marks := riObj.Value.UnmarkDeepWithPaths() - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(value, nil)...) + var value cty.Value + var sensitivePaths []cty.Path + value, current.AttributeValues, sensitivePaths, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) } - s := SensitiveAsBool(value.MarkWithPaths(marks)) + sensitivePaths = append(sensitivePaths, schema.SensitivePaths(value, nil)...) + s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { return nil, err @@ -448,13 +453,14 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module return nil, err } - deposed.AttributeValues = marshalAttributeValues(riObj.Value) - - value, marks := riObj.Value.UnmarkDeepWithPaths() - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(value, nil)...) + var value cty.Value + var sensitivePaths []cty.Path + value, deposed.AttributeValues, sensitivePaths, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) } - s := SensitiveAsBool(value.MarkWithPaths(marks)) + sensitivePaths = append(sensitivePaths, schema.SensitivePaths(value, nil)...) + s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { return nil, err @@ -552,3 +558,24 @@ func SensitiveAsBool(val cty.Value) cty.Value { panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val)) } } + +// unmarkValueForMarshaling takes a value that possibly contains marked values +// and returns an equal value without markings along with the separated mark +// metadata that should be presented alongside the value in another JSON +// property. +// +// This function only accepts the marks that are valid to persist, and so will +// return an error if other marks are present. Marks that this package doesn't +// know how to store must be dealt with somehow by a caller -- presumably by +// replacing each marked value with some sort of storage placeholder. +func unmarkValueForMarshaling(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { + val, pvms := v.UnmarkDeepWithPaths() + sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive) + if len(otherMarks) != 0 { + return cty.NilVal, nil, fmt.Errorf( + "%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarks[0].Path), otherMarks[0].Marks, + ) + } + return val, sensitivePaths, err +} diff --git a/internal/command/jsonstate/state_test.go b/internal/command/jsonstate/state_test.go index bbb46b6ba9c5..2e7e8ed22b77 100644 --- a/internal/command/jsonstate/state_test.go +++ b/internal/command/jsonstate/state_test.go @@ -5,6 +5,7 @@ package jsonstate import ( "encoding/json" + "fmt" "reflect" "testing" @@ -114,28 +115,33 @@ func TestMarshalOutputs(t *testing.T) { func TestMarshalAttributeValues(t *testing.T) { tests := []struct { - Attr cty.Value - Want AttributeValues + Attr cty.Value + Want AttributeValues + WantSensitivePaths []cty.Path }{ { cty.NilVal, nil, + nil, }, { cty.NullVal(cty.String), nil, + nil, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), }), AttributeValues{"foo": json.RawMessage(`"bar"`)}, + nil, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.NullVal(cty.String), }), AttributeValues{"foo": json.RawMessage(`null`)}, + nil, }, { cty.ObjectVal(map[string]cty.Value{ @@ -151,8 +157,9 @@ func TestMarshalAttributeValues(t *testing.T) { "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, + nil, }, - // Marked values + // Sensitive values { cty.ObjectVal(map[string]cty.Value{ "bar": cty.MapVal(map[string]cty.Value{ @@ -167,16 +174,43 @@ func TestMarshalAttributeValues(t *testing.T) { "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, + []cty.Path{ + cty.GetAttrPath("baz").IndexInt(1), + }, }, } for _, test := range tests { - got := marshalAttributeValues(test.Attr) - eq := reflect.DeepEqual(got, test.Want) - if !eq { - t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) - } + t.Run(fmt.Sprintf("%#v", test.Attr), func(t *testing.T) { + val, got, sensitivePaths, err := marshalAttributeValues(test.Attr) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v\n", got, test.Want) + } + if !reflect.DeepEqual(sensitivePaths, test.WantSensitivePaths) { + t.Errorf("wrong marks\ngot: %#v\nwant: %#v\n", sensitivePaths, test.WantSensitivePaths) + } + if _, marks := val.Unmark(); len(marks) != 0 { + t.Errorf("returned value still has marks; should have been unmarked\n%#v", marks) + } + }) } + + t.Run("reject unsupported marks", func(t *testing.T) { + _, _, _, err := marshalAttributeValues(cty.ObjectVal(map[string]cty.Value{ + "disallowed": cty.StringVal("a").Mark("unsupported"), + })) + if err == nil { + t.Fatalf("unexpected success; want error") + } + got := err.Error() + want := `.disallowed: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + }) } func TestMarshalResources(t *testing.T) { @@ -292,9 +326,8 @@ func TestMarshalResources(t *testing.T) { Current: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"foozles":"confuzles"}`), - AttrSensitivePaths: []cty.PathValueMarks{{ - Path: cty.Path{cty.GetAttrStep{Name: "foozles"}}, - Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foozles"), }, }, }, @@ -558,9 +591,8 @@ func TestMarshalResources(t *testing.T) { Current: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"data":{"woozles":"confuzles"}}`), - AttrSensitivePaths: []cty.PathValueMarks{{ - Path: cty.Path{cty.GetAttrStep{Name: "data"}}, - Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("data"), }, }, }, diff --git a/internal/command/meta.go b/internal/command/meta.go index 0d832251697a..c9dd11a71d7d 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -204,7 +204,7 @@ type Meta struct { backendState *workdir.BackendState // Variables for the context (private) - variableArgs rawFlags + variableArgs arguments.FlagNameValueSlice input bool // Targets for this context (private) @@ -579,11 +579,11 @@ func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { f := m.defaultFlagSet(n) f.BoolVar(&m.input, "input", true, "input") - f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target") + f.Var((*arguments.FlagStringSlice)(&m.targetFlags), "target", "resource to target") f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings") - if m.variableArgs.items == nil { - m.variableArgs = newRawFlags("-var") + if m.variableArgs.Items == nil { + m.variableArgs = arguments.NewFlagNameValueSlice("-var") } varValues := m.variableArgs.Alias("-var") varFiles := m.variableArgs.Alias("-var-file") diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index eb282cd66355..594560c1a1d0 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -208,7 +208,7 @@ func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upg if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module installation was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module installation was canceled by an interrupt signal.")) return true, diags } @@ -241,7 +241,7 @@ func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr str diags = diags.Append(moreDiags) if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module initialization was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module initialization was canceled by an interrupt signal.")) return true, diags } return false, diags @@ -411,60 +411,3 @@ func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty. return val, diags } } - -// rawFlags is a flag.Value implementation that just appends raw flag -// names and values to a slice. -type rawFlags struct { - flagName string - items *[]rawFlag -} - -func newRawFlags(flagName string) rawFlags { - var items []rawFlag - return rawFlags{ - flagName: flagName, - items: &items, - } -} - -func (f rawFlags) Empty() bool { - if f.items == nil { - return true - } - return len(*f.items) == 0 -} - -func (f rawFlags) AllItems() []rawFlag { - if f.items == nil { - return nil - } - return *f.items -} - -func (f rawFlags) Alias(flagName string) rawFlags { - return rawFlags{ - flagName: flagName, - items: f.items, - } -} - -func (f rawFlags) String() string { - return "" -} - -func (f rawFlags) Set(str string) error { - *f.items = append(*f.items, rawFlag{ - Name: f.flagName, - Value: str, - }) - return nil -} - -type rawFlag struct { - Name string - Value string -} - -func (f rawFlag) String() string { - return fmt.Sprintf("%s=%q", f.Name, f.Value) -} diff --git a/internal/command/meta_vars.go b/internal/command/meta_vars.go index 18a151678529..dc0690232526 100644 --- a/internal/command/meta_vars.go +++ b/internal/command/meta_vars.go @@ -150,13 +150,13 @@ func (m *Meta) collectVariableValues() (map[string]backendrun.UnparsedVariableVa // Finally we process values given explicitly on the command line, either // as individual literal settings or as additional files to read. - for _, rawFlag := range m.variableArgs.AllItems() { - switch rawFlag.Name { + for _, flagNameValue := range m.variableArgs.AllItems() { + switch flagNameValue.Name { case "-var": // Value should be in the form "name=value", where value is a // raw string whose interpretation will depend on the variable's // parsing mode. - raw := rawFlag.Value + raw := flagNameValue.Value eq := strings.Index(raw, "=") if eq == -1 { diags = diags.Append(tfdiags.Sourceless( @@ -183,13 +183,13 @@ func (m *Meta) collectVariableValues() (map[string]backendrun.UnparsedVariableVa } case "-var-file": - moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromNamedFile, ret) + moreDiags := m.addVarsFromFile(flagNameValue.Value, terraform.ValueFromNamedFile, ret) diags = diags.Append(moreDiags) default: // Should never happen; always a bug in the code that built up // the contents of m.variableArgs. - diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) + diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", flagNameValue.Name)) } } diff --git a/internal/command/plan.go b/internal/command/plan.go index 056dfd3b9731..a477b4061a90 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -200,12 +200,12 @@ func (c *PlanCommand) GatherVariables(opReq *backendrun.Operation, args *argumen // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index c78d30f8bee0..2571bbe33a69 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -10,6 +10,7 @@ import ( "os" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/providercache" @@ -40,7 +41,7 @@ func (c *ProvidersLockCommand) Synopsis() string { func (c *ProvidersLockCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers lock") - var optPlatforms FlagStringSlice + var optPlatforms arguments.FlagStringSlice var fsMirrorDir string var netMirrorURL string diff --git a/internal/command/providers_mirror.go b/internal/command/providers_mirror.go index 5b2b127f0cba..a7f0d4556905 100644 --- a/internal/command/providers_mirror.go +++ b/internal/command/providers_mirror.go @@ -13,6 +13,7 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/tfdiags" @@ -33,7 +34,7 @@ func (c *ProvidersMirrorCommand) Synopsis() string { func (c *ProvidersMirrorCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers mirror") - var optPlatforms FlagStringSlice + var optPlatforms arguments.FlagStringSlice cmdFlags.Var(&optPlatforms, "platform", "target platform") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index ce3ee2425cf3..235f1094338f 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -59,9 +59,11 @@ func TestProvidersSchema_output(t *testing.T) { p := providersSchemaFixtureProvider() ui := new(cli.MockUi) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -70,12 +72,9 @@ func TestProvidersSchema_output(t *testing.T) { Meta: m, } if code := ic.Run([]string{}); code != 0 { - t.Fatalf("init failed\n%s", ui.ErrorWriter) + t.Fatalf("init failed\n%s", done(t).Stderr()) } - // flush the init output from the mock ui - ui.OutputWriter.Reset() - // `terraform provider schemas` command pc := &ProvidersSchemaCommand{Meta: m} if code := pc.Run([]string{"-json"}); code != 0 { diff --git a/internal/command/providers_test.go b/internal/command/providers_test.go index 56f5c0fd7eb4..98c0c1fe190d 100644 --- a/internal/command/providers_test.go +++ b/internal/command/providers_test.go @@ -84,6 +84,7 @@ func TestProviders_modules(t *testing.T) { // first run init with mock provider sources to install the module initUi := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "foo": {"1.0.0"}, "bar": {"2.0.0"}, @@ -93,6 +94,7 @@ func TestProviders_modules(t *testing.T) { m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: initUi, + View: view, ProviderSource: providerSource, } ic := &InitCommand{ diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 15160f98d0e6..4c43d6d5808b 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -182,12 +182,12 @@ func (c *RefreshCommand) GatherVariables(opReq *backendrun.Operation, args *argu // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/show_test.go b/internal/command/show_test.go index cc549e860edf..a8c369066b84 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -558,10 +558,12 @@ func TestShow_json_output(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -666,10 +668,12 @@ func TestShow_json_output_sensitive(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -759,10 +763,12 @@ func TestShow_json_output_conditions_refresh_only(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -868,10 +874,12 @@ func TestShow_json_output_state(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } diff --git a/internal/command/test.go b/internal/command/test.go index cbe2d0b19f8d..bab14fc38b83 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -28,13 +28,13 @@ func (c *TestCommand) Help() string { helpText := ` Usage: terraform [global options] test [options] - Executes automated integration tests against the current Terraform + Executes automated integration tests against the current Terraform configuration. - Terraform will search for .tftest.hcl files within the current configuration - and testing directories. Terraform will then execute the testing run blocks - within any testing files in order, and verify conditional checks and - assertions against the created infrastructure. + Terraform will search for .tftest.hcl files within the current configuration + and testing directories. Terraform will then execute the testing run blocks + within any testing files in order, and verify conditional checks and + assertions against the created infrastructure. This command creates real infrastructure and will attempt to clean up the testing infrastructure on completion. Monitor the output carefully to ensure @@ -42,11 +42,11 @@ Usage: terraform [global options] test [options] Options: - -cloud-run=source If specified, Terraform will execute this test run - remotely using Terraform Cloud. You must specify the + -cloud-run=source If specified, Terraform will execute this test run + remotely using Terraform Cloud. You must specify the source of a module registered in a private module - registry as the argument to this flag. This allows - Terraform to associate the cloud run with the correct + registry as the argument to this flag. This allows + Terraform to associate the cloud run with the correct Terraform Cloud module and organization. -filter=testfile If specified, Terraform will only execute the test files @@ -58,7 +58,7 @@ Options: -no-color If specified, output won't contain any color. - -test-directory=path Set the Terraform test directory, defaults to "tests". + -test-directory=path Set the Terraform test directory, defaults to "tests". -var 'foo=bar' Set a value for one of the input variables in the root module of the configuration. Use this option more than @@ -147,14 +147,14 @@ func (c *TestCommand) Run(rawArgs []string) int { // Users can also specify variables via the command line, so we'll parse // all that here. - var items []rawFlag + var items []arguments.FlagNameValue for _, variable := range args.Vars.All() { - items = append(items, rawFlag{ + items = append(items, arguments.FlagNameValue{ Name: variable.Name, Value: variable.Value, }) } - c.variableArgs = rawFlags{items: &items} + c.variableArgs = arguments.FlagNameValueSlice{Items: &items} // Collect variables for "terraform test" testVariables, variableDiags := c.collectVariableValuesForTests(args.TestDirectory) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index bf8a1f3e36d9..cff14768e2bf 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -284,14 +284,15 @@ func TestTest_Runs(t *testing.T) { } if code := init.Run(nil); code != tc.initCode { - t.Fatalf("expected status code %d but got %d: %s", tc.initCode, code, ui.ErrorWriter) + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", tc.initCode, code, output.All()) } if tc.initCode > 0 { // Then we don't expect the init step to succeed. So we'll check // the init output for our expected error messages and outputs. - - stdout, stderr := ui.ErrorWriter.String(), ui.ErrorWriter.String() + output := done(t).All() + stdout, stderr := output, output if !strings.Contains(stdout, tc.expectedOut) { t.Errorf("output didn't contain expected string:\n\n%s", stdout) @@ -313,6 +314,14 @@ func TestTest_Runs(t *testing.T) { return } + // discard the output from the init command + done(t) + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } @@ -486,16 +495,23 @@ func TestTest_ProviderAlias(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + command := &TestCommand{ Meta: meta, } code := command.Run(nil) - output := done(t) + output = done(t) printedOutput := false @@ -562,16 +578,23 @@ func TestTest_ModuleDependencies(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + command := &TestCommand{ Meta: meta, } code := command.Run(nil) - output := done(t) + output = done(t) printedOutput := false @@ -854,16 +877,23 @@ can remove the provider configuration again. Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } code := c.Run([]string{"-no-color"}) - output := done(t) + output = done(t) if code != 1 { t.Errorf("expected status code 1 but got %d", code) @@ -872,8 +902,8 @@ can remove the provider configuration again. actualOut, expectedOut := output.Stdout(), tc.expectedOut actualErr, expectedErr := output.Stderr(), tc.expectedErr - if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + if !strings.Contains(actualOut, expectedOut) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expectedOut, actualOut) } if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { @@ -970,16 +1000,23 @@ func TestTest_StatePropagation(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } code := c.Run([]string{"-verbose", "-no-color"}) - output := done(t) + output = done(t) if code != 0 { t.Errorf("expected status code 0 but got %d", code) @@ -1063,8 +1100,8 @@ Success! 5 passed, 0 failed. actual := output.All() - if diff := cmp.Diff(actual, expected); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + if !strings.Contains(actual, expected) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual) } if provider.ResourceCount() > 0 { @@ -1100,16 +1137,23 @@ func TestTest_OnlyExternalModules(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } code := c.Run([]string{"-no-color"}) - output := done(t) + output = done(t) if code != 0 { t.Errorf("expected status code 0 but got %d", code) @@ -1124,10 +1168,10 @@ main.tftest.hcl... pass Success! 2 passed, 0 failed. ` - actual := output.All() + actual := output.Stdout() - if diff := cmp.Diff(actual, expected); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + if !strings.Contains(actual, expected) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual) } if provider.ResourceCount() > 0 { @@ -1681,11 +1725,9 @@ func TestTest_SensitiveInputValues(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) - ui := new(cli.MockUi) meta := Meta{ testingOverrides: metaOverridesForProvider(provider.Provider), - Ui: ui, View: view, Streams: streams, ProviderSource: providerSource, @@ -1695,16 +1737,23 @@ func TestTest_SensitiveInputValues(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } - code := c.Run([]string{"-no-color"}) - output := done(t) + code := c.Run([]string{"-no-color", "-verbose"}) + output = done(t) if code != 0 { t.Errorf("expected status code 0 but got %d", code) @@ -1712,17 +1761,26 @@ func TestTest_SensitiveInputValues(t *testing.T) { expected := `main.tftest.hcl... in progress run "setup"... pass + + + +Outputs: + +password = (sensitive value) + run "test"... pass -Warning: Sensitive metadata on variable lost +# test_resource.resource: +resource "test_resource" "resource" { + destroy_fail = false + id = "9ddca5a9" + value = (sensitive value) +} + - on main.tftest.hcl line 13, in run "test": - 13: password = run.setup.password +Outputs: -The input variable is marked as sensitive, while the receiving configuration -is not. The underlying sensitive information may be exposed when var.password -is referenced. Mark the variable block in the configuration as sensitive to -resolve this warning. +password = (sensitive value) main.tftest.hcl... tearing down main.tftest.hcl... pass @@ -1900,16 +1958,23 @@ func TestTest_InvalidOverrides(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + c := &TestCommand{ Meta: meta, } code := c.Run([]string{"-no-color"}) - output := done(t) + output = done(t) if code != 0 { t.Errorf("expected status code 0 but got %d", code) @@ -1994,16 +2059,23 @@ func TestTest_RunBlocksInProviders(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + test := &TestCommand{ Meta: meta, } code := test.Run([]string{"-no-color"}) - output := done(t) + output = done(t) if code != 0 { t.Errorf("expected status code 0 but got %d", code) @@ -2055,16 +2127,23 @@ func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { Meta: meta, } + output := done(t) + if code := init.Run(nil); code != 0 { - t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + test := &TestCommand{ Meta: meta, } code := test.Run([]string{"-no-color"}) - output := done(t) + output = done(t) if code != 1 { t.Errorf("expected status code 1 but got %d", code) diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog new file mode 100644 index 000000000000..88acf532fd07 --- /dev/null +++ b/internal/command/testdata/init-get/output.jsonlog @@ -0,0 +1,7 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code": "initializing_backend_message","type":"init_output"} +{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","message_code": "initializing_modules_message","type":"init_output"} +{"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} +{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","message_code": "initializing_provider_plugin_message","type":"init_output"} +{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","message_code": "output_init_success_message","type":"init_output"} +{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","message_code": "output_init_success_cli_message","type":"init_output"} diff --git a/internal/command/testdata/init-migrate-state-with-json/hello.tf b/internal/command/testdata/init-migrate-state-with-json/hello.tf new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/internal/command/testdata/init-migrate-state-with-json/output.jsonlog b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog new file mode 100644 index 000000000000..1f52cb38de68 --- /dev/null +++ b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"error","@message":"Error: The -migrate-state and -json options are mutually-exclusive","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"The -migrate-state and -json options are mutually-exclusive","detail":"Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option."},"type":"diagnostic"} diff --git a/internal/command/testdata/test/sensitive_input_values/main.tf b/internal/command/testdata/test/sensitive_input_values/main.tf index 6d61c0c24166..14e85cd4aefb 100644 --- a/internal/command/testdata/test/sensitive_input_values/main.tf +++ b/internal/command/testdata/test/sensitive_input_values/main.tf @@ -2,6 +2,11 @@ variable "password" { type = string } +resource "test_resource" "resource" { + id = "9ddca5a9" + value = var.password +} + output "password" { value = var.password sensitive = true diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index cc960fb87b66..b3ae34bcfe65 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -361,7 +361,43 @@ func TestValidateWithInvalidOverrides(t *testing.T) { } actual := output.All() - expected := ` + expected := `Initializing the backend... +Initializing modules... +- setup in setup +- test.main.setup in setup +Initializing provider plugins... +- Finding latest version of hashicorp/test... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future. + + +Warning: Incomplete lock file information for providers + +Due to your customized provider installation methods, Terraform was forced to +calculate lock file checksums locally for the following providers: + - hashicorp/test + +The current .terraform.lock.hcl file only includes checksums for linux_amd64, +so Terraform running on another platform will fail to install these +providers. + +To calculate additional checksums for another platform, run: + terraform providers lock -platform=linux_amd64 +(where linux_amd64 is the platform to generate) +Terraform has been successfully initialized! + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. + Warning: Invalid override target on main.tftest.hcl line 4, in mock_provider "test": diff --git a/internal/command/views/init.go b/internal/command/views/init.go new file mode 100644 index 000000000000..15ff7f80126f --- /dev/null +++ b/internal/command/views/init.go @@ -0,0 +1,378 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// The Init view is used for the init command. +type Init interface { + Diagnostics(diags tfdiags.Diagnostics) + Output(messageCode InitMessageCode, params ...any) + LogInitMessage(messageCode InitMessageCode, params ...any) + Log(message string, params ...any) + PrepareMessage(messageCode InitMessageCode, params ...any) string +} + +// NewInit returns Init implementation for the given ViewType. +func NewInit(vt arguments.ViewType, view *View) Init { + switch vt { + case arguments.ViewJSON: + return &InitJSON{ + view: NewJSONView(view), + } + case arguments.ViewHuman: + return &InitHuman{ + view: view, + } + default: + panic(fmt.Sprintf("unknown view type %v", vt)) + } +} + +// The InitHuman implementation renders human-readable text logs, suitable for +// a scrolling terminal. +type InitHuman struct { + view *View +} + +var _ Init = (*InitHuman)(nil) + +func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) LogInitMessage(messageCode InitMessageCode, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +// this implements log method for use by interfaces that need to log generic string messages, e.g used for logging in hook_module_install.go +func (v *InitHuman) Log(message string, params ...any) { + v.view.streams.Println(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitHuman) PrepareMessage(messageCode InitMessageCode, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return string(messageCode) + } + + if message.HumanValue == "" { + // no need to apply colorization if the message is empty + return message.HumanValue + } + + return v.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) +} + +// The InitJSON implementation renders streaming JSON logs, suitable for +// integrating with other software. +type InitJSON struct { + view *JSONView +} + +var _ Init = (*InitJSON)(nil) + +func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +func (v *InitJSON) Output(messageCode InitMessageCode, params ...any) { + // don't add empty messages to json output + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + current_timestamp := time.Now().UTC().Format(time.RFC3339) + json_data := map[string]string{ + "@level": "info", + "@message": preppedMessage, + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "init_output", + "message_code": string(messageCode), + } + + init_output, _ := json.Marshal(json_data) + v.view.view.streams.Println(string(init_output)) +} + +func (v *InitJSON) LogInitMessage(messageCode InitMessageCode, params ...any) { + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + v.view.Log(preppedMessage) +} + +// this implements log method for use by services that need to log generic string messages, e.g usage logging in hook_module_install.go +func (v *InitJSON) Log(message string, params ...any) { + v.view.Log(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitJSON) PrepareMessage(messageCode InitMessageCode, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return string(messageCode) + } + + return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) +} + +// InitMessage represents a message string in both json and human decorated text format. +type InitMessage struct { + HumanValue string + JSONValue string +} + +var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMessage{ + "copying_configuration_message": { + HumanValue: "[reset][bold]Copying configuration[reset] from %q...", + JSONValue: "Copying configuration from %q...", + }, + "output_init_empty_message": { + HumanValue: outputInitEmpty, + JSONValue: outputInitEmptyJSON, + }, + "output_init_success_message": { + HumanValue: outputInitSuccess, + JSONValue: outputInitSuccessJSON, + }, + "output_init_success_cloud_message": { + HumanValue: outputInitSuccessCloud, + JSONValue: outputInitSuccessCloudJSON, + }, + "output_init_success_cli_message": { + HumanValue: outputInitSuccessCLI, + JSONValue: outputInitSuccessCLI_JSON, + }, + "output_init_success_cli_cloud_message": { + HumanValue: outputInitSuccessCLICloud, + JSONValue: outputInitSuccessCLICloudJSON, + }, + "upgrading_modules_message": { + HumanValue: "[reset][bold]Upgrading modules...", + JSONValue: "Upgrading modules...", + }, + "initializing_modules_message": { + HumanValue: "[reset][bold]Initializing modules...", + JSONValue: "Initializing modules...", + }, + "initializing_terraform_cloud_message": { + HumanValue: "\n[reset][bold]Initializing Terraform Cloud...", + JSONValue: "Initializing Terraform Cloud...", + }, + "initializing_backend_message": { + HumanValue: "\n[reset][bold]Initializing the backend...", + JSONValue: "Initializing the backend...", + }, + "initializing_provider_plugin_message": { + HumanValue: "\n[reset][bold]Initializing provider plugins...", + JSONValue: "Initializing provider plugins...", + }, + "dependencies_lock_changes_info": { + HumanValue: dependenciesLockChangesInfo, + JSONValue: dependenciesLockChangesInfo, + }, + "lock_info": { + HumanValue: previousLockInfoHuman, + JSONValue: previousLockInfoJSON, + }, + "provider_already_installed_message": { + HumanValue: "- Using previously-installed %s v%s", + JSONValue: "%s v%s: Using previously-installed provider version", + }, + "built_in_provider_available_message": { + HumanValue: "- %s is built in to Terraform", + JSONValue: "%s is built in to Terraform", + }, + "reusing_previous_version_info": { + HumanValue: "- Reusing previous version of %s from the dependency lock file", + JSONValue: "%s: Reusing previous version from the dependency lock file", + }, + "finding_matching_version_message": { + HumanValue: "- Finding %s versions matching %q...", + JSONValue: "Finding matching versions for provider: %s, version_constraint: %q", + }, + "finding_latest_version_message": { + HumanValue: "- Finding latest version of %s...", + JSONValue: "%s: Finding latest version...", + }, + "using_provider_from_cache_dir_info": { + HumanValue: "- Using %s v%s from the shared cache directory", + JSONValue: "%s v%s: Using from the shared cache directory", + }, + "installing_provider_message": { + HumanValue: "- Installing %s v%s...", + JSONValue: "Installing provider version: %s v%s...", + }, + "key_id": { + HumanValue: ", key ID [reset][bold]%s[reset]", + JSONValue: "key_id: %s", + }, + "installed_provider_version_info": { + HumanValue: "- Installed %s v%s (%s%s)", + JSONValue: "Installed provider version: %s v%s (%s%s)", + }, + "partner_and_community_providers_message": { + HumanValue: partnerAndCommunityProvidersInfo, + JSONValue: partnerAndCommunityProvidersInfo, + }, + "init_config_error": { + HumanValue: errInitConfigError, + JSONValue: errInitConfigErrorJSON, + }, + "empty_message": { + HumanValue: "", + JSONValue: "", + }, +} + +type InitMessageCode string + +const ( + CopyingConfigurationMessage InitMessageCode = "copying_configuration_message" + EmptyMessage InitMessageCode = "empty_message" + OutputInitEmptyMessage InitMessageCode = "output_init_empty_message" + OutputInitSuccessMessage InitMessageCode = "output_init_success_message" + OutputInitSuccessCloudMessage InitMessageCode = "output_init_success_cloud_message" + OutputInitSuccessCLIMessage InitMessageCode = "output_init_success_cli_message" + OutputInitSuccessCLICloudMessage InitMessageCode = "output_init_success_cli_cloud_message" + UpgradingModulesMessage InitMessageCode = "upgrading_modules_message" + InitializingTerraformCloudMessage InitMessageCode = "initializing_terraform_cloud_message" + InitializingModulesMessage InitMessageCode = "initializing_modules_message" + InitializingBackendMessage InitMessageCode = "initializing_backend_message" + InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" + LockInfo InitMessageCode = "lock_info" + DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" + ProviderAlreadyInstalledMessage InitMessageCode = "provider_already_installed_message" + BuiltInProviderAvailableMessage InitMessageCode = "built_in_provider_available_message" + ReusingPreviousVersionInfo InitMessageCode = "reusing_previous_version_info" + FindingMatchingVersionMessage InitMessageCode = "finding_matching_version_message" + FindingLatestVersionMessage InitMessageCode = "finding_latest_version_message" + UsingProviderFromCacheDirInfo InitMessageCode = "using_provider_from_cache_dir_info" + InstallingProviderMessage InitMessageCode = "installing_provider_message" + KeyID InitMessageCode = "key_id" + InstalledProviderVersionInfo InitMessageCode = "installed_provider_version_info" + PartnerAndCommunityProvidersMessage InitMessageCode = "partner_and_community_providers_message" + InitConfigError InitMessageCode = "init_config_error" +) + +const outputInitEmpty = ` +[reset][bold]Terraform initialized in an empty directory![reset] + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitEmptyJSON = ` +Terraform initialized in an empty directory! + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitSuccess = ` +[reset][bold][green]Terraform has been successfully initialized![reset][green] +` + +const outputInitSuccessJSON = ` +Terraform has been successfully initialized! +` + +const outputInitSuccessCloud = ` +[reset][bold][green]Terraform Cloud has been successfully initialized![reset][green] +` + +const outputInitSuccessCloudJSON = ` +Terraform Cloud has been successfully initialized! +` + +const outputInitSuccessCLI = `[reset][green] +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLI_JSON = ` +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLICloud = `[reset][green] +You may now begin working with Terraform Cloud. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +const outputInitSuccessCLICloudJSON = ` +You may now begin working with Terraform Cloud. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +const previousLockInfoHuman = ` +Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const previousLockInfoJSON = ` +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const dependenciesLockChangesInfo = ` +Terraform has made some changes to the provider dependency selections recorded +in the .terraform.lock.hcl file. Review those changes and commit them to your +version control system if they represent changes you intended to make.` + +const partnerAndCommunityProvidersInfo = "\nPartner and community providers are signed by their developers.\n" + + "If you'd like to know more about provider signing, you can read about it here:\n" + + "https://www.terraform.io/docs/cli/plugins/signing.html" + +const errInitConfigError = ` +[reset]Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +` + +const errInitConfigErrorJSON = ` +Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +` diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go new file mode 100644 index 000000000000..5017d714772b --- /dev/null +++ b/internal/command/views/init_test.go @@ -0,0 +1,329 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" +) + +func TestNewInit_jsonViewDiagnostics(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "error", + "@message": "Error: Error selecting workspace", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Error selecting workspace", + "detail": "Workspace random_pet does not exist", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: Unsupported backend type", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Unsupported backend type", + "detail": "There is no explicit backend type named fake backend.", + }, + "type": "diagnostic", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_humanViewDiagnostics(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + actual := done(t).All() + expected := "\nError: Error selecting workspace\n\nWorkspace random_pet does not exist\n\nError: Unsupported backend type\n\nThere is no explicit backend type named fake backend.\n" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } +} + +func TestNewInit_unsupportedViewDiagnostics(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatalf("should panic with unsupported view type raw") + } else if r != "unknown view type raw" { + t.Fatalf("unexpected panic message: %v", r) + } + }() + + streams, done := terminal.StreamsForTesting(t) + defer done(t) + + NewInit(arguments.ViewRaw, NewView(streams).SetRunningInAutomation(true)) +} + +func getTestDiags(t *testing.T) tfdiags.Diagnostics { + t.Helper() + + var diags tfdiags.Diagnostics + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Error selecting workspace", + "Workspace random_pet does not exist", + ), + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: "There is no explicit backend type named fake backend.", + Subject: nil, + }, + ) + + return diags +} + +func TestNewInit_jsonViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.Output(InitializingProviderPluginMessage) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "message_code": "initializing_provider_plugin_message", + "@module": "terraform.ui", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + newInit.Output(FindingLatestVersionMessage, packageName) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("%s: Finding latest version...", packageName), + "@module": "terraform.ui", + "message_code": "finding_latest_version_message", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("%s v%s: Using previously-installed provider version", packageName, packageVersion), + "@module": "terraform.ui", + "message_code": "provider_already_installed_message", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) +} + +func TestNewInit_jsonViewLog(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.LogInitMessage(InitializingProviderPluginMessage) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "@module": "terraform.ui", + "type": "log", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_jsonViewPrepareMessage(t *testing.T) { + t.Run("existing message code", func(t *testing.T) { + streams, _ := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + want := "Initializing modules..." + + actual := newInit.PrepareMessage(InitializingModulesMessage) + if !cmp.Equal(want, actual) { + t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) + } + }) +} + +func TestNewInit_humanViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.Output(InitializingProviderPluginMessage) + + actual := done(t).All() + expected := "Initializing provider plugins..." + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + newInit.Output(FindingLatestVersionMessage, packageName) + + actual := done(t).All() + expected := "Finding latest version of hashicorp/aws" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) + + actual := done(t).All() + expected := "- Using previously-installed hashicorp/aws v3.0.0" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) +} diff --git a/internal/command/views/json_view_test.go b/internal/command/views/json_view_test.go index 4f84b8a6977e..ac12410f7119 100644 --- a/internal/command/views/json_view_test.go +++ b/internal/command/views/json_view_test.go @@ -410,7 +410,7 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string delete(gotStruct, "@timestamp") // Verify the timestamp format - if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil { + if _, err := time.Parse(time.RFC3339, timestamp.(string)); err != nil { t.Errorf("error parsing timestamp on line %d: %s", i, err) } } diff --git a/internal/configs/configschema/marks.go b/internal/configs/configschema/marks.go index 4a33fb1c3c8d..919549291374 100644 --- a/internal/configs/configschema/marks.go +++ b/internal/configs/configschema/marks.go @@ -6,7 +6,6 @@ package configschema import ( "fmt" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -19,27 +18,22 @@ func copyAndExtendPath(path cty.Path, nextSteps ...cty.PathStep) cty.Path { return newPath } -// ValueMarks returns a set of path value marks for a given value and path, -// based on the sensitive flag for each attribute within the schema. Nested -// blocks are descended (if present in the given value). -func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { - var pvm []cty.PathValueMarks +// SensitivePaths returns a set of paths into the given value that should +// be marked as sensitive based on the static declarations in the schema. +func (b *Block) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path // We can mark attributes as sensitive even if the value is null for name, attrS := range b.Attributes { if attrS.Sensitive { - // Create a copy of the path, with this step added, to add to our PathValueMarks slice - attrPath := copyAndExtendPath(path, cty.GetAttrStep{Name: name}) - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name}) + ret = append(ret, attrPath) } } // If the value is null, no other marks are possible if val.IsNull() { - return pvm + return ret } // Extract marks for nested attribute type values @@ -51,9 +45,8 @@ func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { } // Create a copy of the path, with this step added, to add to our PathValueMarks slice - attrPath := copyAndExtendPath(path, cty.GetAttrStep{Name: name}) - - pvm = append(pvm, attrS.NestedType.ValueMarks(val.GetAttr(name), attrPath)...) + attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name}) + ret = append(ret, attrS.NestedType.SensitivePaths(val.GetAttr(name), attrPath)...) } // Extract marks for nested blocks @@ -69,35 +62,35 @@ func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { } // Create a copy of the path, with this step added, to add to our PathValueMarks slice - blockPath := copyAndExtendPath(path, cty.GetAttrStep{Name: name}) + blockPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name}) switch blockS.Nesting { case NestingSingle, NestingGroup: - pvm = append(pvm, blockS.Block.ValueMarks(blockV, blockPath)...) + ret = append(ret, blockS.Block.SensitivePaths(blockV, blockPath)...) case NestingList, NestingMap, NestingSet: + blockV, _ = blockV.Unmark() // peel off one level of marking so we can iterate for it := blockV.ElementIterator(); it.Next(); { idx, blockEV := it.Element() // Create a copy of the path, with this block instance's index // step added, to add to our PathValueMarks slice blockInstancePath := copyAndExtendPath(blockPath, cty.IndexStep{Key: idx}) - morePaths := blockS.Block.ValueMarks(blockEV, blockInstancePath) - pvm = append(pvm, morePaths...) + morePaths := blockS.Block.SensitivePaths(blockEV, blockInstancePath) + ret = append(ret, morePaths...) } default: panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) } } - return pvm + return ret } -// ValueMarks returns a set of path value marks for a given value and path, -// based on the sensitive flag for each attribute within the nested attribute. -// Attributes with nested types are descended (if present in the given value). -func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { - var pvm []cty.PathValueMarks +// SensitivePaths returns a set of paths into the given value that should be +// marked as sensitive based on the static declarations in the schema. +func (o *Object) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path if val.IsNull() || !val.IsKnown() { - return pvm + return ret } for name, attrS := range o.Attributes { @@ -109,22 +102,20 @@ func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { switch o.Nesting { case NestingSingle, NestingGroup: // Create a path to this attribute - attrPath := copyAndExtendPath(path, cty.GetAttrStep{Name: name}) + attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name}) if attrS.Sensitive { // If the entire attribute is sensitive, mark it so - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + ret = append(ret, attrPath) } else { // The attribute has a nested type which contains sensitive // attributes, so recurse - pvm = append(pvm, attrS.NestedType.ValueMarks(val.GetAttr(name), attrPath)...) + ret = append(ret, attrS.NestedType.SensitivePaths(val.GetAttr(name), attrPath)...) } case NestingList, NestingMap, NestingSet: // For nested attribute types which have a non-single nesting mode, // we add path value marks for each element of the collection + val, _ = val.Unmark() // peel off one level of marking so we can iterate for it := val.ElementIterator(); it.Next(); { idx, attrEV := it.Element() attrV := attrEV.GetAttr(name) @@ -134,23 +125,18 @@ func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { // of the loops: index into the collection, then the contained // attribute name. This is because we have one type // representing multiple collection elements. - attrPath := copyAndExtendPath(path, cty.IndexStep{Key: idx}, cty.GetAttrStep{Name: name}) + attrPath := copyAndExtendPath(basePath, cty.IndexStep{Key: idx}, cty.GetAttrStep{Name: name}) if attrS.Sensitive { // If the entire attribute is sensitive, mark it so - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + ret = append(ret, attrPath) } else { - // The attribute has a nested type which contains sensitive - // attributes, so recurse - pvm = append(pvm, attrS.NestedType.ValueMarks(attrV, attrPath)...) + ret = append(ret, attrS.NestedType.SensitivePaths(attrV, attrPath)...) } } default: panic(fmt.Sprintf("unsupported nesting mode %s", attrS.NestedType.Nesting)) } } - return pvm + return ret } diff --git a/internal/configs/configschema/marks_test.go b/internal/configs/configschema/marks_test.go index 273794d4a0cd..9393c6c371ad 100644 --- a/internal/configs/configschema/marks_test.go +++ b/internal/configs/configschema/marks_test.go @@ -176,8 +176,9 @@ func TestBlockValueMarks(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - got := tc.given.MarkWithPaths(schema.ValueMarks(tc.given, nil)) - if !got.RawEquals(tc.expect) { + sensitivePaths := schema.SensitivePaths(tc.given, nil) + got := marks.MarkPaths(tc.given, marks.Sensitive, sensitivePaths) + if !tc.expect.RawEquals(got) { t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expect, got) } }) diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go index 8acf6ed06183..6f519c0ca9a0 100644 --- a/internal/genconfig/generate_config.go +++ b/internal/genconfig/generate_config.go @@ -38,7 +38,6 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance, buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact())) } - stateVal = omitUnknowns(stateVal) if stateVal.RawEquals(cty.NilVal) { diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2)) diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2)) @@ -151,11 +150,17 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri val = attrS.EmptyValue() } if val.Type() == cty.String { + // Before we inspect the string, take off any marks. + unmarked, marks := val.Unmark() + // SHAMELESS HACK: If we have "" for an optional value, assume // it is actually null, due to the legacy SDK. - if !val.IsNull() && attrS.Optional && len(val.AsString()) == 0 { - val = attrS.EmptyValue() + if !unmarked.IsNull() && attrS.Optional && len(unmarked.AsString()) == 0 { + unmarked = attrS.EmptyValue() } + + // Before we carry on, add the marks back. + val = unmarked.WithMarks(marks) } if attrS.Sensitive || val.IsMarked() { buf.WriteString("null # sensitive") @@ -567,59 +572,3 @@ func ctyCollectionValues(val cty.Value) []cty.Value { return ret } - -// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, -// omitting any unknowns. -// -// The result also normalizes some types: all sequence types are turned into -// tuple types and all mapping types are converted to object types, since we -// assume the result of this is just going to be serialized as JSON (and thus -// lose those distinctions) anyway. -func omitUnknowns(val cty.Value) cty.Value { - ty := val.Type() - switch { - case val.IsNull(): - return val - case !val.IsKnown(): - return cty.NilVal - case ty.IsPrimitiveType(): - return val - case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): - var vals []cty.Value - it := val.ElementIterator() - for it.Next() { - _, v := it.Element() - newVal := omitUnknowns(v) - if newVal != cty.NilVal { - vals = append(vals, newVal) - } else if newVal == cty.NilVal { - // element order is how we correlate unknownness, so we must - // replace unknowns with nulls - vals = append(vals, cty.NullVal(v.Type())) - } - } - // We use tuple types always here, because the work we did above - // may have caused the individual elements to have different types, - // and we're doing this work to produce JSON anyway and JSON marshalling - // represents all of these sequence types as an array. - return cty.TupleVal(vals) - case ty.IsMapType() || ty.IsObjectType(): - vals := make(map[string]cty.Value) - it := val.ElementIterator() - for it.Next() { - k, v := it.Element() - newVal := omitUnknowns(v) - if newVal != cty.NilVal { - vals[k.AsString()] = newVal - } - } - // We use object types always here, because the work we did above - // may have caused the individual elements to have different types, - // and we're doing this work to produce JSON anyway and JSON marshalling - // represents both of these mapping types as an object. - return cty.ObjectVal(vals) - default: - // Should never happen, since the above should cover all types - panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) - } -} diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index 88829e8bf864..8f0938438636 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" ) func TestConfigGeneration(t *testing.T) { @@ -536,6 +537,67 @@ resource "tfcoremock_simple_resource" "empty" { expected: ` resource "tfcoremock_simple_resource" "empty" { value = "[\"Hello\", \"World\"" +}`, + }, + // Just try all the simple values with sensitive marks. + "sensitive_values": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "string": sensitiveAttribute(cty.String), + "empty_string": sensitiveAttribute(cty.String), + "number": sensitiveAttribute(cty.Number), + "bool": sensitiveAttribute(cty.Bool), + "object": sensitiveAttribute(cty.Object(map[string]cty.Type{ + "nested": cty.String, + })), + "list": sensitiveAttribute(cty.List(cty.String)), + "map": sensitiveAttribute(cty.Map(cty.String)), + "set": sensitiveAttribute(cty.Set(cty.String)), + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_sensitive_values", + Name: "values", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + // Values that are sensitive will now be marked as such + "string": cty.StringVal("Hello, world!").Mark(marks.Sensitive), + "empty_string": cty.StringVal("").Mark(marks.Sensitive), + "number": cty.NumberIntVal(42).Mark(marks.Sensitive), + "bool": cty.True.Mark(marks.Sensitive), + "object": cty.ObjectVal(map[string]cty.Value{ + "nested": cty.StringVal("Hello, solar system!"), + }).Mark(marks.Sensitive), + "list": cty.ListVal([]cty.Value{ + cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + "map": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + "set": cty.SetVal([]cty.Value{ + cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + }), + expected: ` +resource "tfcoremock_sensitive_values" "values" { + bool = null # sensitive + empty_string = null # sensitive + list = null # sensitive + map = null # sensitive + number = null # sensitive + object = null # sensitive + set = null # sensitive + string = null # sensitive }`, }, } @@ -558,3 +620,11 @@ resource "tfcoremock_simple_resource" "empty" { }) } } + +func sensitiveAttribute(t cty.Type) *configschema.Attribute { + return &configschema.Attribute{ + Type: t, + Optional: true, + Sensitive: true, + } +} diff --git a/internal/lang/marks/paths.go b/internal/lang/marks/paths.go new file mode 100644 index 000000000000..461ec1bb42d2 --- /dev/null +++ b/internal/lang/marks/paths.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package marks + +import ( + "github.com/zclconf/go-cty/cty" +) + +// PathsWithMark produces a list of paths identified as having a specified +// mark in a given set of [cty.PathValueMarks] that presumably resulted from +// deeply-unmarking a [cty.Value]. +// +// This is for situations where a subsystem needs to give special treatment +// to one specific mark value, as opposed to just handling all marks +// generically as cty operations would. The second return value is a +// subset of the given [cty.PathValueMarks] values which contained marks +// other than the one requested, so that a caller that can't preserve other +// marks at all can more easily return an error explaining that. +func PathsWithMark(pvms []cty.PathValueMarks, wantMark any) (withWanted []cty.Path, withOthers []cty.PathValueMarks) { + if len(pvms) == 0 { + // No-allocations path for the common case where there are no marks at all. + return nil, nil + } + + for _, pvm := range pvms { + if _, ok := pvm.Marks[wantMark]; ok { + withWanted = append(withWanted, pvm.Path) + } + for mark := range pvm.Marks { + if mark != wantMark { + withOthers = append(withOthers, pvm) + } + } + } + + return withWanted, withOthers +} + +// MarkPaths transforms the given value by marking each of the given paths +// with the given mark value. +func MarkPaths(val cty.Value, mark any, paths []cty.Path) cty.Value { + if len(paths) == 0 { + // No-allocations path for the common case where there are no marked paths at all. + return val + } + + // For now we'll use cty's slightly lower-level function to achieve this + // result. This is a little inefficient due to an additional dynamic + // allocation for the intermediate data structure, so if that becomes + // a problem in practice then we may wish to write a more direct + // implementation here. + markses := make([]cty.PathValueMarks, len(paths)) + marks := cty.NewValueMarks(mark) + for i, path := range paths { + markses[i] = cty.PathValueMarks{ + Path: path, + Marks: marks, + } + } + return val.MarkWithPaths(markses) +} diff --git a/internal/lang/marks/paths_test.go b/internal/lang/marks/paths_test.go new file mode 100644 index 000000000000..f5efd2829f95 --- /dev/null +++ b/internal/lang/marks/paths_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package marks + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestPathsWithMark(t *testing.T) { + input := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("sensitive"), + Marks: cty.NewValueMarks(Sensitive), + }, + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks(Sensitive, "other"), + }, + } + + gotPaths, gotOthers := PathsWithMark(input, Sensitive) + wantPaths := []cty.Path{ + cty.GetAttrPath("sensitive"), + cty.GetAttrPath("both"), + } + wantOthers := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks(Sensitive, "other"), + // Note that this intentionally preserves the fact that the + // attribute was both sensitive _and_ had another mark, since + // that gives the caller the most possible information to + // potentially handle this combination in a special way in + // an error message, or whatever. It also conveniently avoids + // allocating a new mark set, which is nice. + }, + } + + if diff := cmp.Diff(wantPaths, gotPaths, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong matched paths\n%s", diff) + } + if diff := cmp.Diff(wantOthers, gotOthers, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong set of entries with other marks\n%s", diff) + } +} + +func TestMarkPaths(t *testing.T) { + value := cty.ObjectVal(map[string]cty.Value{ + "s": cty.StringVal(".s"), + "l": cty.ListVal([]cty.Value{ + cty.StringVal(".l[0]"), + cty.StringVal(".l[1]"), + }), + "m": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal(`.m["a"]`), + "b": cty.StringVal(`.m["b"]`), + }), + "o": cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal(".o.a"), + "b": cty.StringVal(".o.b"), + }), + "t": cty.TupleVal([]cty.Value{ + cty.StringVal(`.t[0]`), + cty.StringVal(`.t[1]`), + }), + }) + sensitivePaths := []cty.Path{ + cty.GetAttrPath("s"), + cty.GetAttrPath("l").IndexInt(1), + cty.GetAttrPath("m").IndexString("a"), + cty.GetAttrPath("o").GetAttr("b"), + cty.GetAttrPath("t").IndexInt(0), + } + got := MarkPaths(value, Sensitive, sensitivePaths) + want := cty.ObjectVal(map[string]cty.Value{ + "s": cty.StringVal(".s").Mark(Sensitive), + "l": cty.ListVal([]cty.Value{ + cty.StringVal(".l[0]"), + cty.StringVal(".l[1]").Mark(Sensitive), + }), + "m": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal(`.m["a"]`).Mark(Sensitive), + "b": cty.StringVal(`.m["b"]`), + }), + "o": cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal(".o.a"), + "b": cty.StringVal(".o.b").Mark(Sensitive), + }), + "t": cty.TupleVal([]cty.Value{ + cty.StringVal(`.t[0]`).Mark(Sensitive), + cty.StringVal(`.t[1]`), + }), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} diff --git a/internal/legacy/go.sum b/internal/legacy/go.sum index e715f2ac95b8..7123bc1a1d75 100644 --- a/internal/legacy/go.sum +++ b/internal/legacy/go.sum @@ -233,8 +233,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be h1:JoF5rruXECi+VEbdJ6xanklyLnz+TVCZ0FcmvSQq/Go= -github.com/zclconf/go-cty-debug v0.0.0-20240209213017-b8d9e32151be/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a h1:/o/Emn22dZIQ7AhyA0aLOKo528WG/WRAM5tqzIoQIOs= +github.com/zclconf/go-cty-debug v0.0.0-20240417160409-8c45e122ae1a/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/plans/changes.go b/internal/plans/changes.go index db75e20e18b3..3864d7fa807a 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -4,10 +4,14 @@ package plans import ( + "fmt" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Changes describes various actions that Terraform will attempt to take if @@ -555,23 +559,35 @@ type Change struct { // to call the corresponding Encode method of that struct rather than working // directly with its embedded Change. func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) { - // Storing unmarked values so that we can encode unmarked values - // and save the PathValueMarks for re-marking the values later - var beforeVM, afterVM []cty.PathValueMarks - unmarkedBefore := c.Before - unmarkedAfter := c.After - - if c.Before.ContainsMarked() { - unmarkedBefore, beforeVM = c.Before.UnmarkDeepWithPaths() + // We can't serialize value marks directly so we'll need to extract the + // sensitive marks and store them in a separate field. + // + // We don't accept any other marks here. The caller should have dealt + // with those somehow and replaced them with unmarked placeholders before + // writing the value into the state. + unmarkedBefore, marksesBefore := c.Before.UnmarkDeepWithPaths() + unmarkedAfter, marksesAfter := c.After.UnmarkDeepWithPaths() + sensitiveAttrsBefore, unsupportedMarksesBefore := marks.PathsWithMark(marksesBefore, marks.Sensitive) + sensitiveAttrsAfter, unsupportedMarksesAfter := marks.PathsWithMark(marksesAfter, marks.Sensitive) + if len(unsupportedMarksesBefore) != 0 { + return nil, fmt.Errorf( + "prior value %s: can't serialize value marked with %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(unsupportedMarksesBefore[0].Path), + unsupportedMarksesBefore[0].Marks, + ) + } + if len(unsupportedMarksesAfter) != 0 { + return nil, fmt.Errorf( + "new value %s: can't serialize value marked with %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(unsupportedMarksesAfter[0].Path), + unsupportedMarksesAfter[0].Marks, + ) } + beforeDV, err := NewDynamicValue(unmarkedBefore, ty) if err != nil { return nil, err } - - if c.After.ContainsMarked() { - unmarkedAfter, afterVM = c.After.UnmarkDeepWithPaths() - } afterDV, err := NewDynamicValue(unmarkedAfter, ty) if err != nil { return nil, err @@ -583,12 +599,12 @@ func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) { } return &ChangeSrc{ - Action: c.Action, - Before: beforeDV, - After: afterDV, - BeforeValMarks: beforeVM, - AfterValMarks: afterVM, - Importing: importing, - GeneratedConfig: c.GeneratedConfig, + Action: c.Action, + Before: beforeDV, + After: afterDV, + BeforeSensitivePaths: sensitiveAttrsBefore, + AfterSensitivePaths: sensitiveAttrsAfter, + Importing: importing, + GeneratedConfig: c.GeneratedConfig, }, nil } diff --git a/internal/plans/changes_src.go b/internal/plans/changes_src.go index a7588e7f78a9..90670d3b39f0 100644 --- a/internal/plans/changes_src.go +++ b/internal/plans/changes_src.go @@ -6,9 +6,11 @@ package plans import ( "fmt" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) // ResourceInstanceChangeSrc is a not-yet-decoded ResourceInstanceChange. @@ -217,12 +219,13 @@ type ChangeSrc struct { // storage. Before, After DynamicValue - // BeforeValMarks and AfterValMarks are stored path+mark combinations - // that might be discovered when encoding a change. Marks are removed - // to enable encoding (marked values cannot be marshalled), and so storing - // the path+mark combinations allow us to re-mark the value later - // when, for example, displaying the diff to the UI. - BeforeValMarks, AfterValMarks []cty.PathValueMarks + // BeforeSensitivePaths and AfterSensitivePaths are the paths for any + // values in Before or After (respectively) that are considered to be + // sensitive. The sensitive marks are removed from the in-memory values + // to enable encoding (marked values cannot be marshalled), and so we + // store the sensitive paths to allow re-marking later when we decode + // the serialized change. + BeforeSensitivePaths, AfterSensitivePaths []cty.Path // Importing is present if the resource is being imported as part of this // change. @@ -270,8 +273,8 @@ func (cs *ChangeSrc) Decode(ty cty.Type) (*Change, error) { return &Change{ Action: cs.Action, - Before: before.MarkWithPaths(cs.BeforeValMarks), - After: after.MarkWithPaths(cs.AfterValMarks), + Before: marks.MarkPaths(before, marks.Sensitive, cs.BeforeSensitivePaths), + After: marks.MarkPaths(after, marks.Sensitive, cs.AfterSensitivePaths), Importing: importing, GeneratedConfig: cs.GeneratedConfig, }, nil diff --git a/internal/plans/changes_test.go b/internal/plans/changes_test.go index afea79fcf17f..5321bff88f4f 100644 --- a/internal/plans/changes_test.go +++ b/internal/plans/changes_test.go @@ -133,8 +133,8 @@ func TestChangeEncodeSensitive(t *testing.T) { cty.ObjectVal(map[string]cty.Value{ "ding": cty.StringVal("dong").Mark(marks.Sensitive), }), - cty.StringVal("bleep").Mark("bloop"), - cty.ListVal([]cty.Value{cty.UnknownVal(cty.String).Mark("sup?")}), + cty.StringVal("bleep").Mark(marks.Sensitive), + cty.ListVal([]cty.Value{cty.UnknownVal(cty.String).Mark(marks.Sensitive)}), } for _, v := range testVals { diff --git a/internal/plans/planfile/config_snapshot.go b/internal/plans/planfile/config_snapshot.go index 24edb101ee81..7dda3bc5f566 100644 --- a/internal/plans/planfile/config_snapshot.go +++ b/internal/plans/planfile/config_snapshot.go @@ -7,7 +7,7 @@ import ( "archive/zip" "encoding/json" "fmt" - "io/ioutil" + "io" "path" "sort" "strings" @@ -55,7 +55,7 @@ func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { if err != nil { return nil, fmt.Errorf("failed to open module manifest: %s", r) } - manifestSrc, err = ioutil.ReadAll(r) + manifestSrc, err = io.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read module manifest: %s", r) } @@ -77,7 +77,7 @@ func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { if err != nil { return nil, fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err) } - fileSrc, err := ioutil.ReadAll(r) + fileSrc, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err) } diff --git a/internal/plans/planfile/reader.go b/internal/plans/planfile/reader.go index 0c716ac5040c..f5aad59d114b 100644 --- a/internal/plans/planfile/reader.go +++ b/internal/plans/planfile/reader.go @@ -7,7 +7,8 @@ import ( "archive/zip" "bytes" "fmt" - "io/ioutil" + "io" + "os" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" @@ -59,7 +60,7 @@ func Open(filename string) (*Reader, error) { if err != nil { // To give a better error message, we'll sniff to see if this looks // like our old plan format from versions prior to 0.12. - if b, sErr := ioutil.ReadFile(filename); sErr == nil { + if b, sErr := os.ReadFile(filename); sErr == nil { if bytes.HasPrefix(b, []byte("tfplan")) { return nil, errUnusable(fmt.Errorf("the given plan file was created by an earlier version of Terraform; plan files cannot be shared between different Terraform versions")) } @@ -236,7 +237,7 @@ func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) { )) return nil, diags } - src, err := ioutil.ReadAll(r) + src, err := io.ReadAll(r) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index ca792ab6252e..290cd10baf89 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -6,7 +6,6 @@ package planfile import ( "fmt" "io" - "io/ioutil" "time" "github.com/zclconf/go-cty/cty" @@ -15,7 +14,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/lang/globalref" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planproto" "github.com/hashicorp/terraform/internal/providers" @@ -38,7 +36,7 @@ const tfplanFilename = "tfplan" // a plan file, which is stored in a special file in the archive called // "tfplan". func readTfplan(r io.Reader) (*plans.Plan, error) { - src, err := ioutil.ReadAll(r) + src, err := io.ReadAll(r) if err != nil { return nil, err } @@ -416,20 +414,19 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { } ret.GeneratedConfig = rawChange.GeneratedConfig - sensitive := cty.NewValueMarks(marks.Sensitive) - beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive) + beforeValSensitiveAttrs, err := pathsFromTfplan(rawChange.BeforeSensitivePaths) if err != nil { return nil, fmt.Errorf("failed to decode before sensitive paths: %s", err) } - afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive) + afterValSensitiveAttrs, err := pathsFromTfplan(rawChange.AfterSensitivePaths) if err != nil { return nil, fmt.Errorf("failed to decode after sensitive paths: %s", err) } - if len(beforeValMarks) > 0 { - ret.BeforeValMarks = beforeValMarks + if len(beforeValSensitiveAttrs) > 0 { + ret.BeforeSensitivePaths = beforeValSensitiveAttrs } - if len(afterValMarks) > 0 { - ret.AfterValMarks = afterValMarks + if len(afterValSensitiveAttrs) > 0 { + ret.AfterSensitivePaths = afterValSensitiveAttrs } return ret, nil @@ -788,11 +785,11 @@ func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { before := valueToTfplan(change.Before) after := valueToTfplan(change.After) - beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks) + beforeSensitivePaths, err := pathsToTfplan(change.BeforeSensitivePaths) if err != nil { return nil, err } - afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks) + afterSensitivePaths, err := pathsToTfplan(change.AfterSensitivePaths) if err != nil { return nil, err } @@ -842,25 +839,28 @@ func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue { return planproto.NewPlanDynamicValue(val) } -func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) { - ret := make([]cty.PathValueMarks, 0, len(paths)) +func pathsFromTfplan(paths []*planproto.Path) ([]cty.Path, error) { + if len(paths) == 0 { + return nil, nil + } + ret := make([]cty.Path, 0, len(paths)) for _, p := range paths { path, err := pathFromTfplan(p) if err != nil { return nil, err } - ret = append(ret, cty.PathValueMarks{ - Path: path, - Marks: marks, - }) + ret = append(ret, path) } return ret, nil } -func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) { - ret := make([]*planproto.Path, 0, len(pvm)) - for _, p := range pvm { - path, err := pathToTfplan(p.Path) +func pathsToTfplan(paths []cty.Path) ([]*planproto.Path, error) { + if len(paths) == 0 { + return nil, nil + } + ret := make([]*planproto.Path, 0, len(paths)) + for _, p := range paths { + path, err := pathToTfplan(p) if err != nil { return nil, err } diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 2a6cd1256d13..73bad353a79c 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/lang/globalref" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" @@ -90,11 +89,8 @@ func TestTFPlanRoundTrip(t *testing.T) { cty.StringVal("honk"), }), }), objTy), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("boop").IndexInt(1), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("boop").IndexInt(1), }, }, RequiredReplace: cty.NewPathSet( @@ -185,11 +181,8 @@ func TestTFPlanRoundTrip(t *testing.T) { cty.StringVal("bonk"), }), }), objTy), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("boop").IndexInt(1), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("boop").IndexInt(1), }, }, }, diff --git a/internal/rpcapi/stacks_inspector.go b/internal/rpcapi/stacks_inspector.go index 4969b1cd43fc..1ff6fe16ad19 100644 --- a/internal/rpcapi/stacks_inspector.go +++ b/internal/rpcapi/stacks_inspector.go @@ -14,6 +14,7 @@ import ( "google.golang.org/grpc/status" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" @@ -74,6 +75,16 @@ func (i *stacksInspector) InspectExpressionResult(ctx context.Context, req *terr } val, markses := val.UnmarkDeepWithPaths() + sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive) + if len(otherMarkses) != 0 { + // Any other marks should've been dealt with by the stacks runtime + // before getting here, since we only know how to preserve the sensitive + // marking. + return nil, fmt.Errorf( + "%s: unhandled value marks %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarkses[0].Path), otherMarkses[0].Marks, + ) + } valRaw, err := plans.NewDynamicValue(val, cty.DynamicPseudoType) if err != nil { // We might get here if the result was of a type we cannot send @@ -89,7 +100,7 @@ func (i *stacksInspector) InspectExpressionResult(ctx context.Context, req *terr } return &terraform1.InspectExpressionResult_Response{ - Result: terraform1.NewDynamicValue(valRaw, markses), + Result: terraform1.NewDynamicValue(valRaw, sensitivePaths), Diagnostics: diagnosticsToProto(diags), }, nil } diff --git a/internal/rpcapi/terraform1/conversion.go b/internal/rpcapi/terraform1/conversion.go index 7e8d0c4e7d1f..2459dc31a736 100644 --- a/internal/rpcapi/terraform1/conversion.go +++ b/internal/rpcapi/terraform1/conversion.go @@ -9,7 +9,6 @@ import ( "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" ) @@ -44,11 +43,10 @@ func ChangeTypesForPlanAction(action plans.Action) ([]ChangeType, error) { // [plans.DynamicValue], which is Terraform Core's typical in-memory // representation of an already-serialized dynamic value. // -// The plans package represents value marks (including "sensitive") as a -// separate field in [plans.ChangeSrc] rather than as part of the value -// itself, so callers must also provide that separate [cty.PathValueMarks] -// value if encoding a value that might have sensitive elements. -func NewDynamicValue(from plans.DynamicValue, markses []cty.PathValueMarks) *DynamicValue { +// The plans package represents the sensitive value mark as a separate field +// in [plans.ChangeSrc] rather than as part of the value itself, so callers must +// also provide a separate set of paths that are marked as sensitive. +func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path) *DynamicValue { // plans.DynamicValue is always MessagePack-serialized today, so we'll // just write its bytes into the field for msgpack serialization // unconditionally. If plans.DynamicValue grows to support different @@ -57,12 +55,10 @@ func NewDynamicValue(from plans.DynamicValue, markses []cty.PathValueMarks) *Dyn Msgpack: []byte(from), } - if len(markses) != 0 { - ret.Sensitive = make([]*AttributePath, 0, len(markses)) - for _, pathMarks := range markses { - if _, exists := pathMarks.Marks[marks.Sensitive]; exists { - ret.Sensitive = append(ret.Sensitive, NewAttributePath(pathMarks.Path)) - } + if len(sensitivePaths) != 0 { + ret.Sensitive = make([]*AttributePath, 0, len(sensitivePaths)) + for _, path := range sensitivePaths { + ret.Sensitive = append(ret.Sensitive, NewAttributePath(path)) } } diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index 0c24b19fbbac..db48a25a3b1b 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -340,8 +340,14 @@ func (pc *PlannedChangeResourceInstancePlanned) PlannedChangeProto() (*terraform Actions: protoChangeTypes, Values: &terraform1.DynamicValueChange{ - Old: terraform1.NewDynamicValue(pc.ChangeSrc.Before, pc.ChangeSrc.BeforeValMarks), - New: terraform1.NewDynamicValue(pc.ChangeSrc.After, pc.ChangeSrc.AfterValMarks), + Old: terraform1.NewDynamicValue( + pc.ChangeSrc.Before, + pc.ChangeSrc.BeforeSensitivePaths, + ), + New: terraform1.NewDynamicValue( + pc.ChangeSrc.After, + pc.ChangeSrc.AfterSensitivePaths, + ), }, ReplacePaths: replacePaths, // TODO: Moved, Imported @@ -380,8 +386,8 @@ type PlannedChangeOutputValue struct { Addr stackaddrs.OutputValue // Covers only root stack output values Action plans.Action - OldValue, NewValue plans.DynamicValue - OldValueMarks, NewValueMarks []cty.PathValueMarks + OldValue, NewValue plans.DynamicValue + OldValueSensitivePaths, NewValueSensitivePaths []cty.Path } var _ PlannedChange = (*PlannedChangeOutputValue)(nil) @@ -405,8 +411,8 @@ func (pc *PlannedChangeOutputValue) PlannedChangeProto() (*terraform1.PlannedCha Actions: protoChangeTypes, Values: &terraform1.DynamicValueChange{ - Old: terraform1.NewDynamicValue(pc.OldValue, pc.OldValueMarks), - New: terraform1.NewDynamicValue(pc.NewValue, pc.NewValueMarks), + Old: terraform1.NewDynamicValue(pc.OldValue, pc.OldValueSensitivePaths), + New: terraform1.NewDynamicValue(pc.NewValue, pc.NewValueSensitivePaths), }, }, }, diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index 96d594c0b2e8..b0f6f58c00f1 100644 --- a/internal/stacks/stackruntime/apply_test.go +++ b/internal/stacks/stackruntime/apply_test.go @@ -317,11 +317,8 @@ func TestApplyWithSensitivePropagation(t *testing.T) { "id": "bb5cf32312ec", "value": "secret", }), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), }, Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go index 08160d4c46da..ea0c53bd722b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig" @@ -564,6 +565,20 @@ func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfd // produce accurate change actions. v, markses := v.UnmarkDeepWithPaths() + sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive) + if len(otherMarkses) != 0 { + // Any other marks should've been dealt with by our caller before + // getting here, since we only know how to preserve the sensitive + // marking. + var diags tfdiags.Diagnostics + diags = diags.Append(fmt.Errorf( + "%s%s: unhandled value marks %#v (this is a bug in Terraform)", + outputAddr, + tfdiags.FormatCtyPath(otherMarkses[0].Path), + otherMarkses[0].Marks, + )) + return nil, diags + } dv, err := plans.NewDynamicValue(v, v.Type()) if err != nil { // Should not be possible since we generated the value internally; @@ -581,11 +596,11 @@ func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfd Addr: outputAddr, Action: plans.Create, - OldValue: oldDV, - OldValueMarks: nil, + OldValue: oldDV, + OldValueSensitivePaths: nil, - NewValue: dv, - NewValueMarks: markses, + NewValue: dv, + NewValueSensitivePaths: sensitivePaths, }) } return changes, nil diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index a696c96c3c6c..f7179a49b106 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -650,11 +650,13 @@ func TestPlanSensitiveOutput(t *testing.T) { TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ - Addr: stackaddrs.OutputValue{Name: "result"}, - Action: plans.Create, - OldValue: plans.DynamicValue{0xc0}, // MessagePack nil - NewValue: mustPlanDynamicValue(cty.StringVal("secret")), - NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("secret")), + NewValueSensitivePaths: []cty.Path{ + nil, // the whole value is sensitive + }, }, } sort.SliceStable(gotChanges, func(i, j int) bool { @@ -701,11 +703,13 @@ func TestPlanSensitiveOutputNested(t *testing.T) { TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ - Addr: stackaddrs.OutputValue{Name: "result"}, - Action: plans.Create, - OldValue: plans.DynamicValue{0xc0}, // MessagePack nil - NewValue: mustPlanDynamicValue(cty.StringVal("secret")), - NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("secret")), + NewValueSensitivePaths: []cty.Path{ + nil, // the whole value is sensitive + }, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( @@ -795,11 +799,13 @@ func TestPlanSensitiveOutputAsInput(t *testing.T) { TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ - Addr: stackaddrs.OutputValue{Name: "result"}, - Action: plans.Create, - OldValue: plans.DynamicValue{0xc0}, // MessagePack nil - NewValue: mustPlanDynamicValue(cty.StringVal("SECRET")), - NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("SECRET")), + NewValueSensitivePaths: []cty.Path{ + nil, // the whole value is sensitive + }, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( @@ -1128,11 +1134,8 @@ func TestPlanWithSensitivePropagation(t *testing.T) { "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), }), stacks_testing_provider.TestingResourceSchema), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), }, }, }, @@ -1278,11 +1281,8 @@ func TestPlanWithSensitivePropagationNested(t *testing.T) { "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), }), stacks_testing_provider.TestingResourceSchema), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), }, }, }, diff --git a/internal/stacks/stackstate/applied_change.go b/internal/stacks/stackstate/applied_change.go index 94d2cb4d9bc9..7bc19e7e8a65 100644 --- a/internal/stacks/stackstate/applied_change.go +++ b/internal/stacks/stackstate/applied_change.go @@ -6,9 +6,14 @@ package stackstate import ( "fmt" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" @@ -16,9 +21,7 @@ import ( "github.com/hashicorp/terraform/internal/stacks/stackutils" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/terraform/internal/tfdiags" ) // AppliedChange represents a single isolated change, emitted as @@ -125,7 +128,17 @@ func (ac *AppliedChangeResourceInstanceObject) protosForObject() ([]*terraform1. // Separate out sensitive marks from the decoded value so we can re-serialize it // with MessagePack. Sensitive paths get encoded separately in the final message. - unmarkedValue, sensitivePaths := obj.Value.UnmarkDeepWithPaths() + unmarkedValue, markses := obj.Value.UnmarkDeepWithPaths() + sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive) + if len(otherMarkses) != 0 { + // Any other marks should've been dealt with by our caller before + // getting here, since we only know how to preserve the sensitive + // marking. + return nil, nil, fmt.Errorf( + "%s: unhandled value marks %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarkses[0].Path), otherMarkses[0].Marks, + ) + } encValue, err := plans.NewDynamicValue(unmarkedValue, ty) if err != nil { return nil, nil, fmt.Errorf("cannot encode new state for %s in preparation for saving it: %w", addr, err) @@ -205,7 +218,17 @@ func (ac *AppliedChangeComponentInstance) AppliedChangeProto() (*terraform1.Appl outputDescs := make(map[string]*terraform1.DynamicValue, len(ac.OutputValues)) for addr, val := range ac.OutputValues { - unmarkedValue, sensitivePaths := val.UnmarkDeepWithPaths() + unmarkedValue, markses := val.UnmarkDeepWithPaths() + sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive) + if len(otherMarkses) != 0 { + // Any other marks should've been dealt with by our caller before + // getting here, since we only know how to preserve the sensitive + // marking. + return nil, fmt.Errorf( + "%s: unhandled value marks %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarkses[0].Path), otherMarkses[0].Marks, + ) + } encValue, err := plans.NewDynamicValue(unmarkedValue, cty.DynamicPseudoType) if err != nil { return nil, fmt.Errorf("encoding new state for %s in %s in preparation for saving it: %w", addr, ac.ComponentInstanceAddr, err) diff --git a/internal/stacks/stackstate/applied_change_test.go b/internal/stacks/stackstate/applied_change_test.go index a086cf4cea55..3bdca09cc0c5 100644 --- a/internal/stacks/stackstate/applied_change_test.go +++ b/internal/stacks/stackstate/applied_change_test.go @@ -9,11 +9,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/marks" - "github.com/hashicorp/terraform/internal/plans/planproto" - "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" - "github.com/hashicorp/terraform/internal/states" "github.com/zclconf/go-cty/cty" ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" "google.golang.org/protobuf/proto" @@ -21,8 +16,12 @@ import ( "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans/planproto" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" ) func TestAppliedChangeAsProto(t *testing.T) { @@ -69,11 +68,8 @@ func TestAppliedChangeAsProto(t *testing.T) { NewStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: []cty.PathStep{cty.GetAttrStep{Name: "secret"}}, - Marks: map[interface{}]struct{}{marks.Sensitive: {}}, - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("secret"), }, }, }, diff --git a/internal/stacks/stackstate/from_proto.go b/internal/stacks/stackstate/from_proto.go index 2274e3000bd7..ac3c36ae0c95 100644 --- a/internal/stacks/stackstate/from_proto.go +++ b/internal/stacks/stackstate/from_proto.go @@ -13,7 +13,6 @@ import ( "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" @@ -232,17 +231,13 @@ func DecodeProtoResourceInstanceObject(protoObj *tfstackdata1.StateResourceInsta return nil, fmt.Errorf("unsupported status %s", protoObj.Status.String()) } - paths := make([]cty.PathValueMarks, 0, len(protoObj.SensitivePaths)) - marks := cty.NewValueMarks(marks.Sensitive) + paths := make([]cty.Path, 0, len(protoObj.SensitivePaths)) for _, p := range protoObj.SensitivePaths { path, err := planfile.PathFromProto(p) if err != nil { return nil, err } - paths = append(paths, cty.PathValueMarks{ - Path: path, - Marks: marks, - }) + paths = append(paths, path) } objSrc.AttrSensitivePaths = paths diff --git a/internal/stacks/tfstackdata1/convert.go b/internal/stacks/tfstackdata1/convert.go index 7f7232a9517d..7bb56a814e17 100644 --- a/internal/stacks/tfstackdata1/convert.go +++ b/internal/stacks/tfstackdata1/convert.go @@ -71,6 +71,13 @@ func ComponentInstanceResultsToTFStackData1(outputValues map[addrs.OutputValue]c func DynamicValueToTFStackData1(val cty.Value, ty cty.Type) (*DynamicValue, error) { unmarkedVal, markPaths := val.UnmarkDeepWithPaths() + sensitivePaths, withOtherMarks := marks.PathsWithMark(markPaths, marks.Sensitive) + if len(withOtherMarks) != 0 { + return nil, withOtherMarks[0].Path.NewErrorf( + "can't serialize value marked with %#v (this is a bug in Terraform)", + withOtherMarks[0].Marks, + ) + } rawVal, err := msgpack.Marshal(unmarkedVal, ty) if err != nil { @@ -86,16 +93,12 @@ func DynamicValueToTFStackData1(val cty.Value, ty cty.Type) (*DynamicValue, erro } ret.SensitivePaths = make([]*planproto.Path, 0, len(markPaths)) - for _, pathMarks := range markPaths { - if _, isSensitive := pathMarks.Marks[marks.Sensitive]; !isSensitive { - // Some other kind of mark we don't know how to handle, then. - continue - } - path, err := planproto.NewPath(pathMarks.Path) + for _, path := range sensitivePaths { + protoPath, err := planproto.NewPath(path) if err != nil { - return nil, pathMarks.Path.NewErrorf("failed to encode path: %w", err) + return nil, path.NewErrorf("failed to encode path: %w", err) } - ret.SensitivePaths = append(ret.SensitivePaths, path) + ret.SensitivePaths = append(ret.SensitivePaths, protoPath) } return ret, nil } diff --git a/internal/states/instance_object.go b/internal/states/instance_object.go index 0e12f6550e8e..aa8335821b2d 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -4,12 +4,15 @@ package states import ( + "fmt" "sort" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" ) // ResourceInstanceObject is the local representation of a specific remote @@ -95,7 +98,10 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res // If it contains marks, remove these marks before traversing the // structure with UnknownAsNull, and save the PathValueMarks // so we can save them in state. - val, pvm := o.Value.UnmarkDeepWithPaths() + val, sensitivePaths, err := unmarkValueForStorage(o.Value) + if err != nil { + return nil, err + } // Our state serialization can't represent unknown values, so we convert // them to nulls here. This is lossy, but nobody should be writing unknown @@ -128,7 +134,7 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res return &ResourceInstanceObjectSrc{ SchemaVersion: schemaVersion, AttrsJSON: src, - AttrSensitivePaths: pvm, + AttrSensitivePaths: sensitivePaths, Private: o.Private, Status: o.Status, Dependencies: dependencies, @@ -149,3 +155,24 @@ func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { ret.Status = ObjectTainted return ret } + +// unmarkValueForStorage takes a value that possibly contains marked values +// and returns an equal value without markings along with the separated mark +// metadata that should be stored alongside the value in another field. +// +// This function only accepts the marks that are valid to store, and so will +// return an error if other marks are present. Marks that this package doesn't +// know how to store must be dealt with somehow by a caller -- presumably by +// replacing each marked value with some sort of storage placeholder -- before +// writing a value into the state. +func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { + val, pvms := v.UnmarkDeepWithPaths() + sensitivePaths, withOtherMarks := marks.PathsWithMark(pvms, marks.Sensitive) + if len(withOtherMarks) != 0 { + return cty.NilVal, nil, fmt.Errorf( + "%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)", + tfdiags.FormatCtyPath(withOtherMarks[0].Path), withOtherMarks[0].Marks, + ) + } + return val, sensitivePaths, nil +} diff --git a/internal/states/instance_object_src.go b/internal/states/instance_object_src.go index 442b6084ed82..3bf4317c8344 100644 --- a/internal/states/instance_object_src.go +++ b/internal/states/instance_object_src.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/lang/marks" ) // ResourceInstanceObjectSrc is a not-fully-decoded version of @@ -54,7 +55,7 @@ type ResourceInstanceObjectSrc struct { // AttrSensitivePaths is an array of paths to mark as sensitive coming out of // state, or to save as sensitive paths when saving state - AttrSensitivePaths []cty.PathValueMarks + AttrSensitivePaths []cty.Path // These fields all correspond to the fields of the same name on // ResourceInstanceObject. @@ -85,10 +86,7 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec } } else { val, err = ctyjson.Unmarshal(os.AttrsJSON, ty) - // Mark the value with paths if applicable - if os.AttrSensitivePaths != nil { - val = val.MarkWithPaths(os.AttrSensitivePaths) - } + val = marks.MarkPaths(val, marks.Sensitive, os.AttrSensitivePaths) if err != nil { return nil, err } diff --git a/internal/states/instance_object_test.go b/internal/states/instance_object_test.go index c5b893dade82..5d9c68ac4fe2 100644 --- a/internal/states/instance_object_test.go +++ b/internal/states/instance_object_test.go @@ -84,3 +84,27 @@ func TestResourceInstanceObject_encode(t *testing.T) { } } } + +func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) { + value := cty.ObjectVal(map[string]cty.Value{ + // State only supports a subset of marks that we know how to persist + // between plan/apply rounds. All values with other marks must be + // replaced with unmarked placeholders before attempting to store the + // value in the state. + "foo": cty.True.Mark("unsupported"), + }) + + obj := &ResourceInstanceObject{ + Value: value, + Status: ObjectReady, + } + _, err := obj.Encode(value.Type(), 0) + if err == nil { + t.Fatalf("unexpected success; want error") + } + got := err.Error() + want := `.foo: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } +} diff --git a/internal/states/state_deepcopy.go b/internal/states/state_deepcopy.go index 5bd88f77772a..b5bfccabc795 100644 --- a/internal/states/state_deepcopy.go +++ b/internal/states/state_deepcopy.go @@ -142,9 +142,9 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc { copy(attrsJSON, os.AttrsJSON) } - var attrPaths []cty.PathValueMarks + var attrPaths []cty.Path if os.AttrSensitivePaths != nil { - attrPaths = make([]cty.PathValueMarks, len(os.AttrSensitivePaths)) + attrPaths = make([]cty.Path, len(os.AttrSensitivePaths)) copy(attrPaths, os.AttrSensitivePaths) } diff --git a/internal/states/state_test.go b/internal/states/state_test.go index ef49bb95f245..adfeec3aeae6 100644 --- a/internal/states/state_test.go +++ b/internal/states/state_test.go @@ -13,7 +13,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang/marks" ) func TestState(t *testing.T) { @@ -220,11 +219,8 @@ func TestStateDeepCopy(t *testing.T) { SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), // Sensitive path at "woozles" - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "woozles"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("woozles"), }, Private: []byte("private data"), Dependencies: []addrs.ConfigResource{ diff --git a/internal/states/statefile/version4.go b/internal/states/statefile/version4.go index f3b21c6028b1..518f8fb9ece5 100644 --- a/internal/states/statefile/version4.go +++ b/internal/states/statefile/version4.go @@ -15,7 +15,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -164,15 +163,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { if pathsDiags.HasErrors() { continue } - - var pvm []cty.PathValueMarks - for _, path := range paths { - pvm = append(pvm, cty.PathValueMarks{ - Path: path, - Marks: cty.NewValueMarks(marks.Sensitive), - }) - } - obj.AttrSensitivePaths = pvm + obj.AttrSensitivePaths = paths } { @@ -488,14 +479,8 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc } } - // Extract paths from path value marks - var paths []cty.Path - for _, vm := range obj.AttrSensitivePaths { - paths = append(paths, vm.Path) - } - // Marshal paths to JSON - attributeSensitivePaths, pathsDiags := marshalPaths(paths) + attributeSensitivePaths, pathsDiags := marshalPaths(obj.AttrSensitivePaths) diags = diags.Append(pathsDiags) return append(isV4s, instanceObjectStateV4{ @@ -825,6 +810,9 @@ func unmarshalPaths(buf []byte) ([]cty.Path, tfdiags.Diagnostics) { )) } + if len(jsonPaths) == 0 { + return nil, diags + } paths := make([]cty.Path, 0, len(jsonPaths)) unmarshalOuter: diff --git a/internal/states/statefile/version4_test.go b/internal/states/statefile/version4_test.go index fff2086aa705..f39f999719a9 100644 --- a/internal/states/statefile/version4_test.go +++ b/internal/states/statefile/version4_test.go @@ -8,8 +8,9 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" ) // This test verifies that modules are sorted before resources: diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 04b5023943fb..71afd4702276 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -14,6 +14,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -421,11 +422,8 @@ resource "test_resource" "b" { mustResourceInstanceAddr(`test_resource.a`), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("sensitive_attr"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("sensitive_attr"), }, Status: states.ObjectReady, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -555,8 +553,8 @@ resource "test_object" "y" { // make sure the same marks are compared in the next plan as well for _, c := range plan.Changes.Resources { if c.Action != plans.NoOp { - t.Logf("marks before: %#v", c.BeforeValMarks) - t.Logf("marks after: %#v", c.AfterValMarks) + t.Logf("sensitive paths before: %#v", c.BeforeSensitivePaths) + t.Logf("sensitive paths after: %#v", c.AfterSensitivePaths) t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) } } @@ -2770,11 +2768,8 @@ resource "test_resource" "a" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"value":"secret"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -2810,13 +2805,10 @@ resource "test_resource" "a" { if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) } - expectedMarkses := []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + expectedSensitivePaths := []cty.Path{ + cty.GetAttrPath("value"), } - if diff := cmp.Diff(instance.Current.AttrSensitivePaths, expectedMarkses); len(diff) > 0 { + if diff := cmp.Diff(expectedSensitivePaths, instance.Current.AttrSensitivePaths, ctydebug.CmpOptions); len(diff) > 0 { t.Errorf("unexpected sensitive paths\ndiff:\n%s", diff) } } diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go index 6b5af095a3d0..f9a753a48675 100644 --- a/internal/terraform/context_apply_test.go +++ b/internal/terraform/context_apply_test.go @@ -28,7 +28,6 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" @@ -11973,29 +11972,25 @@ resource "test_resource" "foo" { t.Fatalf("plan errors: %s", diags.Err()) } - verifySensitiveValue := func(pvms []cty.PathValueMarks) { - if len(pvms) != 2 { - t.Fatalf("expected 2 sensitive paths, got %d", len(pvms)) + verifySensitiveValue := func(paths []cty.Path) { + if len(paths) != 2 { + t.Fatalf("expected 2 sensitive paths, got %d", len(paths)) } - for _, pvm := range pvms { + for _, path := range paths { switch { - case pvm.Path.Equals(cty.GetAttrPath("value")): - case pvm.Path.Equals(cty.GetAttrPath("sensitive_value")): + case path.Equals(cty.GetAttrPath("value")): + case path.Equals(cty.GetAttrPath("sensitive_value")): default: - t.Errorf("unexpected path mark: %#v", pvm) + t.Errorf("unexpected sensitive path: %#v", path) return } - - if want := cty.NewValueMarks(marks.Sensitive); !pvm.Marks.Equal(want) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", pvm.Marks, want) - } } } addr := mustResourceInstanceAddr("test_resource.foo") fooChangeSrc := plan.Changes.ResourceInstance(addr) - verifySensitiveValue(fooChangeSrc.AfterValMarks) + verifySensitiveValue(fooChangeSrc.AfterSensitivePaths) state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { @@ -12043,19 +12038,22 @@ resource "test_resource" "baz" { t.Fatalf("plan errors: %s", diags.Err()) } - verifySensitiveValue := func(pvms []cty.PathValueMarks) { - for _, pvm := range pvms { - switch { - case pvm.Path.Equals(cty.GetAttrPath("value")): - case pvm.Path.Equals(cty.GetAttrPath("sensitive_value")): - case pvm.Path.Equals(cty.GetAttrPath("nesting_single").GetAttr("sensitive_value")): - default: - t.Errorf("unexpected path mark: %#v", pvm) - return + wantSensitivePaths := []cty.Path{ + cty.GetAttrPath("value"), + cty.GetAttrPath("sensitive_value"), + cty.GetAttrPath("nesting_single").GetAttr("sensitive_value"), + } + verifySensitiveValue := func(gotSensitivePaths []cty.Path) { + for _, gotPath := range gotSensitivePaths { + wantSensitive := false + for _, wantPath := range wantSensitivePaths { + if wantPath.Equals(gotPath) { + wantSensitive = true + break + } } - - if want := cty.NewValueMarks(marks.Sensitive); !pvm.Marks.Equal(want) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", pvm.Marks, want) + if !wantSensitive { + t.Errorf("unexpected sensitive path %s", tfdiags.FormatCtyPath(gotPath)) } } } @@ -12065,11 +12063,11 @@ resource "test_resource" "baz" { // "bar" references sensitive resources in "foo" barAddr := mustResourceInstanceAddr("test_resource.bar") barChangeSrc := plan.Changes.ResourceInstance(barAddr) - verifySensitiveValue(barChangeSrc.AfterValMarks) + verifySensitiveValue(barChangeSrc.AfterSensitivePaths) bazAddr := mustResourceInstanceAddr("test_resource.baz") bazChangeSrc := plan.Changes.ResourceInstance(bazAddr) - verifySensitiveValue(bazChangeSrc.AfterValMarks) + verifySensitiveValue(bazChangeSrc.AfterSensitivePaths) state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { @@ -12138,18 +12136,14 @@ resource "test_resource" "foo" { t.Fatalf("wrong number of sensitive paths, expected 2, got, %v", len(fooState.Current.AttrSensitivePaths)) } - for _, pvm := range fooState.Current.AttrSensitivePaths { + for _, path := range fooState.Current.AttrSensitivePaths { switch { - case pvm.Path.Equals(cty.GetAttrPath("value")): - case pvm.Path.Equals(cty.GetAttrPath("sensitive_value")): + case path.Equals(cty.GetAttrPath("value")): + case path.Equals(cty.GetAttrPath("sensitive_value")): default: - t.Errorf("unexpected path mark: %#v", pvm) + t.Errorf("unexpected sensitive path: %#v", path) return } - - if want := cty.NewValueMarks(marks.Sensitive); !pvm.Marks.Equal(want) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", pvm.Marks, want) - } } m2 := testModuleInline(t, map[string]string{ @@ -12611,17 +12605,14 @@ func TestContext2Apply_dataSensitive(t *testing.T) { addr := mustResourceInstanceAddr("data.null_data_source.testing") dataSourceState := state.ResourceInstance(addr) - pvms := dataSourceState.Current.AttrSensitivePaths - if len(pvms) != 1 { - t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) + sensitivePaths := dataSourceState.Current.AttrSensitivePaths + if len(sensitivePaths) != 1 { + t.Fatalf("expected 1 sensitive path, got %d", len(sensitivePaths)) } - pvm := pvms[0] - if gotPath, wantPath := pvm.Path, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) { + sensitivePath := sensitivePaths[0] + if gotPath, wantPath := sensitivePath, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) { t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) } - if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) - } } func TestContext2Apply_errorRestorePrivateData(t *testing.T) { diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 8a830f968096..ed14545ecc1b 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -201,11 +201,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("foo"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foo"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -2706,11 +2703,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("foo"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foo"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -2720,11 +2714,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"sensitive":"old"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("sensitive"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("sensitive"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -5403,11 +5394,8 @@ resource "test_resource" "a" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"value":"secret"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -5484,7 +5472,7 @@ resource "test_object" "obj" { assertNoErrors(t, diags) ch := plan.Changes.ResourceInstance(mustResourceInstanceAddr("test_object.obj")) - if len(ch.AfterValMarks) == 0 { + if len(ch.AfterSensitivePaths) == 0 { t.Fatal("expected marked values in test_object.obj") } @@ -5540,9 +5528,11 @@ resource "test_object" "obj" { state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.obj"), &states.ResourceInstanceObjectSrc{ - AttrsJSON: []byte(`{"id":"z","set_block":[{"foo":"bar"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{{Path: cty.Path{cty.GetAttrStep{Name: "set_block"}}, Marks: cty.NewValueMarks(marks.Sensitive)}}, - Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"z","set_block":[{"foo":"bar"}]}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("set_block"), + }, + Status: states.ObjectReady, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) }) diff --git a/internal/terraform/context_plan_import_test.go b/internal/terraform/context_plan_import_test.go index c10d60e0c0b2..91248baca45c 100644 --- a/internal/terraform/context_plan_import_test.go +++ b/internal/terraform/context_plan_import_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -1413,7 +1414,7 @@ func TestContext2Plan_importGenerateNone(t *testing.T) { import { for_each = [] to = test_object.a - id = "123" + id = "81ba7c97" } `, }) @@ -1437,3 +1438,66 @@ import { t.Fatal("expected no resource changes") } } + +// This is a test for the issue raised in #34992 +func TestContext2Plan_importWithSensitives(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "123" +} +`, + }) + + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "sensitive_string": { + Type: cty.String, + Sensitive: true, + Optional: true, + }, + "sensitive_list": { + Type: cty.List(cty.String), + Sensitive: true, + Optional: true, + }, + }, + }, + }, + }, + }, + ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "sensitive_string": cty.StringVal("sensitive"), + "sensitive_list": cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}), + }), + }, + }, + } + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Just don't crash! + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } +} diff --git a/internal/terraform/context_plan_test.go b/internal/terraform/context_plan_test.go index f71e381e9fa8..e61fc1340180 100644 --- a/internal/terraform/context_plan_test.go +++ b/internal/terraform/context_plan_test.go @@ -5605,20 +5605,17 @@ func TestContext2Plan_variableSensitivity(t *testing.T) { checkVals(t, objectVal(t, schema, map[string]cty.Value{ "foo": cty.StringVal("foo").Mark(marks.Sensitive), }), ric.After) - if len(res.ChangeSrc.BeforeValMarks) != 0 { - t.Errorf("unexpected BeforeValMarks: %#v", res.ChangeSrc.BeforeValMarks) + if len(res.ChangeSrc.BeforeSensitivePaths) != 0 { + t.Errorf("unexpected BeforeSensitivePaths: %#v", res.ChangeSrc.BeforeSensitivePaths) } - if len(res.ChangeSrc.AfterValMarks) != 1 { - t.Errorf("unexpected AfterValMarks: %#v", res.ChangeSrc.AfterValMarks) + if len(res.ChangeSrc.AfterSensitivePaths) != 1 { + t.Errorf("unexpected AfterSensitivePaths: %#v", res.ChangeSrc.AfterSensitivePaths) continue } - pvm := res.ChangeSrc.AfterValMarks[0] - if got, want := pvm.Path, cty.GetAttrPath("foo"); !got.Equals(want) { + sensitivePath := res.ChangeSrc.AfterSensitivePaths[0] + if got, want := sensitivePath, cty.GetAttrPath("foo"); !got.Equals(want) { t.Errorf("unexpected path for mark\n got: %#v\nwant: %#v", got, want) } - if got, want := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !got.Equal(want) { - t.Errorf("unexpected value for mark\n got: %#v\nwant: %#v", got, want) - } default: t.Fatal("unknown instance:", i) } @@ -5675,29 +5672,27 @@ func TestContext2Plan_variableSensitivityModule(t *testing.T) { "foo": cty.StringVal("foo").Mark(marks.Sensitive), "value": cty.StringVal("boop").Mark(marks.Sensitive), }), ric.After) - if len(res.ChangeSrc.BeforeValMarks) != 0 { - t.Errorf("unexpected BeforeValMarks: %#v", res.ChangeSrc.BeforeValMarks) + if len(res.ChangeSrc.BeforeSensitivePaths) != 0 { + t.Errorf("unexpected BeforeSensitivePaths: %#v", res.ChangeSrc.BeforeSensitivePaths) } - if len(res.ChangeSrc.AfterValMarks) != 2 { - t.Errorf("expected AfterValMarks to contain two elements: %#v", res.ChangeSrc.AfterValMarks) + if len(res.ChangeSrc.AfterSensitivePaths) != 2 { + t.Errorf("expected AfterSensitivePaths to contain two elements: %#v", res.ChangeSrc.AfterSensitivePaths) continue } // validate that the after marks have "foo" and "value" - contains := func(pvmSlice []cty.PathValueMarks, stepName string) bool { - for _, pvm := range pvmSlice { - if pvm.Path.Equals(cty.GetAttrPath(stepName)) { - if pvm.Marks.Equal(cty.NewValueMarks(marks.Sensitive)) { - return true - } + contains := func(paths []cty.Path, stepName string) bool { + for _, path := range paths { + if path.Equals(cty.GetAttrPath(stepName)) { + return true } } return false } - if !contains(res.ChangeSrc.AfterValMarks, "foo") { - t.Error("unexpected AfterValMarks to contain \"foo\" with sensitive mark") + if !contains(res.ChangeSrc.AfterSensitivePaths, "foo") { + t.Error("unexpected AfterSensitivePaths to contain \"foo\" with sensitive mark") } - if !contains(res.ChangeSrc.AfterValMarks, "value") { - t.Error("unexpected AfterValMarks to contain \"value\" with sensitive mark") + if !contains(res.ChangeSrc.AfterSensitivePaths, "value") { + t.Error("unexpected AfterSensitivePaths to contain \"value\" with sensitive mark") } default: t.Fatal("unknown instance:", i) @@ -6902,9 +6897,9 @@ resource "test_resource" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "value":"hello", "sensitive_value":"hello"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - {Path: cty.Path{cty.GetAttrStep{Name: "value"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - {Path: cty.Path{cty.GetAttrStep{Name: "sensitive_value"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + cty.GetAttrPath("sensitive_value"), }, }, addrs.AbsProviderConfig{ diff --git a/internal/terraform/evaluate_state.go b/internal/terraform/evaluate_state.go index e44c74c0e8e4..104519b2e1a2 100644 --- a/internal/terraform/evaluate_state.go +++ b/internal/terraform/evaluate_state.go @@ -546,8 +546,10 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // Unlike decoding state, decoding a change does not automatically // mark values. - instances[key] = val.MarkWithPaths(change.AfterValMarks) - continue + // FIXME: Correct that inconsistency by moving this logic into + // the decoder function in the plans package, so that we can + // test that behavior being implemented in only one place. + instances[key] = marks.MarkPaths(val, marks.Sensitive, change.AfterSensitivePaths) } ios, err := is.Current.Decode(ty) diff --git a/internal/terraform/evaluate_test.go b/internal/terraform/evaluate_test.go index 1325ce1e5e48..481baf3f3399 100644 --- a/internal/terraform/evaluate_test.go +++ b/internal/terraform/evaluate_test.go @@ -244,31 +244,13 @@ func TestEvaluatorGetResource(t *testing.T) { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "nesting_list": [{"sensitive_value":"abc"}], "nesting_map": {"foo":{"foo":"x"}}, "nesting_set": [{"baz":"abc"}], "nesting_single": {"boop":"abc"}, "nesting_nesting": {"nesting_list":[{"sensitive_value":"abc"}]}, "value":"hello"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("nesting_list").IndexInt(0).GetAttr("sensitive_value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nesting_map").IndexString("foo").GetAttr("foo"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nesting_nesting").GetAttr("nesting_list").IndexInt(0).GetAttr("sensitive_value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nesting_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nesting_single").GetAttr("boop"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("nesting_list").IndexInt(0).GetAttr("sensitive_value"), + cty.GetAttrPath("nesting_map").IndexString("foo").GetAttr("foo"), + cty.GetAttrPath("nesting_nesting").GetAttr("nesting_list").IndexInt(0).GetAttr("sensitive_value"), + cty.GetAttrPath("nesting_set"), + cty.GetAttrPath("nesting_single").GetAttr("boop"), + cty.GetAttrPath("value"), }, }, addrs.AbsProviderConfig{ diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 8c89972a2d00..e8b127885396 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" @@ -634,9 +635,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state // Unmarked before sending to provider var priorMarks []cty.PathValueMarks - if priorVal.ContainsMarked() { - priorVal, priorMarks = priorVal.UnmarkDeepWithPaths() - } + priorVal, priorMarks = priorVal.UnmarkDeepWithPaths() var resp providers.ReadResourceResponse if n.override != nil { @@ -739,8 +738,9 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state // configuration, as well as any marks from the schema which were not in // the prior state. New marks may appear when the prior state was from an // import operation, or if the provider added new marks to the schema. - if marks := append(priorMarks, schema.ValueMarks(ret.Value, nil)...); len(marks) > 0 { - ret.Value = ret.Value.MarkWithPaths(marks) + ret.Value = ret.Value.MarkWithPaths(priorMarks) + if moreSensitivePaths := schema.SensitivePaths(ret.Value, nil); len(moreSensitivePaths) != 0 { + ret.Value = marks.MarkPaths(ret.Value, marks.Sensitive, moreSensitivePaths) } return ret, deferred, diags @@ -1032,12 +1032,10 @@ func (n *NodeAbstractResourceInstance) plan( // ignore changes have been processed. We add in the schema marks as well, // to ensure that provider defined private attributes are marked correctly // here. - - unmarkedPaths = append(unmarkedPaths, schema.ValueMarks(plannedNewVal, nil)...) unmarkedPlannedNewVal := plannedNewVal - - if len(unmarkedPaths) > 0 { - plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) } reqRep, reqRepDiags := getRequiredReplaces(priorVal, plannedNewVal, resp.RequiresReplace, n.ResolvedProvider.Provider, n.Addr) @@ -1588,9 +1586,9 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal newVal = cty.UnknownAsNull(newVal) } } - pvm = append(pvm, schema.ValueMarks(newVal, nil)...) - if len(pvm) > 0 { - newVal = newVal.MarkWithPaths(pvm) + newVal = newVal.MarkWithPaths(pvm) + if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { @@ -1742,9 +1740,9 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule // even though we are only returning the config value because we can't // yet read the data source, we need to incorporate the schema marks so // that downstream consumers can detect them when planning. - unmarkedPaths = append(unmarkedPaths, schema.ValueMarks(proposedNewVal, nil)...) - if len(unmarkedPaths) > 0 { - proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) + proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { + proposedNewVal = marks.MarkPaths(proposedNewVal, marks.Sensitive, sensitivePaths) } // Apply detects that the data source will need to be read by the After @@ -1814,10 +1812,9 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule // not only do we want to ensure this synthetic value has the marks, // but since this is the value being returned from the data source // we need to ensure the schema marks are added as well. - unmarkedPaths = append(unmarkedPaths, schema.ValueMarks(newVal, nil)...) - - if len(unmarkedPaths) > 0 { - newVal = newVal.MarkWithPaths(unmarkedPaths) + newVal = newVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } // We still want to report the check as failed even if we are still @@ -2445,8 +2442,9 @@ func (n *NodeAbstractResourceInstance) apply( // re-check the value against the schema, because nested computed values // won't be included in afterPaths, which are only what was read from the // After plan value. - if marks := append(afterPaths, schema.ValueMarks(newVal, nil)...); len(marks) > 0 { - newVal = newVal.MarkWithPaths(marks) + newVal = newVal.MarkWithPaths(afterPaths) + if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } if newVal == cty.NilVal { diff --git a/internal/terraform/node_resource_plan_partialexp.go b/internal/terraform/node_resource_plan_partialexp.go index 7e16b44f7a56..eb25ba65fca8 100644 --- a/internal/terraform/node_resource_plan_partialexp.go +++ b/internal/terraform/node_resource_plan_partialexp.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/providers" @@ -291,9 +292,9 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo // We need to combine the dynamic marks with the static marks implied by // the provider's schema. - unmarkedPaths = append(unmarkedPaths, schema.ValueMarks(plannedNewVal, nil)...) - if len(unmarkedPaths) > 0 { - plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) } change.After = plannedNewVal diff --git a/website/docs/language/upgrade-guides/index.mdx b/website/docs/language/upgrade-guides/index.mdx index 875b52ef9d6f..9f7c80fb47e2 100644 --- a/website/docs/language/upgrade-guides/index.mdx +++ b/website/docs/language/upgrade-guides/index.mdx @@ -48,7 +48,7 @@ options disabled. Previous versions of Terraform used a mixture of both dynamic and static tracking of sensitive values in resource instance attributes. That meant that, -for example, correctly honoring sensitive valeus when interpreting the +for example, correctly honoring sensitive values when interpreting the `terraform show -json` output required considering both the dynamic sensitivity information directly in the output _and_ static sensitivity information in the provider schema.