Merge pull request #26183 from hashicorp/pselle/sensitive-values

Add sensitive attribute to variables
This commit is contained in:
Pam Selle 2020-09-11 11:24:18 -04:00 committed by GitHub
commit 6a126df0c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 363 additions and 30 deletions

View File

@ -302,6 +302,10 @@ func compactValueStr(val cty.Value) string {
// helpful but concise messages in diagnostics. It is not comprehensive
// nor intended to be used for other purposes.
if val.ContainsMarked() {
return "(sensitive value)"
}
ty := val.Type()
switch {
case val.IsNull():

View File

@ -125,10 +125,18 @@ func ResourceChange(
changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema)
changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema)
// Now that the change is decoded, add back the marks at the defined paths
if len(change.BeforeValMarks) > 0 {
changeV.Change.Before = changeV.Change.Before.MarkWithPaths(change.BeforeValMarks)
}
if len(change.AfterValMarks) > 0 {
changeV.Change.After = changeV.Change.After.MarkWithPaths(change.AfterValMarks)
}
result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
if result.bodyWritten {
p.buf.WriteString("\n")
p.buf.WriteString(strings.Repeat(" ", 4))
buf.WriteString("\n")
buf.WriteString(strings.Repeat(" ", 4))
}
buf.WriteString("}\n")
@ -293,10 +301,18 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol
return result
}
func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
path = append(path, cty.GetAttrStep{Name: name})
showJustNew := false
// getPlanActionAndShow returns the action value
// and a boolean for showJustNew. In this function we
// modify the old and new values to remove any possible marks
func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
var action plans.Action
showJustNew := false
if old.ContainsMarked() {
old, _ = old.UnmarkDeep()
}
if new.ContainsMarked() {
new, _ = new.UnmarkDeep()
}
switch {
case old.IsNull():
action = plans.Create
@ -309,6 +325,12 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At
default:
action = plans.Update
}
return action, showJustNew
}
func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
path = append(path, cty.GetAttrStep{Name: name})
action, showJustNew := getPlanActionAndShow(old, new)
if action == plans.NoOp && p.concise && !identifyingAttribute(name, attrS) {
return true
@ -586,6 +608,12 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string,
}
func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
// Could check specifically for the sensitivity marker
if val.IsMarked() {
p.buf.WriteString("(sensitive)")
return
}
if !val.IsKnown() {
p.buf.WriteString("(known after apply)")
return
@ -739,6 +767,12 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
ty := old.Type()
typesEqual := ctyTypesEqual(ty, new.Type())
// If either the old or new value is marked, don't display the value
if old.ContainsMarked() || new.ContainsMarked() {
p.buf.WriteString("(sensitive)")
return
}
// We have some specialized diff implementations for certain complex
// values where it's useful to see a visualization of the diff of
// the nested elements rather than just showing the entire old and
@ -1284,7 +1318,8 @@ func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
// This allows us to avoid spurious diffs
// until we introduce null to the SDK.
attrValue := val.GetAttr(name)
if ctyEmptyString(attrValue) {
// If the value is marked, the ctyEmptyString function will fail
if !val.ContainsMarked() && ctyEmptyString(attrValue) {
return cty.NullVal(attrType)
}

View File

@ -3622,10 +3622,75 @@ func TestResourceChange_nestedMap(t *testing.T) {
runTestCases(t, testCases)
}
func TestResourceChange_sensitiveVariable(t *testing.T) {
testCases := map[string]testCase{
"in-place update - creation": {
Action: plans.Update,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-AFTER"),
"root_block_device": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"volume_type": cty.StringVal("gp2"),
}),
}),
}),
AfterValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"root_block_device": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"volume_type": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
Nesting: configschema.NestingMap,
},
},
},
ExpectedOutput: ` # test_instance.example will be updated in-place
~ resource "test_instance" "example" {
~ ami = (sensitive)
id = "i-02ae66f368e8518a9"
+ root_block_device "a" {
+ volume_type = "gp2"
}
}
`,
},
}
runTestCases(t, testCases)
}
type testCase struct {
Action plans.Action
Mode addrs.ResourceMode
Before cty.Value
BeforeValMarks []cty.PathValueMarks
AfterValMarks []cty.PathValueMarks
After cty.Value
Schema *configschema.Block
RequiredReplace cty.PathSet
@ -3679,9 +3744,11 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: tc.Action,
Before: before,
After: after,
Action: tc.Action,
Before: before,
After: after,
BeforeValMarks: tc.BeforeValMarks,
AfterValMarks: tc.AfterValMarks,
},
RequiredReplace: tc.RequiredReplace,
}

