mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 08:58:34 -04:00
Merge pull request #38298 from hashicorp/jbardin/terraform-data-extensions
terraform_data write-only and sensitive extensions
This commit is contained in:
commit
32ff98ebee
9 changed files with 739 additions and 155 deletions
5
.changes/v1.16/NEW FEATURES-20260320-085123.yaml
Normal file
5
.changes/v1.16/NEW FEATURES-20260320-085123.yaml
Normal file
|
|
@ -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")
|
||||
|
||||
switch {
|
||||
case req.PriorState.IsNull():
|
||||
// Create
|
||||
// Set the id value to unknown.
|
||||
planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
|
||||
|
||||
// Output type must always match the input, even when it's null.
|
||||
if input.IsNull() {
|
||||
planned["output"] = input
|
||||
} else {
|
||||
planned["output"] = cty.UnknownVal(input.Type())
|
||||
}
|
||||
|
||||
resp.PlannedState = cty.ObjectVal(planned)
|
||||
return resp
|
||||
|
||||
case !req.PriorState.GetAttr("triggers_replace").RawEquals(trigger):
|
||||
// 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"))
|
||||
planned["id"] = cty.UnknownVal(cty.String).RefineNotNull()
|
||||
|
||||
// We need to check the input for the replacement instance to compute a
|
||||
// new output.
|
||||
// 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")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
case !req.PriorState.GetAttr("input").RawEquals(input):
|
||||
// only input changed, so we only need to re-compute output
|
||||
planned["output"] = cty.UnknownVal(input.Type())
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
if !prior.IsNull() && !prior.GetAttr("store").IsNull() {
|
||||
priorVersion = prior.GetAttr("store").GetAttr("version")
|
||||
priorSensitive = prior.GetAttr("store").GetAttr("sensitive")
|
||||
}
|
||||
|
||||
// if sensitive changed, just move the data between outputs
|
||||
if !priorSensitive.RawEquals(objMap.sensitive()) {
|
||||
objMap.swapOutputs()
|
||||
}
|
||||
|
||||
// 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…
Reference in a new issue