Skip to content

Commit a2d0f79

Browse files
authored
Merge pull request #35008 from hashicorp/backport/liamcervante/34992/curiously-beloved-fowl
Backport of Fix crash when generating config for a resource with complex sensitive attributes into v1.8
2 parents 044b95c + 68b7424 commit a2d0f79

File tree

3 files changed

+143
-60
lines changed

3 files changed

+143
-60
lines changed

internal/genconfig/generate_config.go

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance,
3838
buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact()))
3939
}
4040

41-
stateVal = omitUnknowns(stateVal)
4241
if stateVal.RawEquals(cty.NilVal) {
4342
diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2))
4443
diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2))
@@ -151,11 +150,17 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
151150
val = attrS.EmptyValue()
152151
}
153152
if val.Type() == cty.String {
153+
// Before we inspect the string, take off any marks.
154+
unmarked, marks := val.Unmark()
155+
154156
// SHAMELESS HACK: If we have "" for an optional value, assume
155157
// it is actually null, due to the legacy SDK.
156-
if !val.IsNull() && attrS.Optional && len(val.AsString()) == 0 {
157-
val = attrS.EmptyValue()
158+
if !unmarked.IsNull() && attrS.Optional && len(unmarked.AsString()) == 0 {
159+
unmarked = attrS.EmptyValue()
158160
}
161+
162+
// Before we carry on, add the marks back.
163+
val = unmarked.WithMarks(marks)
159164
}
160165
if attrS.Sensitive || val.IsMarked() {
161166
buf.WriteString("null # sensitive")
@@ -567,59 +572,3 @@ func ctyCollectionValues(val cty.Value) []cty.Value {
567572

568573
return ret
569574
}
570-
571-
// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value,
572-
// omitting any unknowns.
573-
//
574-
// The result also normalizes some types: all sequence types are turned into
575-
// tuple types and all mapping types are converted to object types, since we
576-
// assume the result of this is just going to be serialized as JSON (and thus
577-
// lose those distinctions) anyway.
578-
func omitUnknowns(val cty.Value) cty.Value {
579-
ty := val.Type()
580-
switch {
581-
case val.IsNull():
582-
return val
583-
case !val.IsKnown():
584-
return cty.NilVal
585-
case ty.IsPrimitiveType():
586-
return val
587-
case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
588-
var vals []cty.Value
589-
it := val.ElementIterator()
590-
for it.Next() {
591-
_, v := it.Element()
592-
newVal := omitUnknowns(v)
593-
if newVal != cty.NilVal {
594-
vals = append(vals, newVal)
595-
} else if newVal == cty.NilVal {
596-
// element order is how we correlate unknownness, so we must
597-
// replace unknowns with nulls
598-
vals = append(vals, cty.NullVal(v.Type()))
599-
}
600-
}
601-
// We use tuple types always here, because the work we did above
602-
// may have caused the individual elements to have different types,
603-
// and we're doing this work to produce JSON anyway and JSON marshalling
604-
// represents all of these sequence types as an array.
605-
return cty.TupleVal(vals)
606-
case ty.IsMapType() || ty.IsObjectType():
607-
vals := make(map[string]cty.Value)
608-
it := val.ElementIterator()
609-
for it.Next() {
610-
k, v := it.Element()
611-
newVal := omitUnknowns(v)
612-
if newVal != cty.NilVal {
613-
vals[k.AsString()] = newVal
614-
}
615-
}
616-
// We use object types always here, because the work we did above
617-
// may have caused the individual elements to have different types,
618-
// and we're doing this work to produce JSON anyway and JSON marshalling
619-
// represents both of these mapping types as an object.
620-
return cty.ObjectVal(vals)
621-
default:
622-
// Should never happen, since the above should cover all types
623-
panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
624-
}
625-
}

internal/genconfig/generate_config_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/hashicorp/terraform/internal/addrs"
1414
"github.com/hashicorp/terraform/internal/configs/configschema"
15+
"github.com/hashicorp/terraform/internal/lang/marks"
1516
)
1617

1718
func TestConfigGeneration(t *testing.T) {
@@ -536,6 +537,67 @@ resource "tfcoremock_simple_resource" "empty" {
536537
expected: `
537538
resource "tfcoremock_simple_resource" "empty" {
538539
value = "[\"Hello\", \"World\""
540+
}`,
541+
},
542+
// Just try all the simple values with sensitive marks.
543+
"sensitive_values": {
544+
schema: &configschema.Block{
545+
Attributes: map[string]*configschema.Attribute{
546+
"string": sensitiveAttribute(cty.String),
547+
"empty_string": sensitiveAttribute(cty.String),
548+
"number": sensitiveAttribute(cty.Number),
549+
"bool": sensitiveAttribute(cty.Bool),
550+
"object": sensitiveAttribute(cty.Object(map[string]cty.Type{
551+
"nested": cty.String,
552+
})),
553+
"list": sensitiveAttribute(cty.List(cty.String)),
554+
"map": sensitiveAttribute(cty.Map(cty.String)),
555+
"set": sensitiveAttribute(cty.Set(cty.String)),
556+
},
557+
},
558+
addr: addrs.AbsResourceInstance{
559+
Module: addrs.RootModuleInstance,
560+
Resource: addrs.ResourceInstance{
561+
Resource: addrs.Resource{
562+
Mode: addrs.ManagedResourceMode,
563+
Type: "tfcoremock_sensitive_values",
564+
Name: "values",
565+
},
566+
Key: addrs.NoKey,
567+
},
568+
},
569+
provider: addrs.LocalProviderConfig{
570+
LocalName: "tfcoremock",
571+
},
572+
value: cty.ObjectVal(map[string]cty.Value{
573+
// Values that are sensitive will now be marked as such
574+
"string": cty.StringVal("Hello, world!").Mark(marks.Sensitive),
575+
"empty_string": cty.StringVal("").Mark(marks.Sensitive),
576+
"number": cty.NumberIntVal(42).Mark(marks.Sensitive),
577+
"bool": cty.True.Mark(marks.Sensitive),
578+
"object": cty.ObjectVal(map[string]cty.Value{
579+
"nested": cty.StringVal("Hello, solar system!"),
580+
}).Mark(marks.Sensitive),
581+
"list": cty.ListVal([]cty.Value{
582+
cty.StringVal("Hello, world!"),
583+
}).Mark(marks.Sensitive),
584+
"map": cty.MapVal(map[string]cty.Value{
585+
"key": cty.StringVal("Hello, world!"),
586+
}).Mark(marks.Sensitive),
587+
"set": cty.SetVal([]cty.Value{
588+
cty.StringVal("Hello, world!"),
589+
}).Mark(marks.Sensitive),
590+
}),
591+
expected: `
592+
resource "tfcoremock_sensitive_values" "values" {
593+
bool = null # sensitive
594+
empty_string = null # sensitive
595+
list = null # sensitive
596+
map = null # sensitive
597+
number = null # sensitive
598+
object = null # sensitive
599+
set = null # sensitive
600+
string = null # sensitive
539601
}`,
540602
},
541603
}
@@ -558,3 +620,11 @@ resource "tfcoremock_simple_resource" "empty" {
558620
})
559621
}
560622
}
623+
624+
func sensitiveAttribute(t cty.Type) *configschema.Attribute {
625+
return &configschema.Attribute{
626+
Type: t,
627+
Optional: true,
628+
Sensitive: true,
629+
}
630+
}