View File

@ -138,6 +138,17 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
}
}
*/
if !m.ActiveExperiments.Has(experiments.SensitiveVariables) {
for _, v := range m.Variables {
if v.Sensitive {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Variable sensitivity is experimental",
Detail: "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding sensitive_variables to the list of active experiments.",
Subject: v.DeclRange.Ptr(),
})
}
}
}
return diags
}

View File

@ -25,6 +25,7 @@ type Variable struct {
Type cty.Type
ParsingMode VariableParsingMode
Validations []*VariableValidation
Sensitive bool
DescriptionSet bool
@ -94,6 +95,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
v.ParsingMode = parseMode
}
if attr, exists := content.Attributes["sensitive"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
diags = append(diags, valDiags...)
}
if attr, exists := content.Attributes["default"]; exists {
val, valDiags := attr.Expr.Value(nil)
diags = append(diags, valDiags...)
@ -534,6 +540,9 @@ var variableBlockSchema = &hcl.BodySchema{
{
Name: "type",
},
{
Name: "sensitive",
},
},
Blocks: []hcl.BlockHeaderSchema{
{

View File

@ -0,0 +1,7 @@
terraform {
experiments = [sensitive_variables] # WARNING: Experimental feature "sensitive_variables" is active
}
variable "sensitive-value" {
sensitive = true
}

View File

@ -14,12 +14,14 @@ type Experiment string
// identifier so that it can be specified in configuration.
const (
VariableValidation = Experiment("variable_validation")
SensitiveVariables = Experiment("sensitive_variables")
)
func init() {
// Each experiment constant defined above must be registered here as either
// a current or a concluded experiment.
registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.")
registerCurrentExperiment(SensitiveVariables)
}
// GetCurrent takes an experiment name and returns the experiment value

2
go.mod
View File

@ -123,7 +123,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v1.6.1
github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3
github.com/zclconf/go-cty-yaml v1.0.2
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect

2
go.sum
View File

@ -503,6 +503,8 @@ github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.6.1 h1:wHtZ+LSSQVwUSb+XIJ5E9hgAQxyWATZsAWT+ESJ9dQ0=
github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3 h1:iGouBJrrvGf/H4L6a2n7YBCO0FDhq81FEHI4ILDphkw=
github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@ -337,18 +337,33 @@ 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) {
beforeDV, err := NewDynamicValue(c.Before, ty)
// 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()
}
beforeDV, err := NewDynamicValue(unmarkedBefore, ty)
if err != nil {
return nil, err
}
afterDV, err := NewDynamicValue(c.After, ty)
if c.After.ContainsMarked() {
unmarkedAfter, afterVM = c.After.UnmarkDeepWithPaths()
}
afterDV, err := NewDynamicValue(unmarkedAfter, ty)
if err != nil {
return nil, err
}
return &ChangeSrc{
Action: c.Action,
Before: beforeDV,
After: afterDV,
Action: c.Action,
Before: beforeDV,
After: afterDV,
BeforeValMarks: beforeVM,
AfterValMarks: afterVM,
}, nil
}

View File

@ -156,6 +156,13 @@ type ChangeSrc struct {
// but have not yet been decoded from the serialized value used for
// 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
}
// Decode unmarshals the raw representations of the before and after values

View File

@ -51,7 +51,17 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
actualV := actual.GetAttr(name)
path := append(path, cty.GetAttrStep{Name: name})
moreErrs := assertValueCompatible(plannedV, actualV, path)
// If our value is marked, unmark it here before
// checking value assertions
unmarkedActualV := actualV
if actualV.ContainsMarked() {
unmarkedActualV, _ = actualV.UnmarkDeep()
}
unmarkedPlannedV := plannedV
if plannedV.ContainsMarked() {
unmarkedPlannedV, _ = actualV.UnmarkDeep()
}
moreErrs := assertValueCompatible(unmarkedPlannedV, unmarkedActualV, path)
if attrS.Sensitive {
if len(moreErrs) > 0 {
// Use a vague placeholder message instead, to avoid disclosing

View File

@ -98,7 +98,12 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
// and raise an error about that.
val := cty.UnknownAsNull(o.Value)
src, err := ctyjson.Marshal(val, ty)
// If it contains marks, dump those now
unmarked := val
if val.ContainsMarked() {
unmarked, _ = val.UnmarkDeep()
}
src, err := ctyjson.Marshal(unmarked, ty)
if err != nil {
return nil, err
}

View File

@ -503,7 +503,6 @@ Note that the -target option is not suitable for routine use, and is provided on
func (c *Context) Plan() (*plans.Plan, tfdiags.Diagnostics) {
defer c.acquireRun("plan")()
c.changes = plans.NewChanges()
var diags tfdiags.Diagnostics
if len(c.targets) > 0 {
@ -575,6 +574,7 @@ The -target option is not for routine use, and is provided only for exceptional
diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags)
if walkDiags.HasErrors() {
fmt.Println("walkerr")
return nil, diags
}
p.Changes = c.changes

View File

@ -5626,6 +5626,61 @@ resource "aws_instance" "foo" {
}
}
func TestContext2Plan_variableSensitivity(t *testing.T) {
m := testModule(t, "plan-variable-sensitivity")
p := testProvider("aws")
p.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) (resp providers.ValidateResourceTypeConfigResponse) {
foo := req.Config.GetAttr("foo").AsString()
if foo == "bar" {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("foo cannot be bar"))
}
return
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
resp.PlannedState = req.ProposedNewState
return
}
ctx := testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan()
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetSchemaReturn.ResourceTypes["aws_instance"]
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
if res.Action != plans.Create {
t.Fatalf("expected resource creation, got %s", res.Action)
}
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
switch i := ric.Addr.String(); i {
case "aws_instance.foo":
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"foo": cty.StringVal("foo"),
}), ric.After)
default:
t.Fatal("unknown instance:", i)
}
}
}
func checkVals(t *testing.T, expected, got cty.Value) {
t.Helper()
if !cmp.Equal(expected, got, valueComparer, typeComparer, equateEmpty) {

View File

@ -104,11 +104,24 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
}
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action)
// If our config or After value contain any marked values,
// ensure those are stripped out before sending
// this to the provider
unmarkedConfigVal := configVal
if configVal.ContainsMarked() {
unmarkedConfigVal, _ = configVal.UnmarkDeep()
}
unmarkedAfter := change.After
if change.After.ContainsMarked() {
unmarkedAfter, _ = change.After.UnmarkDeep()
}
resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
TypeName: n.Addr.Resource.Type,
PriorState: change.Before,
Config: configVal,
PlannedState: change.After,
Config: unmarkedConfigVal,
PlannedState: unmarkedAfter,
PlannedPrivate: change.Private,
ProviderMeta: metaConfigVal,
})

