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 }