github.com/ben-turner/terraform@v0.11.8-0.20180503104400-0cc9e050ecd4/helper/schema/resource.go (about) 1 package schema 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "strconv" 8 9 "github.com/hashicorp/terraform/config" 10 "github.com/hashicorp/terraform/terraform" 11 ) 12 13 // Resource represents a thing in Terraform that has a set of configurable 14 // attributes and a lifecycle (create, read, update, delete). 15 // 16 // The Resource schema is an abstraction that allows provider writers to 17 // worry only about CRUD operations while off-loading validation, diff 18 // generation, etc. to this higher level library. 19 // 20 // In spite of the name, this struct is not used only for terraform resources, 21 // but also for data sources. In the case of data sources, the Create, 22 // Update and Delete functions must not be provided. 23 type Resource struct { 24 // Schema is the schema for the configuration of this resource. 25 // 26 // The keys of this map are the configuration keys, and the values 27 // describe the schema of the configuration value. 28 // 29 // The schema is used to represent both configurable data as well 30 // as data that might be computed in the process of creating this 31 // resource. 32 Schema map[string]*Schema 33 34 // SchemaVersion is the version number for this resource's Schema 35 // definition. The current SchemaVersion stored in the state for each 36 // resource. Provider authors can increment this version number 37 // when Schema semantics change. If the State's SchemaVersion is less than 38 // the current SchemaVersion, the InstanceState is yielded to the 39 // MigrateState callback, where the provider can make whatever changes it 40 // needs to update the state to be compatible to the latest version of the 41 // Schema. 42 // 43 // When unset, SchemaVersion defaults to 0, so provider authors can start 44 // their Versioning at any integer >= 1 45 SchemaVersion int 46 47 // MigrateState is responsible for updating an InstanceState with an old 48 // version to the format expected by the current version of the Schema. 49 // 50 // It is called during Refresh if the State's stored SchemaVersion is less 51 // than the current SchemaVersion of the Resource. 52 // 53 // The function is yielded the state's stored SchemaVersion and a pointer to 54 // the InstanceState that needs updating, as well as the configured 55 // provider's configured meta interface{}, in case the migration process 56 // needs to make any remote API calls. 57 MigrateState StateMigrateFunc 58 59 // The functions below are the CRUD operations for this resource. 60 // 61 // The only optional operation is Update. If Update is not implemented, 62 // then updates will not be supported for this resource. 63 // 64 // The ResourceData parameter in the functions below are used to 65 // query configuration and changes for the resource as well as to set 66 // the ID, computed data, etc. 67 // 68 // The interface{} parameter is the result of the ConfigureFunc in 69 // the provider for this resource. If the provider does not define 70 // a ConfigureFunc, this will be nil. This parameter should be used 71 // to store API clients, configuration structures, etc. 72 // 73 // If any errors occur during each of the operation, an error should be 74 // returned. If a resource was partially updated, be careful to enable 75 // partial state mode for ResourceData and use it accordingly. 76 // 77 // Exists is a function that is called to check if a resource still 78 // exists. If this returns false, then this will affect the diff 79 // accordingly. If this function isn't set, it will not be called. It 80 // is highly recommended to set it. The *ResourceData passed to Exists 81 // should _not_ be modified. 82 Create CreateFunc 83 Read ReadFunc 84 Update UpdateFunc 85 Delete DeleteFunc 86 Exists ExistsFunc 87 88 // CustomizeDiff is a custom function for working with the diff that 89 // Terraform has created for this resource - it can be used to customize the 90 // diff that has been created, diff values not controlled by configuration, 91 // or even veto the diff altogether and abort the plan. It is passed a 92 // *ResourceDiff, a structure similar to ResourceData but lacking most write 93 // functions like Set, while introducing new functions that work with the 94 // diff such as SetNew, SetNewComputed, and ForceNew. 95 // 96 // The phases Terraform runs this in, and the state available via functions 97 // like Get and GetChange, are as follows: 98 // 99 // * New resource: One run with no state 100 // * Existing resource: One run with state 101 // * Existing resource, forced new: One run with state (before ForceNew), 102 // then one run without state (as if new resource) 103 // * Tainted resource: No runs (custom diff logic is skipped) 104 // * Destroy: No runs (standard diff logic is skipped on destroy diffs) 105 // 106 // This function needs to be resilient to support all scenarios. 107 // 108 // If this function needs to access external API resources, remember to flag 109 // the RequiresRefresh attribute mentioned below to ensure that 110 // -refresh=false is blocked when running plan or apply, as this means that 111 // this resource requires refresh-like behaviour to work effectively. 112 // 113 // For the most part, only computed fields can be customized by this 114 // function. 115 // 116 // This function is only allowed on regular resources (not data sources). 117 CustomizeDiff CustomizeDiffFunc 118 119 // Importer is the ResourceImporter implementation for this resource. 120 // If this is nil, then this resource does not support importing. If 121 // this is non-nil, then it supports importing and ResourceImporter 122 // must be validated. The validity of ResourceImporter is verified 123 // by InternalValidate on Resource. 124 Importer *ResourceImporter 125 126 // If non-empty, this string is emitted as a warning during Validate. 127 // This is a private interface for now, for use by DataSourceResourceShim, 128 // and not for general use. (But maybe later...) 129 deprecationMessage string 130 131 // Timeouts allow users to specify specific time durations in which an 132 // operation should time out, to allow them to extend an action to suit their 133 // usage. For example, a user may specify a large Creation timeout for their 134 // AWS RDS Instance due to it's size, or restoring from a snapshot. 135 // Resource implementors must enable Timeout support by adding the allowed 136 // actions (Create, Read, Update, Delete, Default) to the Resource struct, and 137 // accessing them in the matching methods. 138 Timeouts *ResourceTimeout 139 } 140 141 // See Resource documentation. 142 type CreateFunc func(*ResourceData, interface{}) error 143 144 // See Resource documentation. 145 type ReadFunc func(*ResourceData, interface{}) error 146 147 // See Resource documentation. 148 type UpdateFunc func(*ResourceData, interface{}) error 149 150 // See Resource documentation. 151 type DeleteFunc func(*ResourceData, interface{}) error 152 153 // See Resource documentation. 154 type ExistsFunc func(*ResourceData, interface{}) (bool, error) 155 156 // See Resource documentation. 157 type StateMigrateFunc func( 158 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) 159 160 // See Resource documentation. 161 type CustomizeDiffFunc func(*ResourceDiff, interface{}) error 162 163 // Apply creates, updates, and/or deletes a resource. 164 func (r *Resource) Apply( 165 s *terraform.InstanceState, 166 d *terraform.InstanceDiff, 167 meta interface{}) (*terraform.InstanceState, error) { 168 data, err := schemaMap(r.Schema).Data(s, d) 169 if err != nil { 170 return s, err 171 } 172 173 // Instance Diff shoould have the timeout info, need to copy it over to the 174 // ResourceData meta 175 rt := ResourceTimeout{} 176 if _, ok := d.Meta[TimeoutKey]; ok { 177 if err := rt.DiffDecode(d); err != nil { 178 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 179 } 180 } else if s != nil { 181 if _, ok := s.Meta[TimeoutKey]; ok { 182 if err := rt.StateDecode(s); err != nil { 183 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 184 } 185 } 186 } else { 187 log.Printf("[DEBUG] No meta timeoutkey found in Apply()") 188 } 189 data.timeouts = &rt 190 191 if s == nil { 192 // The Terraform API dictates that this should never happen, but 193 // it doesn't hurt to be safe in this case. 194 s = new(terraform.InstanceState) 195 } 196 197 if d.Destroy || d.RequiresNew() { 198 if s.ID != "" { 199 // Destroy the resource since it is created 200 if err := r.Delete(data, meta); err != nil { 201 return r.recordCurrentSchemaVersion(data.State()), err 202 } 203 204 // Make sure the ID is gone. 205 data.SetId("") 206 } 207 208 // If we're only destroying, and not creating, then return 209 // now since we're done! 210 if !d.RequiresNew() { 211 return nil, nil 212 } 213 214 // Reset the data to be stateless since we just destroyed 215 data, err = schemaMap(r.Schema).Data(nil, d) 216 // data was reset, need to re-apply the parsed timeouts 217 data.timeouts = &rt 218 if err != nil { 219 return nil, err 220 } 221 } 222 223 err = nil 224 if data.Id() == "" { 225 // We're creating, it is a new resource. 226 data.MarkNewResource() 227 err = r.Create(data, meta) 228 } else { 229 if r.Update == nil { 230 return s, fmt.Errorf("doesn't support update") 231 } 232 233 err = r.Update(data, meta) 234 } 235 236 return r.recordCurrentSchemaVersion(data.State()), err 237 } 238 239 // Diff returns a diff of this resource. 240 func (r *Resource) Diff( 241 s *terraform.InstanceState, 242 c *terraform.ResourceConfig, 243 meta interface{}) (*terraform.InstanceDiff, error) { 244 245 t := &ResourceTimeout{} 246 err := t.ConfigDecode(r, c) 247 248 if err != nil { 249 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) 250 } 251 252 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta) 253 if err != nil { 254 return instanceDiff, err 255 } 256 257 if instanceDiff != nil { 258 if err := t.DiffEncode(instanceDiff); err != nil { 259 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) 260 } 261 } else { 262 log.Printf("[DEBUG] Instance Diff is nil in Diff()") 263 } 264 265 return instanceDiff, err 266 } 267 268 // Validate validates the resource configuration against the schema. 269 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { 270 warns, errs := schemaMap(r.Schema).Validate(c) 271 272 if r.deprecationMessage != "" { 273 warns = append(warns, r.deprecationMessage) 274 } 275 276 return warns, errs 277 } 278 279 // ReadDataApply loads the data for a data source, given a diff that 280 // describes the configuration arguments and desired computed attributes. 281 func (r *Resource) ReadDataApply( 282 d *terraform.InstanceDiff, 283 meta interface{}, 284 ) (*terraform.InstanceState, error) { 285 // Data sources are always built completely from scratch 286 // on each read, so the source state is always nil. 287 data, err := schemaMap(r.Schema).Data(nil, d) 288 if err != nil { 289 return nil, err 290 } 291 292 err = r.Read(data, meta) 293 state := data.State() 294 if state != nil && state.ID == "" { 295 // Data sources can set an ID if they want, but they aren't 296 // required to; we'll provide a placeholder if they don't, 297 // to preserve the invariant that all resources have non-empty 298 // ids. 299 state.ID = "-" 300 } 301 302 return r.recordCurrentSchemaVersion(state), err 303 } 304 305 // Refresh refreshes the state of the resource. 306 func (r *Resource) Refresh( 307 s *terraform.InstanceState, 308 meta interface{}) (*terraform.InstanceState, error) { 309 // If the ID is already somehow blank, it doesn't exist 310 if s.ID == "" { 311 return nil, nil 312 } 313 314 rt := ResourceTimeout{} 315 if _, ok := s.Meta[TimeoutKey]; ok { 316 if err := rt.StateDecode(s); err != nil { 317 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 318 } 319 } 320 321 if r.Exists != nil { 322 // Make a copy of data so that if it is modified it doesn't 323 // affect our Read later. 324 data, err := schemaMap(r.Schema).Data(s, nil) 325 data.timeouts = &rt 326 327 if err != nil { 328 return s, err 329 } 330 331 exists, err := r.Exists(data, meta) 332 if err != nil { 333 return s, err 334 } 335 if !exists { 336 return nil, nil 337 } 338 } 339 340 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) 341 if needsMigration && r.MigrateState != nil { 342 s, err := r.MigrateState(stateSchemaVersion, s, meta) 343 if err != nil { 344 return s, err 345 } 346 } 347 348 data, err := schemaMap(r.Schema).Data(s, nil) 349 data.timeouts = &rt 350 if err != nil { 351 return s, err 352 } 353 354 err = r.Read(data, meta) 355 state := data.State() 356 if state != nil && state.ID == "" { 357 state = nil 358 } 359 360 return r.recordCurrentSchemaVersion(state), err 361 } 362 363 // InternalValidate should be called to validate the structure 364 // of the resource. 365 // 366 // This should be called in a unit test for any resource to verify 367 // before release that a resource is properly configured for use with 368 // this library. 369 // 370 // Provider.InternalValidate() will automatically call this for all of 371 // the resources it manages, so you don't need to call this manually if it 372 // is part of a Provider. 373 func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { 374 if r == nil { 375 return errors.New("resource is nil") 376 } 377 378 if !writable { 379 if r.Create != nil || r.Update != nil || r.Delete != nil { 380 return fmt.Errorf("must not implement Create, Update or Delete") 381 } 382 383 // CustomizeDiff cannot be defined for read-only resources 384 if r.CustomizeDiff != nil { 385 return fmt.Errorf("cannot implement CustomizeDiff") 386 } 387 } 388 389 tsm := topSchemaMap 390 391 if r.isTopLevel() && writable { 392 // All non-Computed attributes must be ForceNew if Update is not defined 393 if r.Update == nil { 394 nonForceNewAttrs := make([]string, 0) 395 for k, v := range r.Schema { 396 if !v.ForceNew && !v.Computed { 397 nonForceNewAttrs = append(nonForceNewAttrs, k) 398 } 399 } 400 if len(nonForceNewAttrs) > 0 { 401 return fmt.Errorf( 402 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) 403 } 404 } else { 405 nonUpdateableAttrs := make([]string, 0) 406 for k, v := range r.Schema { 407 if v.ForceNew || v.Computed && !v.Optional { 408 nonUpdateableAttrs = append(nonUpdateableAttrs, k) 409 } 410 } 411 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) 412 if updateableAttrs == 0 { 413 return fmt.Errorf( 414 "All fields are ForceNew or Computed w/out Optional, Update is superfluous") 415 } 416 } 417 418 tsm = schemaMap(r.Schema) 419 420 // Destroy, and Read are required 421 if r.Read == nil { 422 return fmt.Errorf("Read must be implemented") 423 } 424 if r.Delete == nil { 425 return fmt.Errorf("Delete must be implemented") 426 } 427 428 // If we have an importer, we need to verify the importer. 429 if r.Importer != nil { 430 if err := r.Importer.InternalValidate(); err != nil { 431 return err 432 } 433 } 434 435 for k, f := range tsm { 436 if isReservedResourceFieldName(k, f) { 437 return fmt.Errorf("%s is a reserved field name", k) 438 } 439 } 440 } 441 442 // Data source 443 if r.isTopLevel() && !writable { 444 tsm = schemaMap(r.Schema) 445 for k, _ := range tsm { 446 if isReservedDataSourceFieldName(k) { 447 return fmt.Errorf("%s is a reserved field name", k) 448 } 449 } 450 } 451 452 return schemaMap(r.Schema).InternalValidate(tsm) 453 } 454 455 func isReservedDataSourceFieldName(name string) bool { 456 for _, reservedName := range config.ReservedDataSourceFields { 457 if name == reservedName { 458 return true 459 } 460 } 461 return false 462 } 463 464 func isReservedResourceFieldName(name string, s *Schema) bool { 465 // Allow phasing out "id" 466 // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415 467 if name == "id" && (s.Deprecated != "" || s.Removed != "") { 468 return false 469 } 470 471 for _, reservedName := range config.ReservedResourceFields { 472 if name == reservedName { 473 return true 474 } 475 } 476 return false 477 } 478 479 // Data returns a ResourceData struct for this Resource. Each return value 480 // is a separate copy and can be safely modified differently. 481 // 482 // The data returned from this function has no actual affect on the Resource 483 // itself (including the state given to this function). 484 // 485 // This function is useful for unit tests and ResourceImporter functions. 486 func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { 487 result, err := schemaMap(r.Schema).Data(s, nil) 488 if err != nil { 489 // At the time of writing, this isn't possible (Data never returns 490 // non-nil errors). We panic to find this in the future if we have to. 491 // I don't see a reason for Data to ever return an error. 492 panic(err) 493 } 494 495 // load the Resource timeouts 496 result.timeouts = r.Timeouts 497 if result.timeouts == nil { 498 result.timeouts = &ResourceTimeout{} 499 } 500 501 // Set the schema version to latest by default 502 result.meta = map[string]interface{}{ 503 "schema_version": strconv.Itoa(r.SchemaVersion), 504 } 505 506 return result 507 } 508 509 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing 510 // 511 // TODO: May be able to be removed with the above ResourceData function. 512 func (r *Resource) TestResourceData() *ResourceData { 513 return &ResourceData{ 514 schema: r.Schema, 515 } 516 } 517 518 // Returns true if the resource is "top level" i.e. not a sub-resource. 519 func (r *Resource) isTopLevel() bool { 520 // TODO: This is a heuristic; replace with a definitive attribute? 521 return (r.Create != nil || r.Read != nil) 522 } 523 524 // Determines if a given InstanceState needs to be migrated by checking the 525 // stored version number with the current SchemaVersion 526 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { 527 // Get the raw interface{} value for the schema version. If it doesn't 528 // exist or is nil then set it to zero. 529 raw := is.Meta["schema_version"] 530 if raw == nil { 531 raw = "0" 532 } 533 534 // Try to convert it to a string. If it isn't a string then we pretend 535 // that it isn't set at all. It should never not be a string unless it 536 // was manually tampered with. 537 rawString, ok := raw.(string) 538 if !ok { 539 rawString = "0" 540 } 541 542 stateSchemaVersion, _ := strconv.Atoi(rawString) 543 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion 544 } 545 546 func (r *Resource) recordCurrentSchemaVersion( 547 state *terraform.InstanceState) *terraform.InstanceState { 548 if state != nil && r.SchemaVersion > 0 { 549 if state.Meta == nil { 550 state.Meta = make(map[string]interface{}) 551 } 552 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) 553 } 554 return state 555 } 556 557 // Noop is a convenience implementation of resource function which takes 558 // no action and returns no error. 559 func Noop(*ResourceData, interface{}) error { 560 return nil 561 } 562 563 // RemoveFromState is a convenience implementation of a resource function 564 // which sets the resource ID to empty string (to remove it from state) 565 // and returns no error. 566 func RemoveFromState(d *ResourceData, _ interface{}) error { 567 d.SetId("") 568 return nil 569 }