internal/terraform/context_plan_import_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hashicorp/terraform/internal/configs/configschema"
1616
"github.com/hashicorp/terraform/internal/plans"
1717
"github.com/hashicorp/terraform/internal/providers"
18+
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
1819
"github.com/hashicorp/terraform/internal/states"
1920
"github.com/hashicorp/terraform/internal/tfdiags"
2021
)
@@ -1413,7 +1414,7 @@ func TestContext2Plan_importGenerateNone(t *testing.T) {
14131414
import {
14141415
for_each = []
14151416
to = test_object.a
1416-
id = "123"
1417+
id = "81ba7c97"
14171418
}
14181419
`,
14191420
})
@@ -1437,3 +1438,66 @@ import {
14371438
t.Fatal("expected no resource changes")
14381439
}
14391440
}
1441+
1442+
// This is a test for the issue raised in #34992
1443+
func TestContext2Plan_importWithSensitives(t *testing.T) {
1444+
m := testModuleInline(t, map[string]string{
1445+
"main.tf": `
1446+
import {
1447+
to = test_object.a
1448+
id = "123"
1449+
}
1450+
`,
1451+
})
1452+
1453+
p := &testing_provider.MockProvider{
1454+
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
1455+
ResourceTypes: map[string]providers.Schema{
1456+
"test_object": {
1457+
Block: &configschema.Block{
1458+
Attributes: map[string]*configschema.Attribute{
1459+
"sensitive_string": {
1460+
Type: cty.String,
1461+
Sensitive: true,
1462+
Optional: true,
1463+
},
1464+
"sensitive_list": {
1465+
Type: cty.List(cty.String),
1466+
Sensitive: true,
1467+
Optional: true,
1468+
},
1469+
},
1470+
},
1471+
},
1472+
},
1473+
},
1474+
ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
1475+
return providers.ImportResourceStateResponse{
1476+
ImportedResources: []providers.ImportedResource{
1477+
{
1478+
TypeName: "test_object",
1479+
State: cty.ObjectVal(map[string]cty.Value{
1480+
"sensitive_string": cty.StringVal("sensitive"),
1481+
"sensitive_list": cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
1482+
}),
1483+
},
1484+
},
1485+
}
1486+
},
1487+
}
1488+
1489+
ctx := testContext2(t, &ContextOpts{
1490+
Providers: map[addrs.Provider]providers.Factory{
1491+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
1492+
},
1493+
})
1494+
1495+
// Just don't crash!
1496+
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
1497+
Mode: plans.NormalMode,
1498+
GenerateConfigPath: "generated.tf",
1499+
})
1500+
if diags.HasErrors() {
1501+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
1502+
}
1503+
}

0 commit comments

Comments
 (0)