mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
terraform: Support sensitive input variable values
The stacks runtime interacts directly with the modules runtime's planning operations, rather than through the usual CLI paths. As a result, root module input variable values can be marked as sensitive upon entry. This commit adds support for such marked values to the modules runtime and the `plans.Plan` type. This is sufficient for stacks, which does not use the planfile serialization, but we may in future choose to serialize these decoded marks also.
This commit is contained in:
parent
e4b319401e
commit
fc75657113
5 changed files with 192 additions and 1 deletions
|
|
@ -42,6 +42,7 @@ type Plan struct {
|
|||
UIMode Mode
|
||||
|
||||
VariableValues map[string]DynamicValue
|
||||
VariableMarks map[string][]cty.PathValueMarks
|
||||
Changes *Changes
|
||||
DriftedResources []*ResourceInstanceChangeSrc
|
||||
TargetAddrs []addrs.Targetable
|
||||
|
|
|
|||
|
|
@ -202,6 +202,9 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, opts *App
|
|||
))
|
||||
continue
|
||||
}
|
||||
if pvm, ok := plan.VariableMarks[name]; ok {
|
||||
val = val.MarkWithPaths(pvm)
|
||||
}
|
||||
|
||||
variables[name] = &InputValue{
|
||||
Value: val,
|
||||
|
|
|
|||
|
|
@ -2726,3 +2726,96 @@ removed {
|
|||
|
||||
checkStateString(t, state, `<no state>`)
|
||||
}
|
||||
|
||||
func TestContext2Apply_sensitiveInputVariableValue(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
variable "a" {
|
||||
type = string
|
||||
# this variable is not marked sensitive
|
||||
}
|
||||
|
||||
resource "test_resource" "a" {
|
||||
value = var.a
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
p := testProvider("test")
|
||||
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
// Build state with sensitive value in resource object
|
||||
state := states.NewState()
|
||||
root := state.EnsureModule(addrs.RootModuleInstance)
|
||||
root.SetResourceInstanceCurrent(
|
||||
mustResourceInstanceAddr("test_resource.a").Resource,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"value":"secret"}]}`),
|
||||
AttrSensitivePaths: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.GetAttrPath("value"),
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
|
||||
// Create a sensitive-marked value for the input variable. This is not
|
||||
// possible through the normal CLI path, but is possible when the plan is
|
||||
// created and modified by the stacks runtime.
|
||||
secret := cty.StringVal("updated").Mark(marks.Sensitive)
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
SetVariables: InputValues{
|
||||
"a": &InputValue{
|
||||
Value: secret,
|
||||
SourceType: ValueFromUnknown,
|
||||
},
|
||||
},
|
||||
})
|
||||
assertNoErrors(t, diags)
|
||||
|
||||
state, diags = ctx.Apply(plan, m, nil)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("diags: %s", diags.Err())
|
||||
}
|
||||
|
||||
// check that the provider was not asked to destroy the resource
|
||||
if !p.ApplyResourceChangeCalled {
|
||||
t.Fatalf("Expected ApplyResourceChange to be called, but it was not called")
|
||||
}
|
||||
|
||||
instance := state.ResourceInstance(mustResourceInstanceAddr("test_resource.a"))
|
||||
expected := "{\"value\":\"updated\"}"
|
||||
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),
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(instance.Current.AttrSensitivePaths, expectedMarkses); len(diff) > 0 {
|
||||
t.Errorf("unexpected sensitive paths\ndiff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,15 +253,25 @@ The -target option is not for routine use, and is provided only for exceptional
|
|||
|
||||
// convert the variables into the format expected for the plan
|
||||
varVals := make(map[string]plans.DynamicValue, len(opts.SetVariables))
|
||||
varMarks := make(map[string][]cty.PathValueMarks, len(opts.SetVariables))
|
||||
for k, iv := range opts.SetVariables {
|
||||
if iv.Value == cty.NilVal {
|
||||
continue // We only record values that the caller actually set
|
||||
}
|
||||
|
||||
// Root variable values arriving from the traditional CLI path are
|
||||
// unmarked, as they are directly decoded from .tfvars, CLI arguments,
|
||||
// or the environment. However, variable values arriving from other
|
||||
// plans (via the coordination efforts of the stacks runtime) may have
|
||||
// gathered marks during evaluation. We must separate the value from
|
||||
// its marks here to maintain compatibility with plans.DynamicValue,
|
||||
// which cannot represent marks.
|
||||
value, pvm := iv.Value.UnmarkDeepWithPaths()
|
||||
|
||||
// We use cty.DynamicPseudoType here so that we'll save both the
|
||||
// value _and_ its dynamic type in the plan, so we can recover
|
||||
// exactly the same value later.
|
||||
dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType)
|
||||
dv, err := plans.NewDynamicValue(value, cty.DynamicPseudoType)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
|
|
@ -271,12 +281,16 @@ The -target option is not for routine use, and is provided only for exceptional
|
|||
continue
|
||||
}
|
||||
varVals[k] = dv
|
||||
varMarks[k] = pvm
|
||||
}
|
||||
|
||||
// insert the run-specific data from the context into the plan; variables,
|
||||
// targets and provider SHAs.
|
||||
if plan != nil {
|
||||
plan.VariableValues = varVals
|
||||
if len(varMarks) > 0 {
|
||||
plan.VariableMarks = varMarks
|
||||
}
|
||||
plan.TargetAddrs = opts.Targets
|
||||
} else if !diags.HasErrors() {
|
||||
panic("nil plan but no errors")
|
||||
|
|
|
|||
|
|
@ -4897,3 +4897,83 @@ resource "test_object" "a" {}
|
|||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_sensitiveInputVariableValue(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
variable "boop" {
|
||||
type = string
|
||||
# this variable is not marked sensitive
|
||||
}
|
||||
|
||||
resource "test_resource" "a" {
|
||||
value = var.boop
|
||||
}
|
||||
|
||||
`,
|
||||
})
|
||||
|
||||
p := testProvider("test")
|
||||
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
// Build state with sensitive value in resource object
|
||||
state := states.NewState()
|
||||
root := state.EnsureModule(addrs.RootModuleInstance)
|
||||
root.SetResourceInstanceCurrent(
|
||||
mustResourceInstanceAddr("test_resource.a").Resource,
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"value":"secret"}]}`),
|
||||
AttrSensitivePaths: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.GetAttrPath("value"),
|
||||
Marks: cty.NewValueMarks(marks.Sensitive),
|
||||
},
|
||||
},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
|
||||
// Create a sensitive-marked value for the input variable. This is not
|
||||
// possible through the normal CLI path, but is possible when the plan is
|
||||
// created and modified by the stacks runtime.
|
||||
secret := cty.StringVal("secret").Mark(marks.Sensitive)
|
||||
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
SetVariables: InputValues{
|
||||
"boop": &InputValue{
|
||||
Value: secret,
|
||||
SourceType: ValueFromUnknown,
|
||||
},
|
||||
},
|
||||
})
|
||||
assertNoErrors(t, diags)
|
||||
for _, res := range plan.Changes.Resources {
|
||||
switch res.Addr.String() {
|
||||
case "test_resource.a":
|
||||
spew.Dump(res)
|
||||
if res.Action != plans.NoOp {
|
||||
t.Errorf("unexpected %s change for %s", res.Action, res.Addr)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unexpected %s change for %s", res.Action, res.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue