1package terraform 2 3import ( 4 "fmt" 5 "log" 6 7 "github.com/zclconf/go-cty/cty" 8 9 "github.com/hashicorp/terraform/addrs" 10 "github.com/hashicorp/terraform/configs" 11 "github.com/hashicorp/terraform/plans" 12 "github.com/hashicorp/terraform/plans/objchange" 13 "github.com/hashicorp/terraform/providers" 14 "github.com/hashicorp/terraform/states" 15 "github.com/hashicorp/terraform/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. 21type 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 65func (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 keyData := EvalDataForInstanceKey(n.Addr.Key) 99 100 var configDiags tfdiags.Diagnostics 101 configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) 102 diags = diags.Append(configDiags) 103 if configDiags.HasErrors() { 104 return nil, diags.Err() 105 } 106 107 proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal) 108 109 // If our configuration contains any unknown values then we must defer the 110 // read to the apply phase by producing a "Read" change for this resource, 111 // and a placeholder value for it in the state. 112 if n.ForcePlanRead || !configVal.IsWhollyKnown() { 113 // If the configuration is still unknown when we're applying a planned 114 // change then that indicates a bug in Terraform, since we should have 115 // everything resolved by now. 116 if n.Planned != nil && *n.Planned != nil { 117 return nil, fmt.Errorf( 118 "configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)", 119 absAddr, 120 ) 121 } 122 if n.ForcePlanRead { 123 log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr) 124 } else { 125 log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr) 126 } 127 128 err := ctx.Hook(func(h Hook) (HookAction, error) { 129 return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal) 130 }) 131 if err != nil { 132 return nil, err 133 } 134 135 change = &plans.ResourceInstanceChange{ 136 Addr: absAddr, 137 ProviderAddr: n.ProviderAddr, 138 Change: plans.Change{ 139 Action: plans.Read, 140 Before: priorVal, 141 After: proposedNewVal, 142 }, 143 } 144 145 err = ctx.Hook(func(h Hook) (HookAction, error) { 146 return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal) 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 if n.OutputChange != nil { 153 *n.OutputChange = change 154 } 155 if n.OutputValue != nil { 156 *n.OutputValue = change.After 157 } 158 if n.OutputConfigValue != nil { 159 *n.OutputConfigValue = configVal 160 } 161 if n.OutputState != nil { 162 state := &states.ResourceInstanceObject{ 163 Value: change.After, 164 Status: states.ObjectPlanned, // because the partial value in the plan must be used for now 165 Dependencies: n.Dependencies, 166 } 167 *n.OutputState = state 168 } 169 170 return nil, diags.ErrWithWarnings() 171 } 172 173 if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read { 174 // If any other action gets in here then that's always a bug; this 175 // EvalNode only deals with reading. 176 return nil, fmt.Errorf( 177 "invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)", 178 (*n.Planned).Action, absAddr, 179 ) 180 } 181 182 // If we get down here then our configuration is complete and we're read 183 // to actually call the provider to read the data. 184 log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr) 185 186 err := ctx.Hook(func(h Hook) (HookAction, error) { 187 // We don't have a state yet, so we'll just give the hook an 188 // empty one to work with. 189 return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) 190 }) 191 if err != nil { 192 return nil, err 193 } 194 195 resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ 196 TypeName: n.Addr.Resource.Type, 197 Config: configVal, 198 }) 199 diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) 200 if diags.HasErrors() { 201 return nil, diags.Err() 202 } 203 newVal := resp.State 204 if newVal == cty.NilVal { 205 // This can happen with incompletely-configured mocks. We'll allow it 206 // and treat it as an alias for a properly-typed null value. 207 newVal = cty.NullVal(schema.ImpliedType()) 208 } 209 210 for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { 211 diags = diags.Append(tfdiags.Sourceless( 212 tfdiags.Error, 213 "Provider produced invalid object", 214 fmt.Sprintf( 215 "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.", 216 n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), 217 ), 218 )) 219 } 220 if diags.HasErrors() { 221 return nil, diags.Err() 222 } 223 224 if newVal.IsNull() { 225 diags = diags.Append(tfdiags.Sourceless( 226 tfdiags.Error, 227 "Provider produced null object", 228 fmt.Sprintf( 229 "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.", 230 n.ProviderAddr.ProviderConfig.Type, absAddr, 231 ), 232 )) 233 } 234 if !newVal.IsWhollyKnown() { 235 diags = diags.Append(tfdiags.Sourceless( 236 tfdiags.Error, 237 "Provider produced invalid object", 238 fmt.Sprintf( 239 "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.", 240 n.ProviderAddr.ProviderConfig.Type, absAddr, 241 ), 242 )) 243 244 // We'll still save the object, but we need to eliminate any unknown 245 // values first because we can't serialize them in the state file. 246 // Note that this may cause set elements to be coalesced if they 247 // differed only by having unknown values, but we don't worry about 248 // that here because we're saving the value only for inspection 249 // purposes; the error we added above will halt the graph walk. 250 newVal = cty.UnknownAsNull(newVal) 251 } 252 253 // Since we've completed the read, we actually have no change to make, but 254 // we'll produce a NoOp one anyway to preserve the usual flow of the 255 // plan phase and allow it to produce a complete plan. 256 change = &plans.ResourceInstanceChange{ 257 Addr: absAddr, 258 ProviderAddr: n.ProviderAddr, 259 Change: plans.Change{ 260 Action: plans.NoOp, 261 Before: newVal, 262 After: newVal, 263 }, 264 } 265 state := &states.ResourceInstanceObject{ 266 Value: change.After, 267 Status: states.ObjectReady, // because we completed the read from the provider 268 Dependencies: n.Dependencies, 269 } 270 271 err = ctx.Hook(func(h Hook) (HookAction, error) { 272 return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) 273 }) 274 if err != nil { 275 return nil, err 276 } 277 278 if n.OutputChange != nil { 279 *n.OutputChange = change 280 } 281 if n.OutputValue != nil { 282 *n.OutputValue = change.After 283 } 284 if n.OutputConfigValue != nil { 285 *n.OutputConfigValue = configVal 286 } 287 if n.OutputState != nil { 288 *n.OutputState = state 289 } 290 291 return nil, diags.ErrWithWarnings() 292} 293 294// EvalReadDataApply is an EvalNode implementation that executes a data 295// resource's ReadDataApply method to read data from the data source. 296type EvalReadDataApply struct { 297 Addr addrs.ResourceInstance 298 Provider *providers.Interface 299 ProviderAddr addrs.AbsProviderConfig 300 ProviderSchema **ProviderSchema 301 Output **states.ResourceInstanceObject 302 Config *configs.Resource 303 Change **plans.ResourceInstanceChange 304 StateReferences []addrs.Referenceable 305} 306 307func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) { 308 provider := *n.Provider 309 change := *n.Change 310 providerSchema := *n.ProviderSchema 311 absAddr := n.Addr.Absolute(ctx.Path()) 312 313 var diags tfdiags.Diagnostics 314 315 // If the diff is for *destroying* this resource then we'll 316 // just drop its state and move on, since data resources don't 317 // support an actual "destroy" action. 318 if change != nil && change.Action == plans.Delete { 319 if n.Output != nil { 320 *n.Output = nil 321 } 322 return nil, nil 323 } 324 325 // For the purpose of external hooks we present a data apply as a 326 // "Refresh" rather than an "Apply" because creating a data source 327 // is presented to users/callers as a "read" operation. 328 err := ctx.Hook(func(h Hook) (HookAction, error) { 329 // We don't have a state yet, so we'll just give the hook an 330 // empty one to work with. 331 return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) 332 }) 333 if err != nil { 334 return nil, err 335 } 336 337 resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ 338 TypeName: n.Addr.Resource.Type, 339 Config: change.After, 340 }) 341 diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) 342 if diags.HasErrors() { 343 return nil, diags.Err() 344 } 345 346 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) 347 if schema == nil { 348 // Should be caught during validation, so we don't bother with a pretty error here 349 return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) 350 } 351 352 newVal := resp.State 353 for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { 354 diags = diags.Append(tfdiags.Sourceless( 355 tfdiags.Error, 356 "Provider produced invalid object", 357 fmt.Sprintf( 358 "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.", 359 n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), 360 ), 361 )) 362 } 363 if diags.HasErrors() { 364 return nil, diags.Err() 365 } 366 367 err = ctx.Hook(func(h Hook) (HookAction, error) { 368 return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) 369 }) 370 if err != nil { 371 return nil, err 372 } 373 374 if n.Output != nil { 375 *n.Output = &states.ResourceInstanceObject{ 376 Value: newVal, 377 Status: states.ObjectReady, 378 Dependencies: n.StateReferences, 379 } 380 } 381 382 return nil, diags.ErrWithWarnings() 383} 384