]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blobdiff - vendor/github.com/hashicorp/terraform/terraform/eval_read_data.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / terraform / eval_read_data.go
index fb85a284e8dc2c814881a62279ce77f69dd5f233..34f2d60adec83bc2104cbc36bfccce77cd1d9511 100644 (file)
@@ -2,105 +2,320 @@ package terraform
 
 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
                }
@@ -113,27 +328,56 @@ func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
        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()
 }