github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/eval_read_data.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  
     7  	"github.com/zclconf/go-cty/cty"
     8  
     9  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
    12  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans/objchange"
    13  	"github.com/hashicorp/terraform-plugin-sdk/internal/providers"
    14  	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
    15  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    16  )
    17  
    18  // EvalReadData is an EvalNode implementation that deals with the main part
    19  // of the data resource lifecycle: either actually reading from the data source
    20  // or generating a plan to do so.
    21  type EvalReadData struct {
    22  	Addr           addrs.ResourceInstance
    23  	Config         *configs.Resource
    24  	Dependencies   []addrs.Referenceable
    25  	Provider       *providers.Interface
    26  	ProviderAddr   addrs.AbsProviderConfig
    27  	ProviderSchema **ProviderSchema
    28  
    29  	// Planned is set when dealing with data resources that were deferred to
    30  	// the apply walk, to let us see what was planned. If this is set, the
    31  	// evaluation of the config is required to produce a wholly-known
    32  	// configuration which is consistent with the partial object included
    33  	// in this planned change.
    34  	Planned **plans.ResourceInstanceChange
    35  
    36  	// ForcePlanRead, if true, overrides the usual behavior of immediately
    37  	// reading from the data source where possible, instead forcing us to
    38  	// _always_ generate a plan. This is used during the plan walk, since we
    39  	// mustn't actually apply anything there. (The resulting state doesn't
    40  	// get persisted)
    41  	ForcePlanRead bool
    42  
    43  	// The result from this EvalNode has a few different possibilities
    44  	// depending on the input:
    45  	// - If Planned is nil then we assume we're aiming to _produce_ the plan,
    46  	//   and so the following two outcomes are possible:
    47  	//     - OutputChange.Action is plans.NoOp and OutputState is the complete
    48  	//       result of reading from the data source. This is the easy path.
    49  	//     - OutputChange.Action is plans.Read and OutputState is a planned
    50  	//       object placeholder (states.ObjectPlanned). In this case, the
    51  	//       returned change must be recorded in the overral changeset and
    52  	//       eventually passed to another instance of this struct during the
    53  	//       apply walk.
    54  	// - If Planned is non-nil then we assume we're aiming to complete a
    55  	//   planned read from an earlier plan walk. In this case the only possible
    56  	//   non-error outcome is to set Output.Action (if non-nil) to a plans.NoOp
    57  	//   change and put the complete resulting state in OutputState, ready to
    58  	//   be saved in the overall state and used for expression evaluation.
    59  	OutputChange      **plans.ResourceInstanceChange
    60  	OutputValue       *cty.Value
    61  	OutputConfigValue *cty.Value
    62  	OutputState       **states.ResourceInstanceObject
    63  }
    64  
    65  func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
    66  	absAddr := n.Addr.Absolute(ctx.Path())
    67  	log.Printf("[TRACE] EvalReadData: working on %s", absAddr)
    68  
    69  	if n.ProviderSchema == nil || *n.ProviderSchema == nil {
    70  		return nil, fmt.Errorf("provider schema not available for %s", n.Addr)
    71  	}
    72  
    73  	var diags tfdiags.Diagnostics
    74  	var change *plans.ResourceInstanceChange
    75  	var configVal cty.Value
    76  
    77  	// TODO: Do we need to handle Delete changes here? EvalReadDataDiff and
    78  	// EvalReadDataApply did, but it seems like we should handle that via a
    79  	// separate mechanism since it boils down to just deleting the object from
    80  	// the state... and we do that on every plan anyway, forcing the data
    81  	// resource to re-read.
    82  
    83  	config := *n.Config
    84  	provider := *n.Provider
    85  	providerSchema := *n.ProviderSchema
    86  	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
    87  	if schema == nil {
    88  		// Should be caught during validation, so we don't bother with a pretty error here
    89  		return nil, fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.ProviderConfig.Type, n.Addr.Resource.Type)
    90  	}
    91  
    92  	// We'll always start by evaluating the configuration. What we do after
    93  	// that will depend on the evaluation result along with what other inputs
    94  	// we were given.
    95  	objTy := schema.ImpliedType()
    96  	priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time
    97  
    98  	forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
    99  	keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
   100  
   101  	var configDiags tfdiags.Diagnostics
   102  	configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
   103  	diags = diags.Append(configDiags)
   104  	if configDiags.HasErrors() {
   105  		return nil, diags.Err()
   106  	}
   107  
   108  	proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal)
   109  
   110  	// If our configuration contains any unknown values then we must defer the
   111  	// read to the apply phase by producing a "Read" change for this resource,
   112  	// and a placeholder value for it in the state.
   113  	if n.ForcePlanRead || !configVal.IsWhollyKnown() {
   114  		// If the configuration is still unknown when we're applying a planned
   115  		// change then that indicates a bug in Terraform, since we should have
   116  		// everything resolved by now.
   117  		if n.Planned != nil && *n.Planned != nil {
   118  			return nil, fmt.Errorf(
   119  				"configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
   120  				absAddr,
   121  			)
   122  		}
   123  		if n.ForcePlanRead {
   124  			log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr)
   125  		} else {
   126  			log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr)
   127  		}
   128  
   129  		err := ctx.Hook(func(h Hook) (HookAction, error) {
   130  			return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
   131  		})
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  
   136  		change = &plans.ResourceInstanceChange{
   137  			Addr:         absAddr,
   138  			ProviderAddr: n.ProviderAddr,
   139  			Change: plans.Change{
   140  				Action: plans.Read,
   141  				Before: priorVal,
   142  				After:  proposedNewVal,
   143  			},
   144  		}
   145  
   146  		err = ctx.Hook(func(h Hook) (HookAction, error) {
   147  			return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal)
   148  		})
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  
   153  		if n.OutputChange != nil {
   154  			*n.OutputChange = change
   155  		}
   156  		if n.OutputValue != nil {
   157  			*n.OutputValue = change.After
   158  		}
   159  		if n.OutputConfigValue != nil {
   160  			*n.OutputConfigValue = configVal
   161  		}
   162  		if n.OutputState != nil {
   163  			state := &states.ResourceInstanceObject{
   164  				Value:        change.After,
   165  				Status:       states.ObjectPlanned, // because the partial value in the plan must be used for now
   166  				Dependencies: n.Dependencies,
   167  			}
   168  			*n.OutputState = state
   169  		}
   170  
   171  		return nil, diags.ErrWithWarnings()
   172  	}
   173  
   174  	if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read {
   175  		// If any other action gets in here then that's always a bug; this
   176  		// EvalNode only deals with reading.
   177  		return nil, fmt.Errorf(
   178  			"invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)",
   179  			(*n.Planned).Action, absAddr,
   180  		)
   181  	}
   182  
   183  	log.Printf("[TRACE] Re-validating config for %s", absAddr)
   184  	validateResp := provider.ValidateDataSourceConfig(
   185  		providers.ValidateDataSourceConfigRequest{
   186  			TypeName: n.Addr.Resource.Type,
   187  			Config:   configVal,
   188  		},
   189  	)
   190  	if validateResp.Diagnostics.HasErrors() {
   191  		return nil, validateResp.Diagnostics.InConfigBody(n.Config.Config).Err()
   192  	}
   193  
   194  	// If we get down here then our configuration is complete and we're read
   195  	// to actually call the provider to read the data.
   196  	log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr)
   197  
   198  	err := ctx.Hook(func(h Hook) (HookAction, error) {
   199  		// We don't have a state yet, so we'll just give the hook an
   200  		// empty one to work with.
   201  		return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
   202  	})
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
   208  		TypeName: n.Addr.Resource.Type,
   209  		Config:   configVal,
   210  	})
   211  	diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
   212  	if diags.HasErrors() {
   213  		return nil, diags.Err()
   214  	}
   215  	newVal := resp.State
   216  	if newVal == cty.NilVal {
   217  		// This can happen with incompletely-configured mocks. We'll allow it
   218  		// and treat it as an alias for a properly-typed null value.
   219  		newVal = cty.NullVal(schema.ImpliedType())
   220  	}
   221  
   222  	for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
   223  		diags = diags.Append(tfdiags.Sourceless(
   224  			tfdiags.Error,
   225  			"Provider produced invalid object",
   226  			fmt.Sprintf(
   227  				"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.",
   228  				n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
   229  			),
   230  		))
   231  	}
   232  	if diags.HasErrors() {
   233  		return nil, diags.Err()
   234  	}
   235  
   236  	if newVal.IsNull() {
   237  		diags = diags.Append(tfdiags.Sourceless(
   238  			tfdiags.Error,
   239  			"Provider produced null object",
   240  			fmt.Sprintf(
   241  				"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.",
   242  				n.ProviderAddr.ProviderConfig.Type, absAddr,
   243  			),
   244  		))
   245  	}
   246  	if !newVal.IsWhollyKnown() {
   247  		diags = diags.Append(tfdiags.Sourceless(
   248  			tfdiags.Error,
   249  			"Provider produced invalid object",
   250  			fmt.Sprintf(
   251  				"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.",
   252  				n.ProviderAddr.ProviderConfig.Type, absAddr,
   253  			),
   254  		))
   255  
   256  		// We'll still save the object, but we need to eliminate any unknown
   257  		// values first because we can't serialize them in the state file.
   258  		// Note that this may cause set elements to be coalesced if they
   259  		// differed only by having unknown values, but we don't worry about
   260  		// that here because we're saving the value only for inspection
   261  		// purposes; the error we added above will halt the graph walk.
   262  		newVal = cty.UnknownAsNull(newVal)
   263  	}
   264  
   265  	// Since we've completed the read, we actually have no change to make, but
   266  	// we'll produce a NoOp one anyway to preserve the usual flow of the
   267  	// plan phase and allow it to produce a complete plan.
   268  	change = &plans.ResourceInstanceChange{
   269  		Addr:         absAddr,
   270  		ProviderAddr: n.ProviderAddr,
   271  		Change: plans.Change{
   272  			Action: plans.NoOp,
   273  			Before: newVal,
   274  			After:  newVal,
   275  		},
   276  	}
   277  	state := &states.ResourceInstanceObject{
   278  		Value:        change.After,
   279  		Status:       states.ObjectReady, // because we completed the read from the provider
   280  		Dependencies: n.Dependencies,
   281  	}
   282  
   283  	err = ctx.Hook(func(h Hook) (HookAction, error) {
   284  		return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
   285  	})
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	if n.OutputChange != nil {
   291  		*n.OutputChange = change
   292  	}
   293  	if n.OutputValue != nil {
   294  		*n.OutputValue = change.After
   295  	}
   296  	if n.OutputConfigValue != nil {
   297  		*n.OutputConfigValue = configVal
   298  	}
   299  	if n.OutputState != nil {
   300  		*n.OutputState = state
   301  	}
   302  
   303  	return nil, diags.ErrWithWarnings()
   304  }
   305  
   306  // EvalReadDataApply is an EvalNode implementation that executes a data
   307  // resource's ReadDataApply method to read data from the data source.
   308  type EvalReadDataApply struct {
   309  	Addr            addrs.ResourceInstance
   310  	Provider        *providers.Interface
   311  	ProviderAddr    addrs.AbsProviderConfig
   312  	ProviderSchema  **ProviderSchema
   313  	Output          **states.ResourceInstanceObject
   314  	Config          *configs.Resource
   315  	Change          **plans.ResourceInstanceChange
   316  	StateReferences []addrs.Referenceable
   317  }
   318  
   319  func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
   320  	provider := *n.Provider
   321  	change := *n.Change
   322  	providerSchema := *n.ProviderSchema
   323  	absAddr := n.Addr.Absolute(ctx.Path())
   324  
   325  	var diags tfdiags.Diagnostics
   326  
   327  	// If the diff is for *destroying* this resource then we'll
   328  	// just drop its state and move on, since data resources don't
   329  	// support an actual "destroy" action.
   330  	if change != nil && change.Action == plans.Delete {
   331  		if n.Output != nil {
   332  			*n.Output = nil
   333  		}
   334  		return nil, nil
   335  	}
   336  
   337  	// For the purpose of external hooks we present a data apply as a
   338  	// "Refresh" rather than an "Apply" because creating a data source
   339  	// is presented to users/callers as a "read" operation.
   340  	err := ctx.Hook(func(h Hook) (HookAction, error) {
   341  		// We don't have a state yet, so we'll just give the hook an
   342  		// empty one to work with.
   343  		return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
   344  	})
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
   350  		TypeName: n.Addr.Resource.Type,
   351  		Config:   change.After,
   352  	})
   353  	diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
   354  	if diags.HasErrors() {
   355  		return nil, diags.Err()
   356  	}
   357  
   358  	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
   359  	if schema == nil {
   360  		// Should be caught during validation, so we don't bother with a pretty error here
   361  		return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type)
   362  	}
   363  
   364  	newVal := resp.State
   365  	for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
   366  		diags = diags.Append(tfdiags.Sourceless(
   367  			tfdiags.Error,
   368  			"Provider produced invalid object",
   369  			fmt.Sprintf(
   370  				"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.",
   371  				n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
   372  			),
   373  		))
   374  	}
   375  	if diags.HasErrors() {
   376  		return nil, diags.Err()
   377  	}
   378  
   379  	err = ctx.Hook(func(h Hook) (HookAction, error) {
   380  		return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
   381  	})
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	if n.Output != nil {
   387  		*n.Output = &states.ResourceInstanceObject{
   388  			Value:        newVal,
   389  			Status:       states.ObjectReady,
   390  			Dependencies: n.StateReferences,
   391  		}
   392  	}
   393  
   394  	return nil, diags.ErrWithWarnings()
   395  }