package terraform import ( "fmt" "log" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) // evalReadData implements shared methods and data for the individual data // source eval nodes. type evalReadData struct { Addr addrs.ResourceInstance Config *configs.Resource Provider *providers.Interface ProviderAddr addrs.AbsProviderConfig ProviderMetas map[addrs.Provider]*configs.ProviderMeta 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 // State is the current state for the data source, and is updated once the // new state has been read. // While data sources are read-only, we need to start with the prior state // to determine if we have a change or not. If we needed to read a new // value, but it still matches the previous state, then we can record a // NoNop change. If the states don't match then we record a Read change so // that the new value is applied to the state. State **states.ResourceInstanceObject // Output change records any change for this data source, which is // interpreted differently than changes for managed resources. // - During Refresh, this change is only used to correctly evaluate // references to the data source, but it is not saved. // - If a planned change has the action of plans.Read, it indicates that the // data source could not be evaluated yet, and reading is being deferred to // apply. // - If planned action is plans.Update, it indicates that the data source // was read, and the result needs to be stored in state during apply. OutputChange **plans.ResourceInstanceChange // dependsOn stores the list of transitive resource addresses that any // configuration depends_on references may resolve to. This is used to // determine if there are any changes that will force this data sources to // be deferred to apply. dependsOn []addrs.ConfigResource } // readDataSource handles everything needed to call ReadDataSource on the provider. // A previously evaluated configVal can be passed in, or a new one is generated // from the resource configuration. func (n *evalReadData) readDataSource(ctx EvalContext, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var newVal cty.Value config := *n.Config absAddr := n.Addr.Absolute(ctx.Path()) if n.ProviderSchema == nil || *n.ProviderSchema == nil { diags = diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr)) return newVal, diags } 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 diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.Provider.String(), n.Addr.Resource.Type)) return newVal, diags } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { return newVal, diags } log.Printf("[TRACE] EvalReadData: Re-validating config for %s", absAddr) validateResp := provider.ValidateDataSourceConfig( providers.ValidateDataSourceConfigRequest{ TypeName: n.Addr.Resource.Type, Config: configVal, }, ) if validateResp.Diagnostics.HasErrors() { return newVal, validateResp.Diagnostics.InConfigBody(config.Config) } // 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) resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ TypeName: n.Addr.Resource.Type, Config: configVal, ProviderMeta: metaConfigVal, }) diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config)) if diags.HasErrors() { return newVal, diags } 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.Provider.String(), tfdiags.FormatErrorPrefixed(err, absAddr.String()), ), )) } if diags.HasErrors() { return newVal, diags } 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.Provider.String(), absAddr, ), )) } if !newVal.IsNull() && !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.Provider.String(), 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) } return newVal, diags } func (n *evalReadData) providerMetas(ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics metaConfigVal := cty.NullVal(cty.DynamicPseudoType) if n.ProviderMetas != nil { if m, ok := n.ProviderMetas[n.ProviderAddr.Provider]; ok && m != nil { // if the provider doesn't support this feature, throw an error if (*n.ProviderSchema).ProviderMeta == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", n.ProviderAddr.Provider.String()), Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr), Subject: &m.ProviderRange, }) } else { var configDiags tfdiags.Diagnostics metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, (*n.ProviderSchema).ProviderMeta, nil, EvalDataForNoInstanceKey) diags = diags.Append(configDiags) } } } return metaConfigVal, diags }