From 3d211eb42ffcdc9d33d56bc2785d64b22f158c39 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Tue, 16 Apr 2024 16:59:02 +0200 Subject: [PATCH 01/25] Fix crash when importing a resource with complex sensitive attributes (#34996) --- internal/genconfig/generate_config.go | 67 +++--------------- internal/genconfig/generate_config_test.go | 70 +++++++++++++++++++ .../terraform/context_plan_import_test.go | 66 ++++++++++++++++- 3 files changed, 143 insertions(+), 60 deletions(-) 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/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()) + } +} From c9fd354d68ccc4b85b84890c5e49214b615eb745 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:24:38 -0700 Subject: [PATCH 02/25] Fix typo in docs --- website/docs/language/upgrade-guides/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 14c53f74f266371e04e57d58443ccbd7d3bb2802 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 03:57:59 -0400 Subject: [PATCH 03/25] add init view for json and human type view --- internal/command/views/init.go | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 internal/command/views/init.go diff --git a/internal/command/views/init.go b/internal/command/views/init.go new file mode 100644 index 000000000000..80967a282000 --- /dev/null +++ b/internal/command/views/init.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + + "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) +} + +// 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) +} + +// 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) +} From 7709db45dcde5a27c650cd43514e606d1d35e028 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 03:58:34 -0400 Subject: [PATCH 04/25] add init view test --- internal/command/views/init_test.go | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 internal/command/views/init_test.go diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go new file mode 100644 index 000000000000..6b293ed97aa9 --- /dev/null +++ b/internal/command/views/init_test.go @@ -0,0 +1,120 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + "strings" + "testing" + + "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_jsonView(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_humanView(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_unsupportedView(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 +} From a3f3b64e4ce6d50011498c9bc607592eb90edcd7 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 04:00:52 -0400 Subject: [PATCH 05/25] add -json to tf init --- internal/command/init.go | 82 ++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/internal/command/init.go b/internal/command/init.go index 8900d6a3df48..ee1a35e8ab44 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" @@ -42,7 +44,7 @@ type InitCommand struct { func (c *InitCommand) Run(args []string) int { var flagFromModule, flagLockfile, testsDirectory string - var flagBackend, flagCloud, flagGet, flagUpgrade bool + var flagBackend, flagCloud, flagGet, flagUpgrade, flagJson bool var flagPluginPath FlagStringSlice flagConfigExtra := newRawFlags("-backend-config") @@ -63,6 +65,8 @@ func (c *InitCommand) Run(args []string) int { 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.BoolVar(&flagJson, "json", false, "json") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -92,6 +96,15 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } + var viewType arguments.ViewType + switch { + case flagJson: + viewType = arguments.ViewJSON + default: + viewType = arguments.ViewHuman + } + view := views.NewInit(viewType, c.View) + var diags tfdiags.Diagnostics if len(flagPluginPath) > 0 { @@ -102,12 +115,14 @@ func (c *InitCommand) Run(args []string) int { args = cmdFlags.Args() path, err := ModulePath(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 } @@ -124,11 +139,13 @@ func (c *InitCommand) Run(args []string) int { 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 } @@ -149,7 +166,7 @@ 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 @@ -164,7 +181,7 @@ 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 { @@ -182,7 +199,7 @@ func (c *InitCommand) Run(args []string) int { if rootModEarly == nil { c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) diags = diags.Append(earlyConfDiags) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -196,9 +213,9 @@ func (c *InitCommand) Run(args []string) int { switch { case flagCloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, viewType) case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, viewType) default: // load the previously-stored backend config back, backDiags = c.Meta.backendFromState(ctx) @@ -216,17 +233,20 @@ 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 } @@ -237,7 +257,7 @@ func (c *InitCommand) Run(args []string) int { modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade) diags = diags.Append(modsDiags) if modsAbort || modsDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if modsOutput { @@ -258,7 +278,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 } @@ -271,7 +291,7 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(backDiags) if earlyConfDiags.HasErrors() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -279,7 +299,7 @@ func (c *InitCommand) Run(args []string) int { // show the errInitConfigError preamble as we didn't detect problems with // the early configuration. if backDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -288,7 +308,7 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(confDiags) if confDiags.HasErrors() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -296,17 +316,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, flagUpgrade, flagPluginPath, flagLockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if providersOutput { @@ -322,7 +342,7 @@ func (c *InitCommand) Run(args []string) int { // 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 if cloud { @@ -398,7 +418,7 @@ 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 rawFlags, viewType arguments.ViewType) (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() @@ -417,8 +437,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,7 +447,7 @@ 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 rawFlags, viewType arguments.ViewType) (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() @@ -502,6 +523,7 @@ the backend configuration is present and valid. Config: backendConfig, ConfigOverride: backendConfigOverride, Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -511,7 +533,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() @@ -881,7 +903,7 @@ 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) + view.Diagnostics(diags) c.Ui.Error("Provider installation was canceled by an interrupt signal.") return true, true, diags } @@ -1089,6 +1111,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 +1174,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 From 26f01d699ac278507eb7c75a724d87eb9674c66f Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 04:02:09 -0400 Subject: [PATCH 06/25] add test for -json support --- internal/command/init_test.go | 233 +++++++++++------- .../output.jsonlog | 2 + 2 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 internal/command/testdata/init-with-tests-with-provider/output.jsonlog diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 2422b319682f..789af8dc2e11 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -15,10 +15,9 @@ 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/configs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -184,6 +183,34 @@ func TestInit_get(t *testing.T) { } } +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, _ := 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", ui.ErrorWriter.String()) + } + + // Check output + output := ui.OutputWriter.String() + if !strings.Contains(output, "foo in foo") { + t.Fatalf("doesn't look like we installed module 'foo': %s", output) + } +} + func TestInit_getUpgradeModules(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -315,7 +342,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 +352,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 +365,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,7 +377,7 @@ 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") { + if !strings.Contains(done(t).All(), "Unsupported block type") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -358,7 +385,7 @@ func TestInit_backendConfigFile(t *testing.T) { // 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,7 +397,7 @@ 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") { + if !strings.Contains(done(t).All(), "Unsupported argument") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -378,7 +405,7 @@ func TestInit_backendConfigFile(t *testing.T) { // 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,7 +417,7 @@ 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") { + if !strings.Contains(done(t).All(), "Failed to read file") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -398,7 +425,7 @@ func TestInit_backendConfigFile(t *testing.T) { // 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 +435,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 @@ -450,7 +477,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 +495,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) } } @@ -613,10 +641,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, }, } @@ -786,7 +817,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()), @@ -800,7 +831,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) } - 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) } @@ -978,7 +1009,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, @@ -990,7 +1021,7 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -999,7 +1030,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 +1047,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, @@ -1029,7 +1059,7 @@ Cloud configuration block in the root module. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1038,7 +1068,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 +1085,7 @@ Cloud configuration settings. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1068,13 +1097,12 @@ Cloud configuration settings. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() 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 +1115,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, @@ -1099,7 +1127,7 @@ because activating Terraform Cloud involves some additional steps. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1108,7 +1136,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 +1153,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1138,7 +1165,7 @@ storage location is not configurable. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1147,7 +1174,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 +1186,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, @@ -1172,7 +1198,7 @@ prompts. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1181,7 +1207,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 +1224,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1211,7 +1236,7 @@ storage location is not configurable. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1220,7 +1245,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 +1260,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()), @@ -1286,7 +1310,7 @@ func TestInit_inputFalse(t *testing.T) { t.Fatal("init should have failed", ui.OutputWriter) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "interactive input is disabled") { t.Fatal("expected input disabled error, got", errMsg) } @@ -1394,7 +1418,7 @@ 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{ @@ -1405,7 +1429,7 @@ func TestInit_getProvider(t *testing.T) { t.Fatal("expected error, got:", ui.OutputWriter) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1470,7 +1494,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"}, }) @@ -1495,7 +1519,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 := done(t).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 +1535,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") @@ -1557,7 +1581,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 := done(t).All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1588,7 +1612,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -1618,7 +1642,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := ui.ErrorWriter.String() + errOutput := done(t).All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1646,7 +1670,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, @@ -1735,7 +1759,7 @@ func TestInit_providerSource(t *testing.T) { if got, want := ui.OutputWriter.String(), "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 := done(t).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) } } @@ -1965,7 +1989,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, @@ -1982,7 +2006,7 @@ func TestInit_getProviderMissing(t *testing.T) { t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) } - if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") { + if !strings.Contains(done(t).All(), "no available releases match") { t.Fatalf("unexpected error output: %s", ui.ErrorWriter) } } @@ -1994,7 +2018,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 +2029,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).All(), ui.OutputWriter.String()) } - 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 +2049,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 +2060,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).All(), ui.OutputWriter.String()) } - 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 +2073,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 +2084,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).All(), ui.OutputWriter.String()) } - 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 +2107,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, @@ -2128,7 +2152,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 +2292,11 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -2485,7 +2511,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, @@ -2531,7 +2557,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { // 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 := done(t).All() 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) } @@ -2596,7 +2622,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, @@ -2612,7 +2638,7 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() 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 +2653,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, @@ -2641,9 +2667,9 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - 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) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2656,7 +2682,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, @@ -2670,9 +2696,9 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - 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) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2685,7 +2711,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, @@ -2699,9 +2725,9 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - 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) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2717,7 +2743,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, @@ -2731,9 +2757,9 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - 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) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2787,7 +2813,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), @@ -2802,20 +2828,51 @@ func TestInit_testsWithProvider(t *testing.T) { t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) } - got := ui.ErrorWriter.String() + got := done(t).All() 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) } } +func TestInit_jsonTestsWithProvider(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code == 0 { + t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) + } + + checkGoldenReference(t, done(t), "init-with-tests-with-provider") +} + func TestInit_testsWithModule(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() diff --git a/internal/command/testdata/init-with-tests-with-provider/output.jsonlog b/internal/command/testdata/init-with-tests-with-provider/output.jsonlog new file mode 100644 index 000000000000..aad930f3fedf --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-provider/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: Failed to query available provider packages","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"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"},"type":"diagnostic"} \ No newline at end of file From 370a471ecb4d940550d040e9df45d4bcfcd18408 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Thu, 28 Mar 2024 00:40:24 -0400 Subject: [PATCH 07/25] fix json -help display and failing tests --- internal/command/e2etest/init_test.go | 3 +- internal/command/init.go | 4 +- internal/command/providers_schema_test.go | 2 + internal/command/providers_test.go | 2 + internal/command/show_test.go | 8 +++ internal/command/test_test.go | 66 ++++++++++++++++++----- internal/command/validate_test.go | 14 +++++ 7 files changed, 83 insertions(+), 16 deletions(-) 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/init.go b/internal/command/init.go index ee1a35e8ab44..4993c2d9828c 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -1174,8 +1174,8 @@ Options: -no-color If specified, output won't contain any color. - -json If specified, machine readable output will be - printed in JSON format. + -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 diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index ce3ee2425cf3..bcbd48a65cee 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, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, } 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/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_test.go b/internal/command/test_test.go index bf8a1f3e36d9..a1f4f4dca56f 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -290,8 +290,8 @@ func TestTest_Runs(t *testing.T) { 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) @@ -872,8 +872,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 { @@ -1063,8 +1063,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 { @@ -1124,10 +1124,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 { @@ -1915,7 +1915,21 @@ func TestTest_InvalidOverrides(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := ` +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) +main.tftest.hcl... in progress run "setup"... pass Warning: Invalid override target @@ -2009,7 +2023,21 @@ func TestTest_RunBlocksInProviders(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := ` +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) +main.tftest.hcl... in progress run "setup"... pass run "main"... pass main.tftest.hcl... tearing down @@ -2070,7 +2098,21 @@ func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { t.Errorf("expected status code 1 but got %d", code) } - expectedOut := `missing_run_block.tftest.hcl... in progress + expectedOut := ` +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) +missing_run_block.tftest.hcl... in progress run "main"... fail missing_run_block.tftest.hcl... tearing down missing_run_block.tftest.hcl... fail diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index cc960fb87b66..d7a7e94ff068 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -362,6 +362,20 @@ func TestValidateWithInvalidOverrides(t *testing.T) { actual := output.All() expected := ` +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) + Warning: Invalid override target on main.tftest.hcl line 4, in mock_provider "test": From ea8d0869d8ee55db1743979941d941523fb770b8 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Fri, 5 Apr 2024 01:30:55 -0400 Subject: [PATCH 08/25] convert all logs to be view type exclusive for human or json format --- internal/command/arguments/init.go | 116 ++++++++ internal/command/hook_module_install.go | 21 +- internal/command/init.go | 222 ++++++---------- internal/command/views/init.go | 340 ++++++++++++++++++++++++ 4 files changed, 551 insertions(+), 148 deletions(-) create mode 100644 internal/command/arguments/init.go diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go new file mode 100644 index 000000000000..fc4a702769d9 --- /dev/null +++ b/internal/command/arguments/init.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "flag" + "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 +} + +// 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, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + init := &Init{} + + 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") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + 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/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 4993c2d9828c..a869503ce803 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -43,50 +43,47 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagFromModule, flagLockfile, testsDirectory string - var flagBackend, flagCloud, flagGet, flagUpgrade, flagJson 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.Usage = func() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + c.Help(), + )) + } + 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.BoolVar(&flagJson, "json", false, "json") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } + initArgs, initDiags := arguments.ParseInit(args, cmdFlags) - 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 } + 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 + if c.migrateState && c.reconfigure { - c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive") + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -migrate-state and -reconfigure options are mutually-exclusive", + )) + view.Diagnostics(diags) return 1 } @@ -96,17 +93,6 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - var viewType arguments.ViewType - switch { - case flagJson: - viewType = arguments.ViewJSON - default: - viewType = arguments.ViewHuman - } - view := views.NewInit(viewType, c.View) - - var diags tfdiags.Diagnostics - if len(flagPluginPath) > 0 { c.pluginPath = flagPluginPath } @@ -134,8 +120,8 @@ 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 { @@ -149,14 +135,13 @@ func (c *InitCommand) Run(args []string) int { 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{ + hooks := uiModuleInstallHooks{ // here check to verify if downloading prints text, update to handle view type 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( @@ -173,7 +158,7 @@ func (c *InitCommand) Run(args []string) int { } 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 @@ -185,20 +170,19 @@ func (c *InitCommand) Run(args []string) int { 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) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) view.Diagnostics(diags) return 1 @@ -212,10 +196,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, viewType) - case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, viewType) + case initArgs.Cloud && rootModEarly.CloudConfig != nil: + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + case initArgs.Backend: + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) default: // load the previously-stored backend config back, backDiags = c.Meta.backendFromState(ctx) @@ -253,8 +237,8 @@ func (c *InitCommand) Run(args []string) int { 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() { view.Diagnostics(diags) @@ -267,7 +251,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. @@ -290,13 +274,13 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(earlyConfDiags) diags = diags.Append(backDiags) if earlyConfDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) + 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() { view.Diagnostics(diags) @@ -307,7 +291,7 @@ 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)) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError))) view.Diagnostics(diags) return 1 } @@ -323,7 +307,7 @@ func (c *InitCommand) Run(args []string) int { } // 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, view) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, flagPluginPath, initArgs.Lockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { view.Diagnostics(diags) @@ -336,7 +320,7 @@ 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 @@ -344,27 +328,27 @@ func (c *InitCommand) Run(args []string) int { // still the final thing shown. 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 { @@ -385,14 +369,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) @@ -418,12 +403,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, viewType arguments.ViewType) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, 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( @@ -447,12 +432,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, viewType arguments.ViewType) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, 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 @@ -606,15 +591,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.Log(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - c.Ui.Info(fmt.Sprintf("- %s is built in to Terraform", provider.ForDisplay())) + view.Log(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -625,20 +608,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.Log(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.Log(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - c.Ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay())) + view.Log(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.Log(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.Log(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -835,10 +818,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.Log(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 @@ -884,9 +867,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.Log(views.PartnerAndCommunityProvidersMessage) } }, } @@ -895,7 +876,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 } @@ -903,8 +885,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) if ctx.Err() == context.Canceled { + diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) view.Diagnostics(diags) - c.Ui.Error("Provider installation was canceled by an interrupt signal.") return true, true, diags } if err != nil { @@ -971,16 +953,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) @@ -1213,14 +1188,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. @@ -1229,39 +1196,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/views/init.go b/internal/command/views/init.go index 80967a282000..a53a80a28201 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -4,7 +4,10 @@ package views import ( + "encoding/json" "fmt" + "strings" + "time" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/tfdiags" @@ -13,6 +16,9 @@ import ( // The Init view is used for the init command. type Init interface { Diagnostics(diags tfdiags.Diagnostics) + Output(messageCode string, params ...any) + Log(messageCode string, params ...any) + PrepareMessage(messageCode string, params ...any) string } // NewInit returns Init implementation for the given ViewType. @@ -43,6 +49,29 @@ func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } +func (v *InitHuman) Output(messageCode string, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) Log(messageCode string, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) PrepareMessage(messageCode string, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return 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 { @@ -54,3 +83,314 @@ var _ Init = (*InitJSON)(nil) func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } + +func (v *InitJSON) Output(messageCode string, params ...any) { + current_timestamp := time.Now().Format(time.RFC3339) + + json_data := map[string]string{ + "@level": "info", + "@message": v.PrepareMessage(messageCode, params...), + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "init_output"} + + init_output, _ := json.Marshal(json_data) + v.view.view.streams.Println(string(init_output)) +} + +func (v *InitJSON) Log(messageCode string, params ...any) { + v.view.Log(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitJSON) PrepareMessage(messageCode string, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return 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[string]InitMessage = map[string]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: "- Using previously-installed %s v%s", + }, + "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: "- Reusing previous version of %s from the dependency lock file", + }, + "finding_matching_version_message": { + HumanValue: "- Finding %s versions matching %q...", + JSONValue: "- Finding %s versions matching %q...", + }, + "finding_latest_version_message": { + HumanValue: "- Finding latest version of %s...", + JSONValue: "- Finding latest version of %s...", + }, + "using_provider_from_cache_dir_info": { + HumanValue: "- Using %s v%s from the shared cache directory", + JSONValue: "- Using %s v%s from the shared cache directory", + }, + "installing_provider_message": { + HumanValue: "- Installing %s v%s...", + JSONValue: "- Installing %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 %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: "", + }, +} + +const ( + CopyingConfigurationMessage string = "copying_configuration_message" + EmptyMessage string = "empty_message" + OutputInitEmptyMessage string = "output_init_empty_message" + OutputInitSuccessMessage string = "output_init_success_message" + OutputInitSuccessCloudMessage string = "output_init_success_cloud_message" + OutputInitSuccessCLIMessage string = "output_init_success_cli_message" + OutputInitSuccessCLICloudMessage string = "output_init_success_cli_cloud_message" + UpgradingModulesMessage string = "upgrading_modules_message" + InitializingTerraformCloudMessage string = "initializing_terraform_cloud_message" + InitializingModulesMessage string = "initializing_modules_message" + InitializingBackendMessage string = "initializing_backend_message" + InitializingProviderPluginMessage string = "initializing_provider_plugin_message" + LockInfo string = "lock_info" + DependenciesLockChangesInfo string = "dependencies_lock_changes_info" + ProviderAlreadyInstalledMessage string = "provider_already_installed_message" + BuiltInProviderAvailableMessage string = "built_in_provider_available_message" + ReusingPreviousVersionInfo string = "reusing_previous_version_info" + FindingMatchingVersionMessage string = "finding_matching_version_message" + FindingLatestVersionMessage string = "finding_latest_version_message" + UsingProviderFromCacheDirInfo string = "using_provider_from_cache_dir_info" + InstallingProviderMessage string = "installing_provider_message" + KeyID string = "key_id" + InstalledProviderVersionInfo string = "installed_provider_version_info" + PartnerAndCommunityProvidersMessage string = "partner_and_community_providers_message" + InitConfigError string = "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. +` + +// 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. +const providerProtocolTooOld = `Provider %q v%s is not compatible with Terraform %s. +Provider version %s is the latest compatible version. Select it with the following version constraint: + version = %q + +Terraform checked all of the plugin versions matching the given constraint: + %s + +Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. +` + +// providerProtocolTooNew is a message sent to the CLI UI if the provider's +// supported protocol versions are too new for the user's version of terraform, +// and the user could either upgrade terraform or choose an older version of the +// provider. +const providerProtocolTooNew = `Provider %q v%s is not compatible with Terraform %s. +You need to downgrade to v%s or earlier. Select it with the following constraint: + version = %q + +Terraform checked all of the plugin versions matching the given constraint: + %s + +Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. +Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases. +` + +// incompleteLockFileInformationHeader is the summary displayed to users when +// the lock file has only recorded local hashes. +const incompleteLockFileInformationHeader = `Incomplete lock file information for providers` + +// incompleteLockFileInformationBody is the body of text displayed to users when +// the lock file has only recorded local hashes. +const incompleteLockFileInformationBody = `Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers: + - %s + +The current .terraform.lock.hcl file only includes checksums for %s, 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)` + +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. +` From ba8386b8a2b0af7d66e86ff92fb5b993c2ba92d2 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Sun, 7 Apr 2024 12:18:26 -0400 Subject: [PATCH 09/25] json does not support interactive prompt so dont allow setting of both -migrate-state and -json options --- internal/command/init.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/command/init.go b/internal/command/init.go index a869503ce803..f382c1c3e2a4 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -77,6 +77,16 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = initArgs.MigrateState c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion + if initArgs.MigrateState && initArgs.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.", + )) + view.Diagnostics(diags) + return 1 + } + if c.migrateState && c.reconfigure { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, From 0ea25e3b403a91656ababc7df8883e48431d4245 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Sun, 7 Apr 2024 19:31:23 -0400 Subject: [PATCH 10/25] fixing tests internal/command/init, view and argumento --- internal/command/arguments/init_test.go | 136 ++++++ internal/command/init.go | 2 +- internal/command/init_test.go | 437 ++++++++++-------- internal/command/providers_schema_test.go | 7 +- internal/command/test_test.go | 84 +++- .../command/testdata/init-get/output.jsonlog | 8 + .../init-migrate-state-with-json/hello.tf | 0 .../output.jsonlog | 2 + .../output.jsonlog | 2 - internal/command/validate_test.go | 24 +- internal/command/views/init.go | 45 +- internal/command/views/init_test.go | 237 +++++++++- internal/command/views/json_view_test.go | 2 +- 13 files changed, 723 insertions(+), 263 deletions(-) create mode 100644 internal/command/arguments/init_test.go create mode 100644 internal/command/testdata/init-get/output.jsonlog create mode 100644 internal/command/testdata/init-migrate-state-with-json/hello.tf create mode 100644 internal/command/testdata/init-migrate-state-with-json/output.jsonlog delete mode 100644 internal/command/testdata/init-with-tests-with-provider/output.jsonlog diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go new file mode 100644 index 000000000000..6dacbf6eff2e --- /dev/null +++ b/internal/command/arguments/init_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "flag" + "io" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestParseInit_basicValid(t *testing.T) { + 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, + }, + }, + "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", + "-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, + }, + }, + "with cloud option": { + []string{"-cloud=false"}, + &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, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) + cmdFlags.SetOutput(io.Discard) + + got, diags := ParseInit(tc.args, cmdFlags) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseInit_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + wantErr string + }{ + "with unsupported options": { + args: []string{"-raw"}, + wantErr: "flag provided but not defined", + }, + "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", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) + cmdFlags.SetOutput(io.Discard) + + got, diags := ParseInit(tc.args, cmdFlags) + 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 != ViewHuman { + t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman) + } + }) + } +} diff --git a/internal/command/init.go b/internal/command/init.go index f382c1c3e2a4..83800452ecf2 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -148,7 +148,7 @@ func (c *InitCommand) Run(args []string) int { view.Output(views.CopyingConfigurationMessage, src) header = true - hooks := uiModuleInstallHooks{ // here check to verify if downloading prints text, update to handle view type + hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: false, // since they are in a weird location for init View: view, diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 789af8dc2e11..62ad7f284674 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -36,7 +36,7 @@ func TestInit_empty(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -47,7 +47,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()) } } @@ -58,7 +58,7 @@ func TestInit_multipleArgs(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -72,10 +72,40 @@ func TestInit_multipleArgs(t *testing.T) { "bad", } if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } +func TestInit_migrateStateAndJSON(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{ + "-migrate-state=true", + "-json=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("error, -migrate-state and -json should be exclusive: \n%s", testOutput.All()) + } + + // Check output + checkGoldenReference(t, testOutput, "init-migrate-state-with-json") +} + func TestInit_fromModule_cwdDest(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -83,7 +113,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -96,7 +126,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { "-from-module=" + testFixturePath("init"), } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { @@ -134,7 +164,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -147,7 +177,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { "-from-module=./..", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { @@ -162,7 +192,7 @@ func TestInit_get(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -173,11 +203,11 @@ 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) } @@ -190,7 +220,7 @@ func TestInit_json(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()), @@ -201,14 +231,12 @@ func TestInit_json(t *testing.T) { args := []string{"-json"} 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() - if !strings.Contains(output, "foo in foo") { - t.Fatalf("doesn't look like we installed module 'foo': %s", output) - } + output := done(t) + checkGoldenReference(t, output, "init-get") } func TestInit_getUpgradeModules(t *testing.T) { @@ -218,7 +246,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()), @@ -231,14 +259,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()) } } @@ -249,7 +278,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()), @@ -260,7 +289,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 { @@ -278,7 +307,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()), @@ -289,12 +318,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) @@ -310,7 +341,7 @@ func TestInit_backendUnset(t *testing.T) { } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -320,12 +351,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() { @@ -378,7 +411,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Unsupported block type") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -398,7 +431,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Unsupported argument") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -418,7 +451,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Failed to read file") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -518,7 +551,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()), @@ -541,7 +574,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. @@ -549,7 +582,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()) } } @@ -560,7 +593,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()), @@ -571,7 +604,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 @@ -593,7 +626,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()), @@ -624,13 +657,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()) } } @@ -678,7 +711,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()), @@ -689,7 +722,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 @@ -706,7 +739,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()), @@ -717,7 +750,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) @@ -732,7 +765,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 @@ -748,7 +781,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 @@ -769,7 +802,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()), @@ -780,7 +813,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) @@ -796,7 +829,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 @@ -828,7 +861,7 @@ 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 := done(t).All() @@ -856,7 +889,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()), @@ -867,7 +900,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 @@ -882,7 +915,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 { @@ -900,7 +933,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()), @@ -910,7 +943,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 @@ -939,7 +972,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 { @@ -1018,10 +1051,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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1056,10 +1089,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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1094,10 +1127,10 @@ 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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1124,10 +1157,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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1162,10 +1195,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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1195,10 +1228,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 := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1232,11 +1265,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 := done(t).All() + gotStderr := testOutput.Stderr() wantStderr := ` Error: Invalid command-line option @@ -1271,7 +1306,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 @@ -1307,7 +1342,7 @@ 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 := done(t).All() @@ -1327,7 +1362,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()) } } @@ -1339,7 +1374,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"}, @@ -1364,7 +1399,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 @@ -1425,11 +1460,13 @@ func TestInit_getProvider(t *testing.T) { 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 := done(t).All() + errMsg := testOutput.Stderr() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1444,7 +1481,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"}, @@ -1468,7 +1505,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 @@ -1509,9 +1546,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 @@ -1519,7 +1557,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { "Invalid legacy provider address", "You must complete the Terraform 0.13 upgrade process", } - got := done(t).All() + 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) @@ -1567,8 +1605,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 @@ -1581,7 +1621,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { "Failed to install provider", "could not find executable file starting with terraform-provider-package", } - got := done(t).All() + got := testOutput.All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1626,8 +1666,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 @@ -1642,7 +1684,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := done(t).All() + errOutput := testOutput.All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1683,11 +1725,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") } @@ -1756,10 +1799,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 := done(t).All(), "\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) } } @@ -1778,7 +1821,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, @@ -1791,12 +1834,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) } } @@ -1820,7 +1864,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, @@ -1834,15 +1878,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) } } @@ -1864,7 +1909,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, @@ -1885,7 +1930,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() @@ -2002,12 +2047,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(done(t).All(), "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()) } } @@ -2029,7 +2076,7 @@ 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, done(t).All(), 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 := done(t).All() if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { @@ -2060,7 +2107,7 @@ 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, done(t).All(), 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 := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { @@ -2084,7 +2131,7 @@ 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, done(t).All(), 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 := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { @@ -2121,7 +2168,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" @@ -2292,7 +2339,7 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2312,10 +2359,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) @@ -2340,7 +2387,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()), @@ -2361,7 +2408,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() @@ -2386,7 +2433,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() @@ -2410,7 +2457,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, @@ -2451,7 +2498,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() @@ -2549,15 +2596,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 := done(t).All() + 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) } @@ -2584,7 +2633,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, @@ -2597,11 +2646,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) } @@ -2634,11 +2685,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 := done(t).All() + 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) } @@ -2663,13 +2716,15 @@ 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 := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, 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) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2692,13 +2747,15 @@ 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 := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, 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) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2721,13 +2778,15 @@ 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 := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, 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) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2753,13 +2812,15 @@ 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 := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, 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) } if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2783,7 +2844,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), @@ -2795,7 +2856,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()) } } @@ -2824,11 +2885,13 @@ 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 := done(t).All() + got := testOutput.Stderr() want := ` Error: Failed to query available provider packages @@ -2841,38 +2904,6 @@ hashicorp/test: no available releases match the given constraints 1.0.1, } } -func TestInit_jsonTestsWithProvider(t *testing.T) { - // Create a temporary working directory that is empty - td := t.TempDir() - testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) - defer testChdir(t, td)() - - provider := applyFixtureProvider() // We just want the types from this provider. - - providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, - }) - defer close() - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(provider), - Ui: ui, - View: view, - ProviderSource: providerSource, - }, - } - - args := []string{"-json"} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) - } - - checkGoldenReference(t, done(t), "init-with-tests-with-provider") -} - func TestInit_testsWithModule(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -2887,7 +2918,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), @@ -2898,12 +2929,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/providers_schema_test.go b/internal/command/providers_schema_test.go index bcbd48a65cee..235f1094338f 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -59,7 +59,7 @@ func TestProvidersSchema_output(t *testing.T) { p := providersSchemaFixtureProvider() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, @@ -72,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/test_test.go b/internal/command/test_test.go index a1f4f4dca56f..95795fb234ab 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -1710,7 +1710,21 @@ func TestTest_SensitiveInputValues(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := `Initializing the backend... +Initializing modules... +- test.main.setup in setup +Initializing provider plugins... + +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. +main.tftest.hcl... in progress run "setup"... pass run "test"... pass @@ -1915,7 +1929,20 @@ func TestTest_InvalidOverrides(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - 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 @@ -1929,6 +1956,15 @@ 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. main.tftest.hcl... in progress run "setup"... pass @@ -2023,7 +2059,19 @@ func TestTest_RunBlocksInProviders(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := ` + expected := `Initializing the backend... +Initializing modules... +- 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 @@ -2037,6 +2085,15 @@ 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. main.tftest.hcl... in progress run "setup"... pass run "main"... pass @@ -2098,7 +2155,17 @@ func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { t.Errorf("expected status code 1 but got %d", code) } - expectedOut := ` + expectedOut := `Initializing the backend... +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 @@ -2112,6 +2179,15 @@ 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. missing_run_block.tftest.hcl... in progress run "main"... fail missing_run_block.tftest.hcl... tearing down diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog new file mode 100644 index 000000000000..642606d7704b --- /dev/null +++ b/internal/command/testdata/init-get/output.jsonlog @@ -0,0 +1,8 @@ +{"@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","type":"init_output"} +{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} +{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","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","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/init-with-tests-with-provider/output.jsonlog b/internal/command/testdata/init-with-tests-with-provider/output.jsonlog deleted file mode 100644 index aad930f3fedf..000000000000 --- a/internal/command/testdata/init-with-tests-with-provider/output.jsonlog +++ /dev/null @@ -1,2 +0,0 @@ -{"@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: Failed to query available provider packages","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"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"},"type":"diagnostic"} \ No newline at end of file diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index d7a7e94ff068..b3ae34bcfe65 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -361,7 +361,20 @@ 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 @@ -375,6 +388,15 @@ 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 diff --git a/internal/command/views/init.go b/internal/command/views/init.go index a53a80a28201..97076b813bcf 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -85,7 +85,7 @@ func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { } func (v *InitJSON) Output(messageCode string, params ...any) { - current_timestamp := time.Now().Format(time.RFC3339) + current_timestamp := time.Now().UTC().Format(time.RFC3339) json_data := map[string]string{ "@level": "info", @@ -315,49 +315,6 @@ 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. -const providerProtocolTooOld = `Provider %q v%s is not compatible with Terraform %s. -Provider version %s is the latest compatible version. Select it with the following version constraint: - version = %q - -Terraform checked all of the plugin versions matching the given constraint: - %s - -Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. -` - -// providerProtocolTooNew is a message sent to the CLI UI if the provider's -// supported protocol versions are too new for the user's version of terraform, -// and the user could either upgrade terraform or choose an older version of the -// provider. -const providerProtocolTooNew = `Provider %q v%s is not compatible with Terraform %s. -You need to downgrade to v%s or earlier. Select it with the following constraint: - version = %q - -Terraform checked all of the plugin versions matching the given constraint: - %s - -Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. -Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases. -` - -// incompleteLockFileInformationHeader is the summary displayed to users when -// the lock file has only recorded local hashes. -const incompleteLockFileInformationHeader = `Incomplete lock file information for providers` - -// incompleteLockFileInformationBody is the body of text displayed to users when -// the lock file has only recorded local hashes. -const incompleteLockFileInformationBody = `Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers: - - %s - -The current .terraform.lock.hcl file only includes checksums for %s, 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)` - 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 diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 6b293ed97aa9..fb84d6fad8c6 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -15,7 +16,7 @@ import ( tfversion "github.com/hashicorp/terraform/version" ) -func TestNewInit_jsonView(t *testing.T) { +func TestNewInit_jsonViewDiagnostics(t *testing.T) { streams, done := terminal.StreamsForTesting(t) newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) @@ -64,7 +65,7 @@ func TestNewInit_jsonView(t *testing.T) { testJSONViewOutputEqualsFull(t, actual, want) } -func TestNewInit_humanView(t *testing.T) { +func TestNewInit_humanViewDiagnostics(t *testing.T) { streams, done := terminal.StreamsForTesting(t) newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) @@ -82,7 +83,7 @@ func TestNewInit_humanView(t *testing.T) { } } -func TestNewInit_unsupportedView(t *testing.T) { +func TestNewInit_unsupportedViewDiagnostics(t *testing.T) { defer func() { r := recover() if r == nil { @@ -118,3 +119,233 @@ func getTestDiags(t *testing.T) tfdiags.Diagnostics { 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) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Output(messageCode) + + 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": "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" + messageCode := "finding_latest_version_message" + newInit.Output(messageCode, 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("- Finding latest version of %s...", packageName), + "@module": "terraform.ui", + "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" + messageCode := "provider_already_installed_message" + newInit.Output(messageCode, 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("- Using previously-installed %s v%s", packageName, packageVersion), + "@module": "terraform.ui", + "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) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Log(messageCode) + + 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("message code that does not exists", 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) + } + + messageCode := "Terraform has been successfully initialized!" + want := messageCode + + actual := newInit.PrepareMessage(messageCode) + if !cmp.Equal(want, actual) { + t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) + } + }) + + 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) + } + + messageCode := "initializing_modules_message" + want := "Initializing modules..." + + actual := newInit.PrepareMessage(messageCode) + 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) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Output(messageCode) + + 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" + messageCode := "finding_latest_version_message" + newInit.Output(messageCode, 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" + messageCode := "provider_already_installed_message" + newInit.Output(messageCode, 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) } } From ce7824f90549994ec1389283dba32af6af6e7763 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 10 Apr 2024 14:16:22 -0400 Subject: [PATCH 11/25] add interrupt error to the diags so each component becomes responsible for rendering the errors --- internal/command/meta_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index eb282cd66355..917d9d08a834 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 From c7bbc09631783c6aaecdacfc58a135ced6bc0129 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Mon, 15 Apr 2024 19:28:30 -0400 Subject: [PATCH 12/25] move all command args from command/init to args/init --- internal/command/apply.go | 4 +- internal/command/arguments/extended.go | 12 +- internal/command/arguments/flags.go | 60 +++++----- internal/command/arguments/init.go | 49 +++++++- internal/command/arguments/init_test.go | 12 +- internal/command/arguments/test.go | 2 +- internal/command/flag_kv.go | 13 -- internal/command/init.go | 83 +++++-------- internal/command/init_test.go | 3 +- internal/command/meta.go | 8 +- internal/command/meta_config.go | 57 --------- internal/command/meta_vars.go | 10 +- internal/command/plan.go | 4 +- internal/command/providers_lock.go | 3 +- internal/command/providers_mirror.go | 3 +- internal/command/refresh.go | 4 +- internal/command/test.go | 26 ++-- .../command/testdata/init-get/output.jsonlog | 1 - internal/command/views/init.go | 113 +++++++++++------- internal/command/views/init_test.go | 41 ++----- 20 files changed, 223 insertions(+), 285 deletions(-) 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 index fc4a702769d9..b6ae8f98c2aa 100644 --- a/internal/command/arguments/init.go +++ b/internal/command/arguments/init.go @@ -4,7 +4,6 @@ package arguments import ( - "flag" "time" "github.com/hashicorp/terraform/internal/tfdiags" @@ -58,15 +57,39 @@ type Init struct { // 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, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostics) { +func ParseInit(args []string) (*Init, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - init := &Init{} + 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") @@ -81,6 +104,8 @@ func ParseInit(args []string, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostic 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( @@ -90,6 +115,24 @@ func ParseInit(args []string, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostic )) } + 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") diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 6dacbf6eff2e..97f7c76a34c2 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -4,8 +4,6 @@ package arguments import ( - "flag" - "io" "strings" "testing" "time" @@ -86,10 +84,7 @@ func TestParseInit_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) - cmdFlags.SetOutput(io.Discard) - - got, diags := ParseInit(tc.args, cmdFlags) + got, diags := ParseInit(tc.args) if len(diags) > 0 { t.Fatalf("unexpected diags: %v", diags) } @@ -118,10 +113,7 @@ func TestParseInit_invalid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) - cmdFlags.SetOutput(io.Discard) - - got, diags := ParseInit(tc.args, cmdFlags) + got, diags := ParseInit(tc.args) if len(diags) == 0 { t.Fatal("expected diags but got none") } 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/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/init.go b/internal/command/init.go index 83800452ecf2..c2b7bb4cc7a7 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -43,24 +43,9 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagPluginPath FlagStringSlice - flagConfigExtra := newRawFlags("-backend-config") - var diags tfdiags.Diagnostics args = c.Meta.process(args) - cmdFlags := c.Meta.extendedFlagSet("init") - cmdFlags.Usage = func() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to parse command-line flags", - c.Help(), - )) - } - - cmdFlags.Var(flagConfigExtra, "backend-config", "") - cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") - - initArgs, initDiags := arguments.ParseInit(args, cmdFlags) + initArgs, initDiags := arguments.ParseInit(args) view := views.NewInit(initArgs.ViewType, c.View) @@ -76,26 +61,17 @@ func (c *InitCommand) Run(args []string) int { c.reconfigure = initArgs.Reconfigure c.migrateState = initArgs.MigrateState c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion - - if initArgs.MigrateState && initArgs.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.", - )) - view.Diagnostics(diags) - return 1 - } - - if c.migrateState && c.reconfigure { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid init options", - "The -migrate-state and -reconfigure options are mutually-exclusive", - )) - view.Diagnostics(diags) - return 1 + 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 @@ -103,13 +79,12 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - 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 { diags = diags.Append(err) view.Diagnostics(diags) @@ -207,9 +182,9 @@ func (c *InitCommand) Run(args []string) int { switch { case initArgs.Cloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) case initArgs.Backend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + 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) @@ -317,7 +292,7 @@ func (c *InitCommand) Run(args []string) int { } // Now that we have loaded all modules, check the module tree for missing providers. - providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, flagPluginPath, initArgs.Lockfile, view) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { view.Diagnostics(diags) @@ -413,7 +388,7 @@ 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, viewType arguments.ViewType, view views.Init) (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() @@ -442,7 +417,7 @@ 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, viewType arguments.ViewType, view views.Init) (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() @@ -604,10 +579,10 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, view.Output(views.InitializingProviderPluginMessage) }, ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { - view.Log(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) + view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - view.Log(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) + view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -618,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 { - view.Log(views.ReusingPreviousVersionInfo, provider.ForDisplay()) + view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay()) } else { if len(versionConstraints) > 0 { - view.Log(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) + view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - view.Log(views.FindingLatestVersionMessage, provider.ForDisplay()) + view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay()) } } }, LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { - view.Log(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) + view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) }, FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { - view.Log(views.InstallingProviderMessage, provider.ForDisplay(), version) + view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -831,7 +806,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, keyID = view.PrepareMessage(views.KeyID, keyID) } - view.Log(views.InstalledProviderVersionInfo, 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 @@ -877,7 +852,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } } if thirdPartySigned { - view.Log(views.PartnerAndCommunityProvidersMessage) + view.LogInitMessage(views.PartnerAndCommunityProvidersMessage) } }, } @@ -983,7 +958,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // // 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 diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 62ad7f284674..feb884979345 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -19,6 +19,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" @@ -494,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 { 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 917d9d08a834..594560c1a1d0 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -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/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/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/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog index 642606d7704b..87f2d2534f73 100644 --- a/internal/command/testdata/init-get/output.jsonlog +++ b/internal/command/testdata/init-get/output.jsonlog @@ -3,6 +3,5 @@ {"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} {"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} {"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","type":"init_output"} -{"@level":"info","@message":"","@module":"terraform.ui","type":"init_output"} {"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","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","type":"init_output"} diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 97076b813bcf..81b5b416f3a9 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -16,9 +16,10 @@ import ( // The Init view is used for the init command. type Init interface { Diagnostics(diags tfdiags.Diagnostics) - Output(messageCode string, params ...any) - Log(messageCode string, params ...any) - PrepareMessage(messageCode string, params ...any) string + 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. @@ -49,19 +50,24 @@ func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } -func (v *InitHuman) Output(messageCode string, params ...any) { +func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { v.view.streams.Println(v.PrepareMessage(messageCode, params...)) } -func (v *InitHuman) Log(messageCode string, params ...any) { +func (v *InitHuman) LogInitMessage(messageCode InitMessageCode, params ...any) { v.view.streams.Println(v.PrepareMessage(messageCode, params...)) } -func (v *InitHuman) PrepareMessage(messageCode string, params ...any) string { +// 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 messageCode + return string(messageCode) } if message.HumanValue == "" { @@ -84,29 +90,46 @@ func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } -func (v *InitJSON) Output(messageCode string, params ...any) { - current_timestamp := time.Now().UTC().Format(time.RFC3339) +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": v.PrepareMessage(messageCode, params...), - "@module": "terraform.ui", - "@timestamp": current_timestamp, - "type": "init_output"} + "@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) Log(messageCode string, params ...any) { - v.view.Log(v.PrepareMessage(messageCode, params...)) +func (v *InitJSON) LogInitMessage(messageCode InitMessageCode, params ...any) { + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + v.view.Log(preppedMessage) } -func (v *InitJSON) PrepareMessage(messageCode string, params ...any) string { +// 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 messageCode + return string(messageCode) } return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) @@ -118,7 +141,7 @@ type InitMessage struct { JSONValue string } -var MessageRegistry map[string]InitMessage = map[string]InitMessage{ +var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMessage{ "copying_configuration_message": { HumanValue: "[reset][bold]Copying configuration[reset] from %q...", JSONValue: "Copying configuration from %q...", @@ -221,32 +244,34 @@ var MessageRegistry map[string]InitMessage = map[string]InitMessage{ }, } +type InitMessageCode string + const ( - CopyingConfigurationMessage string = "copying_configuration_message" - EmptyMessage string = "empty_message" - OutputInitEmptyMessage string = "output_init_empty_message" - OutputInitSuccessMessage string = "output_init_success_message" - OutputInitSuccessCloudMessage string = "output_init_success_cloud_message" - OutputInitSuccessCLIMessage string = "output_init_success_cli_message" - OutputInitSuccessCLICloudMessage string = "output_init_success_cli_cloud_message" - UpgradingModulesMessage string = "upgrading_modules_message" - InitializingTerraformCloudMessage string = "initializing_terraform_cloud_message" - InitializingModulesMessage string = "initializing_modules_message" - InitializingBackendMessage string = "initializing_backend_message" - InitializingProviderPluginMessage string = "initializing_provider_plugin_message" - LockInfo string = "lock_info" - DependenciesLockChangesInfo string = "dependencies_lock_changes_info" - ProviderAlreadyInstalledMessage string = "provider_already_installed_message" - BuiltInProviderAvailableMessage string = "built_in_provider_available_message" - ReusingPreviousVersionInfo string = "reusing_previous_version_info" - FindingMatchingVersionMessage string = "finding_matching_version_message" - FindingLatestVersionMessage string = "finding_latest_version_message" - UsingProviderFromCacheDirInfo string = "using_provider_from_cache_dir_info" - InstallingProviderMessage string = "installing_provider_message" - KeyID string = "key_id" - InstalledProviderVersionInfo string = "installed_provider_version_info" - PartnerAndCommunityProvidersMessage string = "partner_and_community_providers_message" - InitConfigError string = "init_config_error" + 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 = ` diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index fb84d6fad8c6..ae7e84c2caea 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -129,8 +129,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Output(messageCode) + newInit.Output(InitializingProviderPluginMessage) version := tfversion.String() want := []map[string]interface{}{ @@ -163,8 +162,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { } packageName := "hashicorp/aws" - messageCode := "finding_latest_version_message" - newInit.Output(messageCode, packageName) + newInit.Output(FindingLatestVersionMessage, packageName) version := tfversion.String() want := []map[string]interface{}{ @@ -197,8 +195,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { } var packageName, packageVersion = "hashicorp/aws", "3.0.0" - messageCode := "provider_already_installed_message" - newInit.Output(messageCode, packageName, packageVersion) + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) version := tfversion.String() want := []map[string]interface{}{ @@ -231,8 +228,7 @@ func TestNewInit_jsonViewLog(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Log(messageCode) + newInit.LogInitMessage(InitializingProviderPluginMessage) version := tfversion.String() want := []map[string]interface{}{ @@ -257,23 +253,6 @@ func TestNewInit_jsonViewLog(t *testing.T) { } func TestNewInit_jsonViewPrepareMessage(t *testing.T) { - t.Run("message code that does not exists", 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) - } - - messageCode := "Terraform has been successfully initialized!" - want := messageCode - - actual := newInit.PrepareMessage(messageCode) - if !cmp.Equal(want, actual) { - t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) - } - }) - t.Run("existing message code", func(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) @@ -282,10 +261,9 @@ func TestNewInit_jsonViewPrepareMessage(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_modules_message" want := "Initializing modules..." - actual := newInit.PrepareMessage(messageCode) + actual := newInit.PrepareMessage(InitializingModulesMessage) if !cmp.Equal(want, actual) { t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) } @@ -301,8 +279,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Output(messageCode) + newInit.Output(InitializingProviderPluginMessage) actual := done(t).All() expected := "Initializing provider plugins..." @@ -320,8 +297,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { } packageName := "hashicorp/aws" - messageCode := "finding_latest_version_message" - newInit.Output(messageCode, packageName) + newInit.Output(FindingLatestVersionMessage, packageName) actual := done(t).All() expected := "Finding latest version of hashicorp/aws" @@ -339,8 +315,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { } var packageName, packageVersion = "hashicorp/aws", "3.0.0" - messageCode := "provider_already_installed_message" - newInit.Output(messageCode, packageName, packageVersion) + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) actual := done(t).All() expected := "- Using previously-installed hashicorp/aws v3.0.0" From 7fb13b886819d0dd948eb169b0628405424e310e Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 16 Apr 2024 02:09:09 -0400 Subject: [PATCH 13/25] fixing tests --- internal/command/arguments/init_test.go | 114 ++++++++++++++++-- .../command/testdata/init-get/output.jsonlog | 10 +- internal/command/views/init_test.go | 27 +++-- 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 97f7c76a34c2..93e13b7b6281 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -9,9 +9,11 @@ import ( "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 @@ -34,13 +36,21 @@ func TestParseInit_basicValid(t *testing.T) { 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", + "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", "-ignore-remote-version=true", "-test-directory=./test-dir"}, &Init{ FromModule: "./main-dir", @@ -58,10 +68,19 @@ func TestParseInit_basicValid(t *testing.T) { 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"}, + []string{"-cloud=false", "-input=false", "-target=foo_bar.baz", "-backend-config", "backend.config"}, &Init{ FromModule: "", Lockfile: "", @@ -78,10 +97,21 @@ func TestParseInit_basicValid(t *testing.T) { 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) @@ -89,7 +119,7 @@ func TestParseInit_basicValid(t *testing.T) { t.Fatalf("unexpected diags: %v", diags) } - if diff := cmp.Diff(tc.want, got); diff != "" { + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { t.Errorf("unexpected result\n%s", diff) } }) @@ -98,16 +128,29 @@ func TestParseInit_basicValid(t *testing.T) { func TestParseInit_invalid(t *testing.T) { testCases := map[string]struct { - args []string - wantErr string + args []string + wantErr string + wantViewType ViewType }{ "with unsupported options": { - args: []string{"-raw"}, - wantErr: "flag provided but not defined", + 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", + 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, }, } @@ -120,9 +163,60 @@ func TestParseInit_invalid(t *testing.T) { 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 != ViewHuman { + 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/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog index 87f2d2534f73..88acf532fd07 100644 --- a/internal/command/testdata/init-get/output.jsonlog +++ b/internal/command/testdata/init-get/output.jsonlog @@ -1,7 +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","type":"init_output"} -{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} +{"@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","type":"init_output"} -{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","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","type":"init_output"} +{"@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/views/init_test.go b/internal/command/views/init_test.go index ae7e84c2caea..2f056cb0c9e2 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -142,10 +142,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": "Initializing provider plugins...", - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": "Initializing provider plugins...", + "message_code": "initializing_provider_plugin_message", + "@module": "terraform.ui", + "type": "init_output", }, } @@ -175,10 +176,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), + "@module": "terraform.ui", + "message_code": "finding_latest_version_message", + "type": "init_output", }, } @@ -208,10 +210,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), + "@module": "terraform.ui", + "message_code": "provider_already_installed_message", + "type": "init_output", }, } From a4415c030afc6444dfef2ac12958ea38b16fffa1 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 17 Apr 2024 09:25:52 -0400 Subject: [PATCH 14/25] making json data more useful for client consumption --- internal/command/views/init.go | 18 +++++++++--------- internal/command/views/init_test.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 81b5b416f3a9..15ff7f80126f 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -196,39 +196,39 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe }, "provider_already_installed_message": { HumanValue: "- Using previously-installed %s v%s", - JSONValue: "- 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", + JSONValue: "%s is built in to Terraform", }, "reusing_previous_version_info": { HumanValue: "- Reusing previous version of %s from the dependency lock file", - JSONValue: "- 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 %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: "- 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: "- 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 %s v%s...", + JSONValue: "Installing provider version: %s v%s...", }, "key_id": { HumanValue: ", key ID [reset][bold]%s[reset]", - JSONValue: ", key ID %s", + JSONValue: "key_id: %s", }, "installed_provider_version_info": { HumanValue: "- Installed %s v%s (%s%s)", - JSONValue: "- Installed %s v%s (%s%s)", + JSONValue: "Installed provider version: %s v%s (%s%s)", }, "partner_and_community_providers_message": { HumanValue: partnerAndCommunityProvidersInfo, diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 2f056cb0c9e2..5017d714772b 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -177,7 +177,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { }, { "@level": "info", - "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), + "@message": fmt.Sprintf("%s: Finding latest version...", packageName), "@module": "terraform.ui", "message_code": "finding_latest_version_message", "type": "init_output", @@ -211,7 +211,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { }, { "@level": "info", - "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), + "@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", From f1fb26e480daedd21c4dabdc962f15bc6843b1b6 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 17 Apr 2024 13:25:59 -0400 Subject: [PATCH 15/25] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d716cdfdabe2..97b369560111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 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)) BUG FIXES: @@ -19,7 +20,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 From 8e437daec86463c4fd28d498bd318c33ae8057f2 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 15 Apr 2024 11:55:41 -0700 Subject: [PATCH 16/25] states: Reject attempts to save values with non-sensitive marks The state serialization only knows how to save and restore the marks.Sensitive mark in particular, but previously it was just assuming that any mark it found was sensitive without actually checking. Now we'll return an error if we're asked to save a mark we don't support. In practice there are no other marks currently used by the modules runtime and so this cannot fail, but this is to help notice problems sooner if we introduce any new marks later. --- internal/states/instance_object.go | 35 ++++++++++++++- internal/states/instance_object_test.go | 24 ++++++++++ internal/states/statefile/version4.go | 20 ++++++++- internal/states/statefile/version4_test.go | 51 +++++++++++++++++++++- 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/internal/states/instance_object.go b/internal/states/instance_object.go index 0e12f6550e8e..3f12682247b0 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -4,12 +4,14 @@ 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" ) // ResourceInstanceObject is the local representation of a specific remote @@ -95,7 +97,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, pvm, 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 @@ -149,3 +154,31 @@ func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { ret.Status = ObjectTainted return ret } + +// unmarkValueForStorage takes a value that possibly contains marked values +// and returns an equal value without markings along with the separated mark +// metadata that should be stored alongside the value in another field. +// +// This function only accepts the marks that are valid to store, and so will +// return an error if other marks are present. Marks that this package doesn't +// know how to store must be dealt with somehow by a caller -- presumably by +// replacing each marked value with some sort of storage placeholder -- before +// writing a value into the state. +func unmarkValueForStorage(v cty.Value) (cty.Value, []cty.PathValueMarks, error) { + val, pvms := v.UnmarkDeepWithPaths() + var err error + +Pvms: + for _, pvm := range pvms { + for mark := range pvm.Marks { + // Currently "Sensitive" is the only mark that we know how to + // preserve between rounds in a state snapshot. + if mark != marks.Sensitive { + err = fmt.Errorf("cannot serialize value marked as %q for inclusion in a state snapshot (this is a bug in Terraform)", mark) + break Pvms + } + } + } + + return val, pvms, err +} diff --git a/internal/states/instance_object_test.go b/internal/states/instance_object_test.go index c5b893dade82..acb4c8c15b11 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 := `cannot serialize value marked as "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/statefile/version4.go b/internal/states/statefile/version4.go index f3b21c6028b1..9413f2fa945d 100644 --- a/internal/states/statefile/version4.go +++ b/internal/states/statefile/version4.go @@ -491,7 +491,25 @@ 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) + // It's a bug for AttrSensitivePaths to contain anything other than + // sensitive marks, because we don't know how to serialize anything + // else here. (The main "states" package should've previously rejected + // such marks, so this is here just for robustness.) + for mark := range vm.Marks { + if mark != marks.Sensitive { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unserializable value mark", + fmt.Sprintf( + "An attribute of %s has unserializable value mark %#v. This is a bug in Terraform.", + rs.Addr.Instance(key), mark, + ), + )) + } + } + if _, ok := vm.Marks[marks.Sensitive]; ok { + paths = append(paths, vm.Path) + } } // Marshal paths to JSON diff --git a/internal/states/statefile/version4_test.go b/internal/states/statefile/version4_test.go index fff2086aa705..a5d946c62689 100644 --- a/internal/states/statefile/version4_test.go +++ b/internal/states/statefile/version4_test.go @@ -8,8 +8,11 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // This test verifies that modules are sorted before resources: @@ -259,3 +262,49 @@ func TestVersion4_marshalPaths(t *testing.T) { }) } } + +func TestVersion4_unsupportedMarksInResourceObject(t *testing.T) { + // This tests that we reject attempts to serialize unsupported kinds + // of value marks as part of the AttrSensitivePaths collection, which + // is supposed to be filtered by a caller to include only marks.Sensitive + // in particular. + + _, diags := appendInstanceObjectStateV4( + &states.Resource{ + Addr: addrs.AbsResource{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "any_type", + Name: "any_name", + }, + }, + ProviderConfig: addrs.AbsProviderConfig{ + Provider: addrs.NewBuiltInProvider("test"), + }, + }, + &states.ResourceInstance{}, + addrs.NoKey, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"foo":"bar"}`), + AttrSensitivePaths: []cty.PathValueMarks{ + { + Marks: cty.ValueMarks{ + "unsupported": struct{}{}, + }, + Path: cty.GetAttrPath("foo"), + }, + }, + }, + addrs.NotDeposed, + nil, + ) + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + got := diags.Err().Error() + want := `Unserializable value mark: An attribute of any_type.any_name has unserializable value mark "unsupported". This is a bug in Terraform.` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } +} From 6be3ddf9e72ccabb4cacf62803e57d776ef7fd41 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 15 Apr 2024 13:46:53 -0700 Subject: [PATCH 17/25] planfile: Reject value marks that we cannot serialize The plan file format can only preserve the "sensitive" mark, but previously it was just silently treating any other mark as if it were the sensitive mark. Now we'll reject any other marks at serialization time. There are not currently any other marks used by the modules runtime and so in practice this cannot fail yet, but this is here to guard against misbehavior if we introduce new marks in future without considering whether and how they are to be serialized. (For any mark we choose not to serialize, it'll be the caller's responsibility to replace the values using unsupported marks with suitable unmarked placeholders.) --- internal/plans/planfile/tfplan.go | 9 +++++++++ internal/plans/planfile/tfplan_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index ca792ab6252e..539b81d65599 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform/internal/plans/planproto" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" ) @@ -860,6 +861,14 @@ func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([] func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) { ret := make([]*planproto.Path, 0, len(pvm)) for _, p := range pvm { + for mark := range p.Marks { + if mark != marks.Sensitive { + return nil, fmt.Errorf("%s: cannot serialize values marked as %#v (this is a bug in Terraform)", tfdiags.FormatCtyPath(p.Path), mark) + } + } + if _, ok := p.Marks[marks.Sensitive]; !ok { + continue + } path, err := pathToTfplan(p.Path) if err != nil { return nil, err diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 2a6cd1256d13..12bb8b3fad96 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -447,3 +447,28 @@ func TestTFPlanRoundTripDestroy(t *testing.T) { } } } + +func TestTFPlanEncodeUnsupportedMarks(t *testing.T) { + v := cty.ObjectVal(map[string]cty.Value{ + "beep": cty.StringVal("boop").Mark("unsupported"), + }) + change := &plans.Change{ + Action: plans.Create, + Before: cty.NullVal(v.Type()), + After: v, + } + changeSrc, err := change.Encode(v.Type()) + if err != nil { + t.Fatalf("failed to encode change for testing: %s", err) + } + + _, err = changeToTfplan(changeSrc) + if err == nil { + t.Fatalf("unexpected success; want error") + } + got := err.Error() + want := `.beep: cannot serialize values marked as "unsupported" (this is a bug in Terraform)` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } +} From ea60ca7d70156371b0d3e56eb7d1a84131d248af Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 15 Apr 2024 13:48:02 -0700 Subject: [PATCH 18/25] plans/planfile: Don't use deprecated "io/ioutil" package Everything from ioutil has been moved into other locations in modern Go. This particular package only used ioutil.ReadAll and ioutil.ReadFile, which have been replaced by io.ReadAll and os.ReadFile respectively. --- internal/plans/planfile/config_snapshot.go | 6 +++--- internal/plans/planfile/reader.go | 7 ++++--- internal/plans/planfile/tfplan.go | 3 +-- 3 files changed, 8 insertions(+), 8 deletions(-) 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 539b81d65599..08e5ed10a873 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" @@ -39,7 +38,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 } From 5a944133e623190fd45f9021a82a2421ad805ce2 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 15 Apr 2024 15:54:12 -0700 Subject: [PATCH 19/25] command/jsonstate: Reject unsupported marks Our JSON state format has support for sensitive marks in particular but lacks generalized support and so cannot deal with any other marks. Previously it just assumed that any mark must be the sensitive mark, but now we'll reject other marks to ensure that if any new marks are added we must consider whether and how the JSON state format should handle them. --- internal/command/jsonstate/state.go | 63 +++++++++++++++++++++--- internal/command/jsonstate/state_test.go | 53 +++++++++++++++++--- 2 files changed, 100 insertions(+), 16 deletions(-) diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index e7b1d07de53d..21ba8d8d12ad 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) (cty.Value, AttributeValues, []cty.PathValueMarks, error) { // unmark our value to show all values - value, _ = value.UnmarkDeep() + value, pvms, err := unmarkValueForMarshaling(value) + if err != nil { + return cty.NilVal, nil, nil, err + } if value == cty.NilVal || value.IsNull() { - return nil + return cty.NilVal, 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, pvms, nil } // newState() returns a minimally-initialized state @@ -397,9 +401,13 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module return nil, err } - current.AttributeValues = marshalAttributeValues(riObj.Value) + var value cty.Value + var marks []cty.PathValueMarks + value, current.AttributeValues, marks, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) + } - value, marks := riObj.Value.UnmarkDeepWithPaths() if schema.ContainsSensitive() { marks = append(marks, schema.ValueMarks(value, nil)...) } @@ -448,9 +456,13 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module return nil, err } - deposed.AttributeValues = marshalAttributeValues(riObj.Value) + var value cty.Value + var marks []cty.PathValueMarks + value, deposed.AttributeValues, marks, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) + } - value, marks := riObj.Value.UnmarkDeepWithPaths() if schema.ContainsSensitive() { marks = append(marks, schema.ValueMarks(value, nil)...) } @@ -552,3 +564,38 @@ 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) (cty.Value, []cty.PathValueMarks, error) { + val, pvms := v.UnmarkDeepWithPaths() + var err error + +Pvms: + for _, pvm := range pvms { + for mark := range pvm.Marks { + // Currently "Sensitive" is the only mark that we know how to + // represent in a JSON state result. + if mark != marks.Sensitive { + // We should not actually get here if the rest of Terraform is + // functioning correctly, because values with any other marks + // should have been replaced by unmarked placeholders long + // before we're thinking about JSON-serialized state output. + err = fmt.Errorf( + "%s: cannot serialize value marked as %q for inclusion in a state snapshot (this is a bug in Terraform)", + tfdiags.FormatCtyPath(pvm.Path), mark, + ) + break Pvms + } + } + } + + return val, pvms, err +} diff --git a/internal/command/jsonstate/state_test.go b/internal/command/jsonstate/state_test.go index bbb46b6ba9c5..da77f3f1bbbe 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 + WantMarks []cty.PathValueMarks }{ { 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,46 @@ func TestMarshalAttributeValues(t *testing.T) { "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, + []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("baz").IndexInt(1), + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, }, } 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, marks, 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(marks, test.WantMarks) { + t.Errorf("wrong marks\ngot: %#v\nwant: %#v\n", marks, test.WantMarks) + } + 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 "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) { From 5e7d0f08541b07b9b6b43649fdf24699bde609d6 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 17 Apr 2024 09:08:06 -0700 Subject: [PATCH 20/25] go.mod: Update to latest github.com/zclconf/go-cty-debug This extends the "CmpOptions" to include a comparer for cty.Path values, so that we don't have to hand-write that in each case where we're comparing data structures containing those. --- go.mod | 2 +- go.sum | 4 ++-- internal/backend/remote-state/azure/go.sum | 4 ++-- internal/backend/remote-state/consul/go.sum | 4 ++-- internal/backend/remote-state/cos/go.sum | 4 ++-- internal/backend/remote-state/gcs/go.sum | 4 ++-- internal/backend/remote-state/kubernetes/go.sum | 4 ++-- internal/backend/remote-state/oss/go.sum | 4 ++-- internal/backend/remote-state/pg/go.sum | 4 ++-- internal/backend/remote-state/s3/go.sum | 4 ++-- internal/legacy/go.sum | 4 ++-- 11 files changed, 21 insertions(+), 21 deletions(-) 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/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/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= From 2334fc867c628cee33845d52aba83140253735c0 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 16 Apr 2024 11:33:13 -0700 Subject: [PATCH 21/25] lang/marks: PathsWithMark helper function This helper deals with the somewhat-common situation where a particular codepath can handle only one kind of marking, such as in our various wire and file format serializers which have special support for serializing sensitive value paths but cannot preserve any other marks. The assumption here is that the caller will probably return some sort of error if the second return value is non-empty, but we leave thec caller to decide exactly what to do because different situations might warrant different treatment of the problem, such as whether it's considered to be a Terraform language authoring error or to be a bug in Terraform. --- internal/lang/marks/paths.go | 38 ++++++++++++++++++++ internal/lang/marks/paths_test.go | 58 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 internal/lang/marks/paths.go create mode 100644 internal/lang/marks/paths_test.go diff --git a/internal/lang/marks/paths.go b/internal/lang/marks/paths.go new file mode 100644 index 000000000000..affd23e23bc4 --- /dev/null +++ b/internal/lang/marks/paths.go @@ -0,0 +1,38 @@ +// 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 +} diff --git a/internal/lang/marks/paths_test.go b/internal/lang/marks/paths_test.go new file mode 100644 index 000000000000..c74a6fce5f44 --- /dev/null +++ b/internal/lang/marks/paths_test.go @@ -0,0 +1,58 @@ +// 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) + } +} From b5a4860a36fa5007acfb89e00c99af4f5488cf36 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 16 Apr 2024 13:24:34 -0700 Subject: [PATCH 22/25] lang/marks: MarkPaths helper function This helper is a slightly more convenient wrapper around cty's own "MarkWithPaths" function that applies a single given mark at zero or more paths in a given value. In several parts of Terraform we preserve sensitive marks in particular for serialization and cannot preserve anything else, and so this helper is useful for reapplying the saved sensitive paths back to the stored value during decoding. --- internal/lang/marks/paths.go | 24 ++++++++++++++ internal/lang/marks/paths_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/internal/lang/marks/paths.go b/internal/lang/marks/paths.go index affd23e23bc4..461ec1bb42d2 100644 --- a/internal/lang/marks/paths.go +++ b/internal/lang/marks/paths.go @@ -36,3 +36,27 @@ func PathsWithMark(pvms []cty.PathValueMarks, wantMark any) (withWanted []cty.Pa 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 index c74a6fce5f44..f5efd2829f95 100644 --- a/internal/lang/marks/paths_test.go +++ b/internal/lang/marks/paths_test.go @@ -56,3 +56,55 @@ func TestPathsWithMark(t *testing.T) { 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) + } +} From 30e2fd65250979c9cab936b8301a77303b7d7cbd Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 16 Apr 2024 17:20:33 -0700 Subject: [PATCH 23/25] Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular. --- internal/command/jsonformat/plan_test.go | 325 +++++------------- internal/command/jsonplan/plan.go | 17 +- internal/command/jsonstate/state.go | 60 ++-- internal/command/jsonstate/state_test.go | 31 +- internal/configs/configschema/marks.go | 70 ++-- internal/configs/configschema/marks_test.go | 5 +- internal/plans/changes.go | 54 ++- internal/plans/changes_src.go | 21 +- internal/plans/changes_test.go | 4 +- internal/plans/planfile/tfplan.go | 50 ++- internal/plans/planfile/tfplan_test.go | 40 +-- internal/rpcapi/stacks_inspector.go | 13 +- internal/rpcapi/terraform1/conversion.go | 20 +- internal/stacks/stackplan/planned_change.go | 18 +- internal/stacks/stackruntime/apply_test.go | 7 +- .../stackruntime/internal/stackeval/stack.go | 23 +- internal/stacks/stackruntime/plan_test.go | 50 +-- internal/stacks/stackstate/applied_change.go | 33 +- .../stacks/stackstate/applied_change_test.go | 16 +- internal/stacks/stackstate/from_proto.go | 9 +- internal/stacks/tfstackdata1/convert.go | 19 +- internal/states/instance_object.go | 28 +- internal/states/instance_object_src.go | 8 +- internal/states/instance_object_test.go | 2 +- internal/states/state_deepcopy.go | 4 +- internal/states/state_test.go | 8 +- internal/states/statefile/version4.go | 40 +-- internal/states/statefile/version4_test.go | 48 --- internal/terraform/context_apply2_test.go | 28 +- internal/terraform/context_apply_test.go | 77 ++--- internal/terraform/context_plan2_test.go | 38 +- internal/terraform/context_plan_test.go | 47 ++- internal/terraform/evaluate.go | 5 +- internal/terraform/evaluate_test.go | 32 +- .../node_resource_abstract_instance.go | 42 ++- .../node_resource_plan_partialexp.go | 7 +- 36 files changed, 492 insertions(+), 807 deletions(-) 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 21ba8d8d12ad..f77da0e036c9 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -116,15 +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) (cty.Value, AttributeValues, []cty.PathValueMarks, error) { +func marshalAttributeValues(value cty.Value) (unmarkedVal cty.Value, marshalledVals AttributeValues, sensitivePaths []cty.Path, err error) { // unmark our value to show all values - value, pvms, err := unmarkValueForMarshaling(value) + value, sensitivePaths, err = unmarkValueForMarshaling(value) if err != nil { return cty.NilVal, nil, nil, err } if value == cty.NilVal || value.IsNull() { - return cty.NilVal, nil, nil, nil + return value, nil, nil, nil } ret := make(AttributeValues) @@ -135,7 +135,7 @@ func marshalAttributeValues(value cty.Value) (cty.Value, AttributeValues, []cty. vJSON, _ := ctyjson.Marshal(v, v.Type()) ret[k.AsString()] = json.RawMessage(vJSON) } - return value, ret, pvms, nil + return value, ret, sensitivePaths, nil } // newState() returns a minimally-initialized state @@ -402,16 +402,13 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module } var value cty.Value - var marks []cty.PathValueMarks - value, current.AttributeValues, marks, err = marshalAttributeValues(riObj.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) } - - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(value, nil)...) - } - 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 @@ -457,16 +454,13 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module } var value cty.Value - var marks []cty.PathValueMarks - value, deposed.AttributeValues, marks, err = marshalAttributeValues(riObj.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) } - - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(value, nil)...) - } - 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 @@ -574,28 +568,14 @@ func SensitiveAsBool(val cty.Value) cty.Value { // 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) (cty.Value, []cty.PathValueMarks, error) { +func unmarkValueForMarshaling(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { val, pvms := v.UnmarkDeepWithPaths() - var err error - -Pvms: - for _, pvm := range pvms { - for mark := range pvm.Marks { - // Currently "Sensitive" is the only mark that we know how to - // represent in a JSON state result. - if mark != marks.Sensitive { - // We should not actually get here if the rest of Terraform is - // functioning correctly, because values with any other marks - // should have been replaced by unmarked placeholders long - // before we're thinking about JSON-serialized state output. - err = fmt.Errorf( - "%s: cannot serialize value marked as %q for inclusion in a state snapshot (this is a bug in Terraform)", - tfdiags.FormatCtyPath(pvm.Path), mark, - ) - break Pvms - } - } + 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, pvms, err + return val, sensitivePaths, err } diff --git a/internal/command/jsonstate/state_test.go b/internal/command/jsonstate/state_test.go index da77f3f1bbbe..2e7e8ed22b77 100644 --- a/internal/command/jsonstate/state_test.go +++ b/internal/command/jsonstate/state_test.go @@ -115,9 +115,9 @@ func TestMarshalOutputs(t *testing.T) { func TestMarshalAttributeValues(t *testing.T) { tests := []struct { - Attr cty.Value - Want AttributeValues - WantMarks []cty.PathValueMarks + Attr cty.Value + Want AttributeValues + WantSensitivePaths []cty.Path }{ { cty.NilVal, @@ -174,26 +174,23 @@ func TestMarshalAttributeValues(t *testing.T) { "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, - []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("baz").IndexInt(1), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + []cty.Path{ + cty.GetAttrPath("baz").IndexInt(1), }, }, } for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Attr), func(t *testing.T) { - val, got, marks, err := marshalAttributeValues(test.Attr) + 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(marks, test.WantMarks) { - t.Errorf("wrong marks\ngot: %#v\nwant: %#v\n", marks, test.WantMarks) + 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) @@ -209,7 +206,7 @@ func TestMarshalAttributeValues(t *testing.T) { t.Fatalf("unexpected success; want error") } got := err.Error() - want := `.disallowed: cannot serialize value marked as "unsupported" for inclusion in a state snapshot (this is a bug in Terraform)` + 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) } @@ -329,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"), }, }, }, @@ -595,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/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/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/tfplan.go b/internal/plans/planfile/tfplan.go index 08e5ed10a873..290cd10baf89 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -14,12 +14,10 @@ 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" "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" ) @@ -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,33 +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 { - for mark := range p.Marks { - if mark != marks.Sensitive { - return nil, fmt.Errorf("%s: cannot serialize values marked as %#v (this is a bug in Terraform)", tfdiags.FormatCtyPath(p.Path), mark) - } - } - if _, ok := p.Marks[marks.Sensitive]; !ok { - continue - } - 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 12bb8b3fad96..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), }, }, }, @@ -447,28 +440,3 @@ func TestTFPlanRoundTripDestroy(t *testing.T) { } } } - -func TestTFPlanEncodeUnsupportedMarks(t *testing.T) { - v := cty.ObjectVal(map[string]cty.Value{ - "beep": cty.StringVal("boop").Mark("unsupported"), - }) - change := &plans.Change{ - Action: plans.Create, - Before: cty.NullVal(v.Type()), - After: v, - } - changeSrc, err := change.Encode(v.Type()) - if err != nil { - t.Fatalf("failed to encode change for testing: %s", err) - } - - _, err = changeToTfplan(changeSrc) - if err == nil { - t.Fatalf("unexpected success; want error") - } - got := err.Error() - want := `.beep: cannot serialize values marked as "unsupported" (this is a bug in Terraform)` - if got != want { - t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) - } -} 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 3f12682247b0..aa8335821b2d 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -12,6 +12,7 @@ import ( "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 @@ -97,7 +98,7 @@ 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, err := unmarkValueForStorage(o.Value) + val, sensitivePaths, err := unmarkValueForStorage(o.Value) if err != nil { return nil, err } @@ -133,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, @@ -164,21 +165,14 @@ func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { // 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) (cty.Value, []cty.PathValueMarks, error) { +func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { val, pvms := v.UnmarkDeepWithPaths() - var err error - -Pvms: - for _, pvm := range pvms { - for mark := range pvm.Marks { - // Currently "Sensitive" is the only mark that we know how to - // preserve between rounds in a state snapshot. - if mark != marks.Sensitive { - err = fmt.Errorf("cannot serialize value marked as %q for inclusion in a state snapshot (this is a bug in Terraform)", mark) - break Pvms - } - } + 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, pvms, err + 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 acb4c8c15b11..5d9c68ac4fe2 100644 --- a/internal/states/instance_object_test.go +++ b/internal/states/instance_object_test.go @@ -103,7 +103,7 @@ func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) { t.Fatalf("unexpected success; want error") } got := err.Error() - want := `cannot serialize value marked as "unsupported" for inclusion in a state snapshot (this is a bug in Terraform)` + 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 9413f2fa945d..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,32 +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 { - // It's a bug for AttrSensitivePaths to contain anything other than - // sensitive marks, because we don't know how to serialize anything - // else here. (The main "states" package should've previously rejected - // such marks, so this is here just for robustness.) - for mark := range vm.Marks { - if mark != marks.Sensitive { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unserializable value mark", - fmt.Sprintf( - "An attribute of %s has unserializable value mark %#v. This is a bug in Terraform.", - rs.Addr.Instance(key), mark, - ), - )) - } - } - if _, ok := vm.Marks[marks.Sensitive]; ok { - 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{ @@ -843,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 a5d946c62689..f39f999719a9 100644 --- a/internal/states/statefile/version4_test.go +++ b/internal/states/statefile/version4_test.go @@ -10,8 +10,6 @@ import ( "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -262,49 +260,3 @@ func TestVersion4_marshalPaths(t *testing.T) { }) } } - -func TestVersion4_unsupportedMarksInResourceObject(t *testing.T) { - // This tests that we reject attempts to serialize unsupported kinds - // of value marks as part of the AttrSensitivePaths collection, which - // is supposed to be filtered by a caller to include only marks.Sensitive - // in particular. - - _, diags := appendInstanceObjectStateV4( - &states.Resource{ - Addr: addrs.AbsResource{ - Resource: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "any_type", - Name: "any_name", - }, - }, - ProviderConfig: addrs.AbsProviderConfig{ - Provider: addrs.NewBuiltInProvider("test"), - }, - }, - &states.ResourceInstance{}, - addrs.NoKey, - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsJSON: []byte(`{"foo":"bar"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Marks: cty.ValueMarks{ - "unsupported": struct{}{}, - }, - Path: cty.GetAttrPath("foo"), - }, - }, - }, - addrs.NotDeposed, - nil, - ) - if !diags.HasErrors() { - t.Fatalf("unexpected success; want error") - } - got := diags.Err().Error() - want := `Unserializable value mark: An attribute of any_type.any_name has unserializable value mark "unsupported". This is a bug in Terraform.` - if got != want { - t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) - } -} 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_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.go b/internal/terraform/evaluate.go index 235a7f497473..dc76b9988d0a 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -730,7 +730,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) + // 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) continue } diff --git a/internal/terraform/evaluate_test.go b/internal/terraform/evaluate_test.go index ce7446d15379..c929e7c7e2fd 100644 --- a/internal/terraform/evaluate_test.go +++ b/internal/terraform/evaluate_test.go @@ -236,31 +236,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 From 6963c54bec7336200e1e35ebd3a0a6db1280e9b1 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Thu, 18 Apr 2024 18:58:19 +0200 Subject: [PATCH 24/25] terraform test: don't remove sensitive marks from inputs (#35021) --- internal/backend/local/test.go | 26 +- internal/command/test_test.go | 259 ++++++++---------- .../test/sensitive_input_values/main.tf | 5 + 3 files changed, 116 insertions(+), 174 deletions(-) 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/command/test_test.go b/internal/command/test_test.go index 95795fb234ab..cff14768e2bf 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -284,7 +284,8 @@ 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 { @@ -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) @@ -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) @@ -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) @@ -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,48 +1737,50 @@ 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) } - expected := `Initializing the backend... -Initializing modules... -- test.main.setup in setup -Initializing provider plugins... + expected := `main.tftest.hcl... in progress + run "setup"... pass + -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. +Outputs: + +password = (sensitive value) -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. -main.tftest.hcl... in progress - run "setup"... pass 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 @@ -1914,58 +1958,29 @@ 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) } - 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. -main.tftest.hcl... in progress + expected := `main.tftest.hcl... in progress run "setup"... pass Warning: Invalid override target @@ -2044,57 +2059,29 @@ 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) } - expected := `Initializing the backend... -Initializing modules... -- 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. -main.tftest.hcl... in progress + expected := `main.tftest.hcl... in progress run "setup"... pass run "main"... pass main.tftest.hcl... tearing down @@ -2140,55 +2127,29 @@ 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) } - expectedOut := `Initializing the backend... -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. -missing_run_block.tftest.hcl... in progress + expectedOut := `missing_run_block.tftest.hcl... in progress run "main"... fail missing_run_block.tftest.hcl... tearing down missing_run_block.tftest.hcl... fail 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 From 2b6af4a722891d0509397586bc2c577af0cfde66 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Thu, 18 Apr 2024 18:59:28 +0200 Subject: [PATCH 25/25] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b369560111..4c8968107199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 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: