Merge pull request #38298 from hashicorp/jbardin/terraform-data-extensions

terraform_data write-only and sensitive extensions
pull/38316/head
James Bardin 2 months ago committed by GitHub
commit 32ff98ebee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
kind: NEW FEATURES
body: New store block in terraform_data that can handle ephemeral and sensitive values
time: 2026-03-20T08:51:23.141458-04:00
custom:
Issue: "38298"

@ -42,12 +42,14 @@ func TestMoveResourceState_DataStore(t *testing.T) {
t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err())
}
expectedTargetState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
})
expected, err := dataStoreResourceSchema().Body.CoerceValue(cty.EmptyObjectVal)
if err != nil {
t.Fatal(err)
}
expectedMap := expected.AsValueMap()
expectedMap["id"] = cty.StringVal("test")
expectedTargetState := cty.ObjectVal(expectedMap)
if !resp.TargetState.RawEquals(expectedTargetState) {
t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState)

@ -19,12 +19,44 @@ func dataStoreResourceSchema() providers.Schema {
return providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"input": {Type: cty.DynamicPseudoType, Optional: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
"id": {Type: cty.String, Computed: true},
// forces replacement of the entire resource when changed
"triggers_replace": {Type: cty.DynamicPseudoType, Optional: true},
"id": {Type: cty.String, Computed: true},
// input is reflected in output after apply, and changes to
// input always result in a re-computation of output.
"input": {Type: cty.DynamicPseudoType, Optional: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"store": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
// The input attribute will be exposed as a stable
// value in store.output or store.sensitive_output.
"input": {Type: cty.DynamicPseudoType, Optional: true, WriteOnly: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
"sensitive_output": {Type: cty.DynamicPseudoType, Computed: true, Sensitive: true},
// If there is a version value, a change in that
// value will trigger a change in the stored output
// or sensitive_output. If there is no version
// value, then input will be compared directly
// against output.
"version": {Type: cty.DynamicPseudoType, Optional: true},
"sensitive": {Type: cty.Bool, Optional: true},
// replace causes the resource to be replaced when
// there is a change to a store output value.
"replace": {Type: cty.Bool, Optional: true},
},
},
},
},
},
Identity: dataStoreResourceIdentitySchema().Body,
}
}
@ -61,8 +93,9 @@ func validateDataStoreResourceConfig(req providers.ValidateResourceConfigRequest
}
func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
ty := dataStoreResourceSchema().Body.ImpliedType()
val, err := ctyjson.Unmarshal(req.RawStateJSON, ty)
// We've only added new nullable block attributes, so unmarshaling from json
// will complete the data structure correctly.
val, err := ctyjson.Unmarshal(req.RawStateJSON, dataStoreResourceSchema().Body.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
@ -79,59 +112,193 @@ func upgradeDataStoreResourceIdentity(providers.UpgradeResourceIdentityRequest)
func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
resp.NewState = req.PriorState
resp.Private = req.Private
return resp
}
func planDataStoreResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
resp.PlannedState = req.ProposedNewState
if req.ProposedNewState.IsNull() {
// destroy op
resp.PlannedState = req.ProposedNewState
return resp
}
planned := req.ProposedNewState.AsValueMap()
prior := req.PriorState
input := req.ProposedNewState.GetAttr("input")
trigger := req.ProposedNewState.GetAttr("triggers_replace")
// first determine if this is a create or replace
if !prior.IsNull() && !prior.GetAttr("triggers_replace").RawEquals(req.ProposedNewState.GetAttr("triggers_replace")) {
// trigger changed, so we need to replace the entire instance
resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("triggers_replace"))
switch {
case req.PriorState.IsNull():
// Create
// Set the id value to unknown.
// set the prior value to null so that that everything else is treated
// as if it's a new instance
prior = cty.NullVal(req.ProposedNewState.Type())
}
// creating a new instance, so we need a new ID
if prior.IsNull() {
// New instances, so set the id value to unknown.
planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
}
// check the input/output for changes
input := req.ProposedNewState.GetAttr("input")
priorInput := cty.NullVal(cty.DynamicPseudoType)
if !prior.IsNull() {
priorInput = prior.GetAttr("input")
}
// Output type must always match the input, even when it's null.
if !priorInput.RawEquals(input) {
if input.IsNull() {
planned["output"] = input
// we reflect the type even if the value is null
planned["output"] = cty.NullVal(input.Type())
} else {
// input changed, so we need to re-compute output
planned["output"] = cty.UnknownVal(input.Type())
}
}
resp.PlannedState = cty.ObjectVal(planned)
return resp
// check the store object for changes
if store := req.ProposedNewState.GetAttr("store"); !store.IsNull() {
objMap := storeMap(store.AsValueMap())
priorVersion := cty.NullVal(cty.DynamicPseudoType)
priorSensitive := cty.NullVal(cty.Bool)
for _, mustKnow := range []string{"sensitive", "replace"} {
if !store.GetAttr(mustKnow).IsKnown() {
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"unexpected unknown value",
fmt.Sprintf("the %q attribute must be known in order to plan changes to this resource", mustKnow),
cty.GetAttrPath("store").GetAttr(mustKnow),
))
return resp
}
}
case !req.PriorState.GetAttr("triggers_replace").RawEquals(trigger):
// trigger changed, so we need to replace the entire instance
resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("triggers_replace"))
planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
if !prior.IsNull() && !prior.GetAttr("store").IsNull() {
priorVersion = prior.GetAttr("store").GetAttr("version")
priorSensitive = prior.GetAttr("store").GetAttr("sensitive")
}
// We need to check the input for the replacement instance to compute a
// new output.
if input.IsNull() {
planned["output"] = input
} else {
planned["output"] = cty.UnknownVal(input.Type())
// if sensitive changed, just move the data between outputs
if !priorSensitive.RawEquals(objMap.sensitive()) {
objMap.swapOutputs()
}
case !req.PriorState.GetAttr("input").RawEquals(input):
// only input changed, so we only need to re-compute output
planned["output"] = cty.UnknownVal(input.Type())
// Plan an update if the version changed, or if the input and output don't
// match in the absence of a version value.
switch {
// if input and outputs are all null, just pass through a possible null type.
case objMap.valuesNull():
objMap.storeNull()
// if there is a version, checked if it has changed
case objMap.hasVersion():
// The version value comparison is done within this case, because we
// don't want to fall into the input comparison case when there is a
// version, nor do we want to prevent evaluating that case if the
// input and output changed.
if !objMap.version().RawEquals(priorVersion) {
objMap.storeChange()
}
// if there is no version, we automatically update if the input and output
// don't match
case objMap.hasChange():
objMap.storeChange()
}
// see if we want store to replace the resource
if objMap.replace() {
planned["id"] = cty.UnknownVal(cty.String)
resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("store").GetAttr("input"))
}
// and the input must always be returned as the unset null value because it
// is write-only
objMap.clearInput()
planned["store"] = cty.ObjectVal(objMap)
}
resp.PlannedState = cty.ObjectVal(planned)
return resp
}
// storeMap encapsulates some of the logic around handling the various
// combinations of the object attributes. There are a few accessors and simple
// set functions just to make accessing the data consistent, so nothing needs to
// index the map directly.
type storeMap map[string]cty.Value
func (d storeMap) valuesNull() bool {
return d["input"].IsNull() && d["output"].IsNull() && d["sensitive_output"].IsNull()
}
func (d storeMap) isSensitive() bool {
return !d["sensitive"].IsNull() && d["sensitive"].True()
}
func (d storeMap) sensitive() cty.Value {
return d["sensitive"]
}
func (d storeMap) hasVersion() bool {
return !d["version"].IsNull()
}
func (d storeMap) version() cty.Value {
return d["version"]
}
func (d storeMap) storeNull() {
d.write(cty.NullVal(d["input"].Type()))
}
func (d storeMap) storeChange() {
d.write(cty.UnknownVal(d["input"].Type()))
}
func (d storeMap) swapOutputs() {
output := d["output"]
if tmp := d["sensitive_output"]; !tmp.IsNull() {
output = tmp
}
d.write(output)
}
func (d storeMap) write(v cty.Value) {
if d.isSensitive() {
d["sensitive_output"] = v
d["output"] = cty.NullVal(cty.DynamicPseudoType)
return
}
d["output"] = v
d["sensitive_output"] = cty.NullVal(cty.DynamicPseudoType)
}
func (d storeMap) clearInput() {
d["input"] = cty.NullVal(cty.DynamicPseudoType)
}
func (d storeMap) hasChange() bool {
old := d["output"]
if !d["sensitive_output"].IsNull() {
old = d["sensitive_output"]
}
return !old.RawEquals(d["input"])
}
func (d storeMap) replace() bool {
return d["replace"].True() && !(d["output"].IsKnown() && d["sensitive_output"].IsKnown())
}
var testUUIDHook func() string
func applyDataStoreResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
@ -140,35 +307,51 @@ func applyDataStoreResourceChange(req providers.ApplyResourceChangeRequest) (res
return resp
}
newState := req.PlannedState.AsValueMap()
if !req.PlannedState.GetAttr("output").IsKnown() {
newState["output"] = req.PlannedState.GetAttr("input")
}
if !req.PlannedState.GetAttr("id").IsKnown() {
idString, err := uuid.GenerateUUID()
// Terraform would probably never get this far without a good random
// source, but catch the error anyway.
if err != nil {
diag := tfdiags.AttributeValue(
tfdiags.Error,
"Error generating id",
err.Error(),
cty.GetAttrPath("id"),
)
resp.Diagnostics = resp.Diagnostics.Append(diag)
// Applying a plan only consists of filling in any unknown values. We can
// write this as a single transformation, and base the logic on the path of
// the transform value.
resp.NewState, _ = cty.Transform(req.PlannedState, func(path cty.Path, val cty.Value) (cty.Value, error) {
if val.IsKnown() {
return val, nil
}
if testUUIDHook != nil {
idString = testUUIDHook()
// val is unknown, so find the correct value based on our path
switch {
case path.Equals(cty.GetAttrPath("id")):
idString, err := uuid.GenerateUUID()
// Terraform would probably never get this far without a good random
// source, but catch the error anyway.
if err != nil {
diag := tfdiags.AttributeValue(
tfdiags.Error,
"Error generating id",
err.Error(),
cty.GetAttrPath("id"),
)
resp.Diagnostics = resp.Diagnostics.Append(diag)
}
if testUUIDHook != nil {
idString = testUUIDHook()
}
return cty.StringVal(idString), nil
case path.Equals(cty.GetAttrPath("output")):
return req.PlannedState.GetAttr("input"), nil
case path.Equals(cty.GetAttrPath("store").GetAttr("output")):
// input is write-only, so won't be in the planned state. We ned to get
// the latest ephemeral value directly from the config.
return req.Config.GetAttr("store").GetAttr("input"), nil
case path.Equals(cty.GetAttrPath("store").GetAttr("sensitive_output")):
// input is write-only, so won't be in the planned state. We ned to get
// the latest ephemeral value directly from the config.
return req.Config.GetAttr("store").GetAttr("input"), nil
}
newState["id"] = cty.StringVal(idString)
}
resp.NewState = cty.ObjectVal(newState)
return val, nil
})
return resp
}

@ -47,27 +47,43 @@ func TestManagedDataValidate(t *testing.T) {
}
func TestManagedDataUpgradeState(t *testing.T) {
schema := dataStoreResourceSchema()
ty := schema.Body.ImpliedType()
rawState := `{
"id": "not-quite-unique",
"input": {
"value": "input",
"type": "string"
},
"output": {
"value": "input",
"type": "string"
},
"triggers_replace": {
"value": [
"a",
"b"
],
"type": [
"list",
"string"
]
}
}`
state := cty.ObjectVal(map[string]cty.Value{
upgradedState, err := dataStoreResourceSchema().Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.StringVal("input"),
"triggers_replace": cty.ListVal([]cty.Value{
cty.StringVal("a"), cty.StringVal("b"),
}),
"id": cty.StringVal("not-quite-unique"),
})
jsState, err := ctyjson.Marshal(state, ty)
}))
if err != nil {
t.Fatal(err)
}
// empty
req := providers.UpgradeResourceStateRequest{
TypeName: "terraform_data",
RawStateJSON: jsState,
RawStateJSON: []byte(rawState),
}
resp := upgradeDataStoreResourceState(req)
@ -75,8 +91,8 @@ func TestManagedDataUpgradeState(t *testing.T) {
t.Error("upgrade state error:", resp.Diagnostics.ErrWithWarnings())
}
if !resp.UpgradedState.RawEquals(state) {
t.Errorf("prior state was:\n%#v\nupgraded state is:\n%#v\n", state, resp.UpgradedState)
if !resp.UpgradedState.RawEquals(upgradedState) {
t.Errorf("prior state was:\n%s\nupgraded state is:\n%#v\n", rawState, resp.UpgradedState)
}
}
@ -135,12 +151,49 @@ func TestManagedDataPlan(t *testing.T) {
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.NullVal(cty.String),
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.Number),
"output": cty.NullVal(cty.DynamicPseudoType),
}),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.String),
"output": cty.NullVal(cty.String),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.UnknownVal(cty.String).RefineNotNull(),
"store": cty.ObjectVal(map[string]cty.Value{
// write-only values are always returned as null
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.NullVal(cty.Number),
}),
}),
},
"create-typed-null-sensitive-write-only": {
prior: cty.NullVal(ty),
proposed: cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.String),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.NullVal(cty.String),
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.Number),
"output": cty.NullVal(cty.DynamicPseudoType),
"sensitive": cty.BoolVal(true),
}),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.String),
"output": cty.NullVal(cty.String),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.UnknownVal(cty.String).RefineNotNull(),
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"sensitive_output": cty.NullVal(cty.Number),
"sensitive": cty.BoolVal(true),
}),
}),
},
@ -228,26 +281,279 @@ func TestManagedDataPlan(t *testing.T) {
"id": cty.UnknownVal(cty.String).RefineNotNull(),
}),
},
"update-store-trigger": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(1),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem"),
"version": cty.NumberIntVal(2),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(2),
"output": cty.UnknownVal(cty.String),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"update-store-trigger-to-sensitive": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(1),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem"),
"sensitive": cty.BoolVal(true),
"version": cty.NumberIntVal(2),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"sensitive": cty.BoolVal(true),
"version": cty.NumberIntVal(2),
"output": cty.NullVal(cty.DynamicPseudoType),
"sensitive_output": cty.UnknownVal(cty.String),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"update-store-auto": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem_1"),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.UnknownVal(cty.String),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"update-store-auto-replace": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem_1"),
"output": cty.StringVal("ephem_2"),
"replace": cty.BoolVal(true),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.UnknownVal(cty.String),
"replace": cty.BoolVal(true),
}),
"id": cty.UnknownVal(cty.String),
}),
},
"remove-value-auto-replace": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_1"),
"replace": cty.BoolVal(true),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_1"),
"replace": cty.BoolVal(true),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.UnknownVal(cty.DynamicPseudoType),
"replace": cty.BoolVal(true),
}),
"id": cty.UnknownVal(cty.String),
}),
},
"update-store-auto-sensitive": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"sensitive": cty.BoolVal(true),
"sensitive_output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem_1"),
"sensitive": cty.BoolVal(true),
"sensitive_output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"sensitive": cty.BoolVal(true),
"sensitive_output": cty.UnknownVal(cty.String),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"no-update-store-trigger": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(1),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem 2"),
"version": cty.NumberIntVal(1),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(1),
"output": cty.StringVal("ephem"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"no-update-store-auto": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem_2"),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_2"),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"swap-sensitive": {
prior: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("ephem_1"),
"replace": cty.BoolVal(true),
}),
"id": cty.StringVal("not-quite-unique"),
}),
proposed: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("ephem_1"),
"output": cty.StringVal("ephem_1"),
"replace": cty.BoolVal(true),
// sensitive changes, swap the outputs
"sensitive": cty.BoolVal(true),
}),
"id": cty.StringVal("not-quite-unique"),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"replace": cty.BoolVal(true),
"sensitive_output": cty.StringVal("ephem_1"),
"sensitive": cty.BoolVal(true),
}),
// swapping outputs does not replace, the value is not changing
"id": cty.StringVal("not-quite-unique"),
}),
},
} {
t.Run("plan-"+name, func(t *testing.T) {
req := providers.PlanResourceChangeRequest{
TypeName: "terraform_data",
PriorState: tc.prior,
ProposedNewState: tc.proposed,
PriorState: mustCoerceManagedData(t, tc.prior),
ProposedNewState: mustCoerceManagedData(t, tc.proposed),
}
resp := planDataStoreResourceChange(req)
if resp.Diagnostics.HasErrors() {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
expectedPlanned := mustCoerceManagedData(t, tc.planned)
if !resp.PlannedState.RawEquals(tc.planned) {
t.Errorf("expected:\n%#v\ngot:\n%#v\n", tc.planned, resp.PlannedState)
if !resp.PlannedState.RawEquals(expectedPlanned) {
t.Errorf("expected:\n%#v\ngot:\n%#v\n", expectedPlanned, resp.PlannedState)
}
})
}
}
func mustCoerceManagedData(t *testing.T, v cty.Value) cty.Value {
schema := dataStoreResourceSchema().Body
v, err := schema.CoerceValue(v)
if err != nil {
t.Fatalf("failed to coerce value: %s", err)
}
return v
}
func TestManagedDataApply(t *testing.T) {
testUUIDHook = func() string {
return "not-quite-unique"
@ -257,15 +563,14 @@ func TestManagedDataApply(t *testing.T) {
}()
schema := dataStoreResourceSchema().Body
ty := schema.ImpliedType()
for name, tc := range map[string]struct {
prior cty.Value
config cty.Value
planned cty.Value
state cty.Value
}{
"create": {
prior: cty.NullVal(ty),
config: schema.EmptyValue(),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.NullVal(cty.DynamicPseudoType),
@ -281,7 +586,12 @@ func TestManagedDataApply(t *testing.T) {
},
"create-output": {
prior: cty.NullVal(ty),
config: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.UnknownVal(cty.String),
@ -297,11 +607,11 @@ func TestManagedDataApply(t *testing.T) {
},
"update-input": {
prior: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.StringVal("input"),
config: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("new-input"),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.StringVal("not-quite-unique"),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}),
@ -318,11 +628,11 @@ func TestManagedDataApply(t *testing.T) {
},
"update-trigger": {
prior: cty.ObjectVal(map[string]cty.Value{
config: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.StringVal("input"),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
"id": cty.StringVal("not-quite-unique"),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.StringVal("new-value"),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
@ -339,13 +649,13 @@ func TestManagedDataApply(t *testing.T) {
},
"update-input-trigger": {
prior: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("input"),
"output": cty.StringVal("input"),
config: cty.ObjectVal(map[string]cty.Value{
"input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.MapVal(map[string]cty.Value{
"key": cty.StringVal("value"),
}),
"id": cty.StringVal("not-quite-unique"),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}),
@ -364,12 +674,69 @@ func TestManagedDataApply(t *testing.T) {
"id": cty.StringVal("not-quite-unique"),
}),
},
"update-store-trigger": {
config: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("new_ephem"),
"version": cty.NumberIntVal(1),
"output": cty.NullVal(cty.DynamicPseudoType),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(2),
"output": cty.UnknownVal(cty.String),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.StringVal("not-quite-unique"),
}),
state: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"version": cty.NumberIntVal(2),
"output": cty.StringVal("new_ephem"),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
"update-store-auto": {
config: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("new_ephem"),
"output": cty.StringVal("ephem"),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.NullVal(cty.String),
}),
planned: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.UnknownVal(cty.String),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.StringVal("not-quite-unique"),
}),
state: cty.ObjectVal(map[string]cty.Value{
"store": cty.ObjectVal(map[string]cty.Value{
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.StringVal("new_ephem"),
"replace": cty.NullVal(cty.Bool),
}),
"id": cty.StringVal("not-quite-unique"),
}),
},
} {
t.Run("apply-"+name, func(t *testing.T) {
req := providers.ApplyResourceChangeRequest{
TypeName: "terraform_data",
PriorState: tc.prior,
PlannedState: tc.planned,
Config: mustCoerceManagedData(t, tc.config),
PlannedState: mustCoerceManagedData(t, tc.planned),
}
resp := applyDataStoreResourceChange(req)
@ -377,8 +744,10 @@ func TestManagedDataApply(t *testing.T) {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
if !resp.NewState.RawEquals(tc.state) {
t.Errorf("expected:\n%#v\ngot:\n%#v\n", tc.state, resp.NewState)
expected := mustCoerceManagedData(t, tc.state)
if !resp.NewState.RawEquals(expected) {
t.Errorf("expected:\n%#v\ngot:\n%#v\n", expected, resp.NewState)
}
})
}
@ -409,12 +778,14 @@ func TestMoveDataStoreResourceState_Id(t *testing.T) {
t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err())
}
expectedTargetState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.NullVal(cty.DynamicPseudoType),
})
expected, err := dataStoreResourceSchema().Body.CoerceValue(cty.EmptyObjectVal)
if err != nil {
t.Fatal(err)
}
expectedMap := expected.AsValueMap()
expectedMap["id"] = cty.StringVal("test")
expectedTargetState := cty.ObjectVal(expectedMap)
if !resp.TargetState.RawEquals(expectedTargetState) {
t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState)
@ -475,14 +846,17 @@ func TestMoveDataStoreResourceState_Triggers(t *testing.T) {
t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err())
}
expectedTargetState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"input": cty.NullVal(cty.DynamicPseudoType),
"output": cty.NullVal(cty.DynamicPseudoType),
"triggers_replace": cty.ObjectVal(map[string]cty.Value{
"testkey": cty.StringVal("testvalue"),
}),
expected, err := dataStoreResourceSchema().Body.CoerceValue(cty.EmptyObjectVal)
if err != nil {
t.Fatal(err)
}
expectedMap := expected.AsValueMap()
expectedMap["id"] = cty.StringVal("test")
expectedMap["triggers_replace"] = cty.ObjectVal(map[string]cty.Value{
"testkey": cty.StringVal("testvalue"),
})
expectedTargetState := cty.ObjectVal(expectedMap)
if !resp.TargetState.RawEquals(expectedTargetState) {
t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState)

