import (
"fmt"
+ "log"
+
+ "github.com/zclconf/go-cty/cty"
+
+ "github.com/hashicorp/terraform/addrs"
+ "github.com/hashicorp/terraform/configs"
+ "github.com/hashicorp/terraform/plans"
+ "github.com/hashicorp/terraform/plans/objchange"
+ "github.com/hashicorp/terraform/providers"
+ "github.com/hashicorp/terraform/states"
+ "github.com/hashicorp/terraform/tfdiags"
)
-// EvalReadDataDiff is an EvalNode implementation that executes a data
-// resource's ReadDataDiff method to discover what attributes it exports.
-type EvalReadDataDiff struct {
- Provider *ResourceProvider
- Output **InstanceDiff
- OutputState **InstanceState
- Config **ResourceConfig
- Info *InstanceInfo
-
- // Set Previous when re-evaluating diff during apply, to ensure that
- // the "Destroy" flag is preserved.
- Previous **InstanceDiff
+// EvalReadData is an EvalNode implementation that deals with the main part
+// of the data resource lifecycle: either actually reading from the data source
+// or generating a plan to do so.
+type EvalReadData struct {
+ Addr addrs.ResourceInstance
+ Config *configs.Resource
+ Dependencies []addrs.Referenceable
+ Provider *providers.Interface
+ ProviderAddr addrs.AbsProviderConfig
+ ProviderSchema **ProviderSchema
+
+ // Planned is set when dealing with data resources that were deferred to
+ // the apply walk, to let us see what was planned. If this is set, the
+ // evaluation of the config is required to produce a wholly-known
+ // configuration which is consistent with the partial object included
+ // in this planned change.
+ Planned **plans.ResourceInstanceChange
+
+ // ForcePlanRead, if true, overrides the usual behavior of immediately
+ // reading from the data source where possible, instead forcing us to
+ // _always_ generate a plan. This is used during the plan walk, since we
+ // mustn't actually apply anything there. (The resulting state doesn't
+ // get persisted)
+ ForcePlanRead bool
+
+ // The result from this EvalNode has a few different possibilities
+ // depending on the input:
+ // - If Planned is nil then we assume we're aiming to _produce_ the plan,
+ // and so the following two outcomes are possible:
+ // - OutputChange.Action is plans.NoOp and OutputState is the complete
+ // result of reading from the data source. This is the easy path.
+ // - OutputChange.Action is plans.Read and OutputState is a planned
+ // object placeholder (states.ObjectPlanned). In this case, the
+ // returned change must be recorded in the overral changeset and
+ // eventually passed to another instance of this struct during the
+ // apply walk.
+ // - If Planned is non-nil then we assume we're aiming to complete a
+ // planned read from an earlier plan walk. In this case the only possible
+ // non-error outcome is to set Output.Action (if non-nil) to a plans.NoOp
+ // change and put the complete resulting state in OutputState, ready to
+ // be saved in the overall state and used for expression evaluation.
+ OutputChange **plans.ResourceInstanceChange
+ OutputValue *cty.Value
+ OutputConfigValue *cty.Value
+ OutputState **states.ResourceInstanceObject
}
-func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) {
- // TODO: test
+func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
+ absAddr := n.Addr.Absolute(ctx.Path())
+ log.Printf("[TRACE] EvalReadData: working on %s", absAddr)
- err := ctx.Hook(func(h Hook) (HookAction, error) {
- return h.PreDiff(n.Info, nil)
- })
- if err != nil {
- return nil, err
+ if n.ProviderSchema == nil || *n.ProviderSchema == nil {
+ return nil, fmt.Errorf("provider schema not available for %s", n.Addr)
}
- var diff *InstanceDiff
+ var diags tfdiags.Diagnostics
+ var change *plans.ResourceInstanceChange
+ var configVal cty.Value
- if n.Previous != nil && *n.Previous != nil && (*n.Previous).GetDestroy() {
- // If we're re-diffing for a diff that was already planning to
- // destroy, then we'll just continue with that plan.
- diff = &InstanceDiff{Destroy: true}
- } else {
- provider := *n.Provider
- config := *n.Config
+ // TODO: Do we need to handle Delete changes here? EvalReadDataDiff and
+ // EvalReadDataApply did, but it seems like we should handle that via a
+ // separate mechanism since it boils down to just deleting the object from
+ // the state... and we do that on every plan anyway, forcing the data
+ // resource to re-read.
- var err error
- diff, err = provider.ReadDataDiff(n.Info, config)
+ config := *n.Config
+ provider := *n.Provider
+ providerSchema := *n.ProviderSchema
+ schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+ if schema == nil {
+ // Should be caught during validation, so we don't bother with a pretty error here
+ return nil, fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.ProviderConfig.Type, n.Addr.Resource.Type)
+ }
+
+ // We'll always start by evaluating the configuration. What we do after
+ // that will depend on the evaluation result along with what other inputs
+ // we were given.
+ objTy := schema.ImpliedType()
+ priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time
+
+ keyData := EvalDataForInstanceKey(n.Addr.Key)
+
+ var configDiags tfdiags.Diagnostics
+ configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
+ diags = diags.Append(configDiags)
+ if configDiags.HasErrors() {
+ return nil, diags.Err()
+ }
+
+ proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal)
+
+ // If our configuration contains any unknown values then we must defer the
+ // read to the apply phase by producing a "Read" change for this resource,
+ // and a placeholder value for it in the state.
+ if n.ForcePlanRead || !configVal.IsWhollyKnown() {
+ // If the configuration is still unknown when we're applying a planned
+ // change then that indicates a bug in Terraform, since we should have
+ // everything resolved by now.
+ if n.Planned != nil && *n.Planned != nil {
+ return nil, fmt.Errorf(
+ "configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
+ absAddr,
+ )
+ }
+ if n.ForcePlanRead {
+ log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr)
+ } else {
+ log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr)
+ }
+
+ err := ctx.Hook(func(h Hook) (HookAction, error) {
+ return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
+ })
if err != nil {
return nil, err
}
- if diff == nil {
- diff = new(InstanceDiff)
+
+ change = &plans.ResourceInstanceChange{
+ Addr: absAddr,
+ ProviderAddr: n.ProviderAddr,
+ Change: plans.Change{
+ Action: plans.Read,
+ Before: priorVal,
+ After: proposedNewVal,
+ },
}
- // if id isn't explicitly set then it's always computed, because we're
- // always "creating a new resource".
- diff.init()
- if _, ok := diff.Attributes["id"]; !ok {
- diff.SetAttribute("id", &ResourceAttrDiff{
- Old: "",
- NewComputed: true,
- RequiresNew: true,
- Type: DiffAttrOutput,
- })
+ err = ctx.Hook(func(h Hook) (HookAction, error) {
+ return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if n.OutputChange != nil {
+ *n.OutputChange = change
+ }
+ if n.OutputValue != nil {
+ *n.OutputValue = change.After
+ }
+ if n.OutputConfigValue != nil {
+ *n.OutputConfigValue = configVal
}
+ if n.OutputState != nil {
+ state := &states.ResourceInstanceObject{
+ Value: change.After,
+ Status: states.ObjectPlanned, // because the partial value in the plan must be used for now
+ Dependencies: n.Dependencies,
+ }
+ *n.OutputState = state
+ }
+
+ return nil, diags.ErrWithWarnings()
}
- err = ctx.Hook(func(h Hook) (HookAction, error) {
- return h.PostDiff(n.Info, diff)
+ if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read {
+ // If any other action gets in here then that's always a bug; this
+ // EvalNode only deals with reading.
+ return nil, fmt.Errorf(
+ "invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)",
+ (*n.Planned).Action, absAddr,
+ )
+ }
+
+ // If we get down here then our configuration is complete and we're read
+ // to actually call the provider to read the data.
+ log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr)
+
+ err := ctx.Hook(func(h Hook) (HookAction, error) {
+ // We don't have a state yet, so we'll just give the hook an
+ // empty one to work with.
+ return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
})
if err != nil {
return nil, err
}
- *n.Output = diff
+ resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
+ TypeName: n.Addr.Resource.Type,
+ Config: configVal,
+ })
+ diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
+ if diags.HasErrors() {
+ return nil, diags.Err()
+ }
+ newVal := resp.State
+ if newVal == cty.NilVal {
+ // This can happen with incompletely-configured mocks. We'll allow it
+ // and treat it as an alias for a properly-typed null value.
+ newVal = cty.NullVal(schema.ImpliedType())
+ }
+
+ for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Provider produced invalid object",
+ fmt.Sprintf(
+ "Provider %q produced an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
+ n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
+ ),
+ ))
+ }
+ if diags.HasErrors() {
+ return nil, diags.Err()
+ }
+
+ if newVal.IsNull() {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Provider produced null object",
+ fmt.Sprintf(
+ "Provider %q produced a null value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
+ n.ProviderAddr.ProviderConfig.Type, absAddr,
+ ),
+ ))
+ }
+ if !newVal.IsWhollyKnown() {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Provider produced invalid object",
+ fmt.Sprintf(
+ "Provider %q produced a value for %s that is not wholly known.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
+ n.ProviderAddr.ProviderConfig.Type, absAddr,
+ ),
+ ))
+
+ // We'll still save the object, but we need to eliminate any unknown
+ // values first because we can't serialize them in the state file.
+ // Note that this may cause set elements to be coalesced if they
+ // differed only by having unknown values, but we don't worry about
+ // that here because we're saving the value only for inspection
+ // purposes; the error we added above will halt the graph walk.
+ newVal = cty.UnknownAsNull(newVal)
+ }
+
+ // Since we've completed the read, we actually have no change to make, but
+ // we'll produce a NoOp one anyway to preserve the usual flow of the
+ // plan phase and allow it to produce a complete plan.
+ change = &plans.ResourceInstanceChange{
+ Addr: absAddr,
+ ProviderAddr: n.ProviderAddr,
+ Change: plans.Change{
+ Action: plans.NoOp,
+ Before: newVal,
+ After: newVal,
+ },
+ }
+ state := &states.ResourceInstanceObject{
+ Value: change.After,
+ Status: states.ObjectReady, // because we completed the read from the provider
+ Dependencies: n.Dependencies,
+ }
+
+ err = ctx.Hook(func(h Hook) (HookAction, error) {
+ return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
+ })
+ if err != nil {
+ return nil, err
+ }
+ if n.OutputChange != nil {
+ *n.OutputChange = change
+ }
+ if n.OutputValue != nil {
+ *n.OutputValue = change.After
+ }
+ if n.OutputConfigValue != nil {
+ *n.OutputConfigValue = configVal
+ }
if n.OutputState != nil {
- state := &InstanceState{}
*n.OutputState = state
-
- // Apply the diff to the returned state, so the state includes
- // any attribute values that are not computed.
- if !diff.Empty() && n.OutputState != nil {
- *n.OutputState = state.MergeDiff(diff)
- }
}
- return nil, nil
+ return nil, diags.ErrWithWarnings()
}
// EvalReadDataApply is an EvalNode implementation that executes a data
// resource's ReadDataApply method to read data from the data source.
type EvalReadDataApply struct {
- Provider *ResourceProvider
- Output **InstanceState
- Diff **InstanceDiff
- Info *InstanceInfo
+ Addr addrs.ResourceInstance
+ Provider *providers.Interface
+ ProviderAddr addrs.AbsProviderConfig
+ ProviderSchema **ProviderSchema
+ Output **states.ResourceInstanceObject
+ Config *configs.Resource
+ Change **plans.ResourceInstanceChange
+ StateReferences []addrs.Referenceable
}
func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
- // TODO: test
provider := *n.Provider
- diff := *n.Diff
+ change := *n.Change
+ providerSchema := *n.ProviderSchema
+ absAddr := n.Addr.Absolute(ctx.Path())
+
+ var diags tfdiags.Diagnostics
// If the diff is for *destroying* this resource then we'll
// just drop its state and move on, since data resources don't
// support an actual "destroy" action.
- if diff != nil && diff.GetDestroy() {
+ if change != nil && change.Action == plans.Delete {
if n.Output != nil {
*n.Output = nil
}
err := ctx.Hook(func(h Hook) (HookAction, error) {
// We don't have a state yet, so we'll just give the hook an
// empty one to work with.
- return h.PreRefresh(n.Info, &InstanceState{})
+ return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
})
if err != nil {
return nil, err
}
- state, err := provider.ReadDataApply(n.Info, diff)
- if err != nil {
- return nil, fmt.Errorf("%s: %s", n.Info.Id, err)
+ resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
+ TypeName: n.Addr.Resource.Type,
+ Config: change.After,
+ })
+ diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
+ if diags.HasErrors() {
+ return nil, diags.Err()
+ }
+
+ schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+ if schema == nil {
+ // Should be caught during validation, so we don't bother with a pretty error here
+ return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type)
+ }
+
+ newVal := resp.State
+ for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Provider produced invalid object",
+ fmt.Sprintf(
+ "Provider %q planned an invalid value for %s. The result could not be saved.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
+ n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
+ ),
+ ))
+ }
+ if diags.HasErrors() {
+ return nil, diags.Err()
}
err = ctx.Hook(func(h Hook) (HookAction, error) {
- return h.PostRefresh(n.Info, state)
+ return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
})
if err != nil {
return nil, err
}
if n.Output != nil {
- *n.Output = state
+ *n.Output = &states.ResourceInstanceObject{
+ Value: newVal,
+ Status: states.ObjectReady,
+ Dependencies: n.StateReferences,
+ }
}
- return nil, nil
+ return nil, diags.ErrWithWarnings()
}