View File

@ -141,6 +141,17 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
return nil, diags.Err()
}
// Create an unmarked version of our config val, defaulting
// to the configVal so we don't do the work of unmarking unless
// necessary
unmarkedConfigVal := configVal
var unmarkedPaths []cty.PathValueMarks
if configVal.ContainsMarked() {
// store the marked values so we can re-mark them later after
// we've sent things over the wire.
unmarkedConfigVal, unmarkedPaths = configVal.UnmarkDeepWithPaths()
}
metaConfigVal := cty.NullVal(cty.DynamicPseudoType)
if n.ProviderMetas != nil {
if m, ok := n.ProviderMetas[n.ProviderAddr.Provider]; ok && m != nil {
@ -184,7 +195,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
priorVal = cty.NullVal(schema.ImpliedType())
}
proposedNewVal := objchange.ProposedNewObject(schema, priorVal, configVal)
proposedNewVal := objchange.ProposedNewObject(schema, priorVal, unmarkedConfigVal)
// Call pre-diff hook
if !n.Stub {
@ -203,7 +214,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
validateResp := provider.ValidateResourceTypeConfig(
providers.ValidateResourceTypeConfigRequest{
TypeName: n.Addr.Resource.Type,
Config: configVal,
Config: unmarkedConfigVal,
},
)
if validateResp.Diagnostics.HasErrors() {
@ -223,7 +234,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: n.Addr.Resource.Type,
Config: configVal,
Config: unmarkedConfigVal,
PriorState: priorVal,
ProposedNewState: proposedNewVal,
PriorPrivate: priorPrivate,
@ -244,6 +255,11 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String()))
}
// Add the marks back to the planned new value
if configVal.ContainsMarked() {
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
}
// We allow the planned new value to disagree with configuration _values_
// here, since that allows the provider to do special logic like a
// DiffSuppressFunc, but we still require that the provider produces
@ -480,7 +496,10 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
Change: plans.Change{
Action: action,
Before: priorVal,
After: plannedNewVal,
// Pass the marked planned value through in our change
// to propogate through evaluation.
// Marks will be removed when encoding.
After: plannedNewVal,
},
RequiredReplace: reqRep,
}