@ -31,6 +31,7 @@ func (b *Block) CoerceValue(in cty.Value) (cty.Value, error) {
func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
convType := b.specType()
impliedType := convType.WithoutOptionalAttributesDeep()
switch {
@ -85,6 +86,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
if err != nil {
return cty.UnknownVal(impliedType), append(path, cty.GetAttrStep{Name: name}).NewError(err)
}
attrs[name] = val
}

@ -103,7 +103,6 @@ func (b *Block) DecoderSpec() hcldec.Spec {
}
childSpec := blockS.Block.DecoderSpec()
switch blockS.Nesting {
case NestingSingle, NestingGroup:
ret[name] = &hcldec.BlockSpec{
@ -178,7 +177,6 @@ func (b *Block) DecoderSpec() hcldec.Spec {
continue
}
}
decoderSpecCache.set(b, ret)
return ret
}

@ -149,7 +149,7 @@ func (a *Attribute) internalValidate(name, prefix string) error {
if a.NestedType != nil {
switch a.NestedType.Nesting {
case NestingSingle, NestingMap:
case NestingSingle, NestingMap, NestingGroup:
// no validations to perform
case NestingList, NestingSet:
if a.NestedType.Nesting == NestingSet {

@ -347,6 +347,7 @@ func TestDependenciesProviderSchema(t *testing.T) {
}
{
got := schemaResp.Schema
want := &dependencies.ProviderSchema{
ProviderConfig: &dependencies.Schema{
Block: &dependencies.Schema_Block{
@ -431,6 +432,47 @@ func TestDependenciesProviderSchema(t *testing.T) {
Optional: true,
},
},
BlockTypes: []*dependencies.Schema_NestedBlock{
{
TypeName: "store",
Nesting: dependencies.Schema_NestedBlock_SINGLE,
Block: &dependencies.Schema_Block{
Attributes: []*dependencies.Schema_Attribute{
{
Name: "input",
Type: []byte(`"dynamic"`),
Optional: true,
},
{
Name: "output",
Type: []byte(`"dynamic"`),
Computed: true,
},
{
Name: "replace",
Type: []byte(`"bool"`),
Optional: true,
},
{
Name: "sensitive",
Type: []byte(`"bool"`),
Optional: true,
},
{
Name: "sensitive_output",
Type: []byte(`"dynamic"`),
Computed: true,
Sensitive: true,
},
{
Name: "version",
Type: []byte(`"dynamic"`),
Optional: true,
},
},
},
},
},
},
},
},

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/msgpack"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/depsfile"
@ -26,6 +27,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/builtin/providers/terraform"
terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/configs"
@ -1987,6 +1989,20 @@ func TestPlanWithSingleResource(t *testing.T) {
return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc)
})
// extract the schema from the builtin provider so we can generate correctly
// shaped plan objects.
schema := terraform.NewProvider().GetProviderSchema().ResourceTypes["terraform_data"]
wantMap := schema.Body.EmptyValue().AsValueMap()
wantMap["id"] = cty.UnknownVal(cty.String).RefineNotNull()
wantMap["input"] = cty.StringVal("hello")
wantMap["output"] = cty.UnknownVal(cty.String)
wantVal := cty.ObjectVal(wantMap)
wantValEncoded, err := msgpack.Marshal(wantVal, schema.Body.ImpliedType())
if err != nil {
t.Fatal(err)
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
@ -2062,26 +2078,7 @@ func TestPlanWithSingleResource(t *testing.T) {
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: plans.DynamicValue{
// This is an object conforming to the terraform_data
// resource type's schema.
//
// FIXME: Should write this a different way that is
// scrutable and won't break each time something gets
// added to the terraform_data schema. (We can't use
// mustPlanDynamicValue here because the resource type
// uses DynamicPseudoType attributes, which require
// explicitly-typed encoding.)
0x84, 0xa2, 0x69, 0x64, 0xc7, 0x03, 0x0c, 0x81,
0x01, 0xc2, 0xa5, 0x69, 0x6e, 0x70, 0x75, 0x74,
0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x22, 0xa5, 0x68, 0x65, 0x6c, 0x6c,
0x6f, 0xa6, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74,
0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x22, 0xd4, 0x00, 0x00, 0xb0, 0x74,
0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x5f,
0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0xc0,
},
After: plans.DynamicValue(wantValEncoded),
},
},
@ -2089,26 +2086,7 @@ func TestPlanWithSingleResource(t *testing.T) {
// type from the real terraform.io/builtin/terraform provider
// maintained elsewhere in this codebase. If that schema changes
// in future then this should change to match it.
Schema: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"input": {Type: cty.DynamicPseudoType, Optional: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
"triggers_replace": {Type: cty.DynamicPseudoType, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
Identity: &configschema.Object{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Description: "The unique identifier for the data store.",
Required: true,
},
},
Nesting: configschema.NestingSingle,
},
},
Schema: schema,
},
}

Loading…
Cancel
Save