View File

@ -48,6 +48,14 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.V
forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
diags = diags.Append(forEachDiags)
if forEachVal.ContainsMarked() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: "Sensitive variable, or values derived from sensitive variables, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
Subject: expr.Range().Ptr(),
})
}
if diags.HasErrors() {
return nullMap, diags
}

View File

@ -292,6 +292,10 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
val = cty.UnknownVal(wantType)
}
if config.Sensitive {
val = val.Mark("sensitive")
}
return val, diags
}

View File

@ -1,9 +1,9 @@
variable "foo" {}
resource "aws_instance" "foo" {
ami = "${var.foo}"
ami = var.foo
lifecycle {
ignore_changes = ["ami"]
ignore_changes = [ami]
}
}

View File

@ -0,0 +1,12 @@
terraform {
experiments = [sensitive_variables]
}
variable "sensitive_var" {
default = "foo"
sensitive = true
}
resource "aws_instance" "foo" {
foo = var.sensitive_var
}

View File

@ -10,7 +10,7 @@ import (
func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error {
if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized")
return path.NewErrorf("value has marks, so it cannot be serialized as JSON")
}
// If we're going to decode as DynamicPseudoType then we need to save

View File

@ -67,6 +67,23 @@ func (m ValueMarks) GoString() string {
return s.String()
}
// PathValueMarks is a structure that enables tracking marks
// and the paths where they are located in one type
type PathValueMarks struct {
Path Path
Marks ValueMarks
}
func (p PathValueMarks) Equal(o PathValueMarks) bool {
if !p.Path.Equals(o.Path) {
return false
}
if !p.Marks.Equal(o.Marks) {
return false
}
return true
}
// IsMarked returns true if and only if the receiving value carries at least
// one mark. A marked value cannot be used directly with integration methods
// without explicitly unmarking it (and retrieving the markings) first.
@ -174,6 +191,21 @@ func (val Value) Mark(mark interface{}) Value {
}
}
// MarkWithPaths accepts a slice of PathValueMarks to apply
// markers to particular paths and returns the marked
// Value.
func (val Value) MarkWithPaths(pvm []PathValueMarks) Value {
ret, _ := Transform(val, func(p Path, v Value) (Value, error) {
for _, path := range pvm {
if p.Equals(path.Path) {
return v.WithMarks(path.Marks), nil
}
}
return v, nil
})
return ret
}
// Unmark separates the marks of the receiving value from the value itself,
// removing a new unmarked value and a map (representing a set) of the marks.
//
@ -209,6 +241,22 @@ func (val Value) UnmarkDeep() (Value, ValueMarks) {
return ret, marks
}
// UnmarkDeepWithPaths is like UnmarkDeep, except it returns a slice
// of PathValueMarks rather than a superset of all marks. This allows
// a caller to know which marks are associated with which paths
// in the Value.
func (val Value) UnmarkDeepWithPaths() (Value, []PathValueMarks) {
var marks []PathValueMarks
ret, _ := Transform(val, func(p Path, v Value) (Value, error) {
unmarkedV, valueMarks := v.Unmark()
if v.IsMarked() {
marks = append(marks, PathValueMarks{p, valueMarks})
}
return unmarkedV, nil
})
return ret, marks
}
func (val Value) unmarkForce() Value {
unw, _ := val.Unmark()
return unw

View File

@ -43,7 +43,7 @@ func Marshal(val cty.Value, ty cty.Type) ([]byte, error) {
func marshal(val cty.Value, ty cty.Type, path cty.Path, enc *msgpack.Encoder) error {
if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized")
return path.NewErrorf("value has marks, so it cannot be serialized")
}
// If we're going to decode as DynamicPseudoType then we need to save

2
vendor/modules.txt vendored
View File

@ -605,7 +605,7 @@ github.com/xanzy/ssh-agent
# github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
## explicit
github.com/xlab/treeprint
# github.com/zclconf/go-cty v1.6.1
# github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3
## explicit
github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/convert