github.com/ticketmaster/terraform@v0.10.0-beta2.0.20170711045249-a12daf5aba4f/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 // Importer is the ResourceImporter implementation for this resource. 89 // If this is nil, then this resource does not support importing. If 90 // this is non-nil, then it supports importing and ResourceImporter 91 // must be validated. The validity of ResourceImporter is verified 92 // by InternalValidate on Resource. 93 Importer *ResourceImporter 94 95 // If non-empty, this string is emitted as a warning during Validate. 96 // This is a private interface for now, for use by DataSourceResourceShim, 97 // and not for general use. (But maybe later...) 98 deprecationMessage string 99 100 // Timeouts allow users to specify specific time durations in which an 101 // operation should time out, to allow them to extend an action to suit their 102 // usage. For example, a user may specify a large Creation timeout for their 103 // AWS RDS Instance due to it's size, or restoring from a snapshot. 104 // Resource implementors must enable Timeout support by adding the allowed 105 // actions (Create, Read, Update, Delete, Default) to the Resource struct, and 106 // accessing them in the matching methods. 107 Timeouts *ResourceTimeout 108 } 109 110 // See Resource documentation. 111 type CreateFunc func(*ResourceData, interface{}) error 112 113 // See Resource documentation. 114 type ReadFunc func(*ResourceData, interface{}) error 115 116 // See Resource documentation. 117 type UpdateFunc func(*ResourceData, interface{}) error 118 119 // See Resource documentation. 120 type DeleteFunc func(*ResourceData, interface{}) error 121 122 // See Resource documentation. 123 type ExistsFunc func(*ResourceData, interface{}) (bool, error) 124 125 // See Resource documentation. 126 type StateMigrateFunc func( 127 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) 128 129 // Apply creates, updates, and/or deletes a resource. 130 func (r *Resource) Apply( 131 s *terraform.InstanceState, 132 d *terraform.InstanceDiff, 133 meta interface{}) (*terraform.InstanceState, error) { 134 data, err := schemaMap(r.Schema).Data(s, d) 135 if err != nil { 136 return s, err 137 } 138 139 // Instance Diff shoould have the timeout info, need to copy it over to the 140 // ResourceData meta 141 rt := ResourceTimeout{} 142 if _, ok := d.Meta[TimeoutKey]; ok { 143 if err := rt.DiffDecode(d); err != nil { 144 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 145 } 146 } else if s != nil { 147 if _, ok := s.Meta[TimeoutKey]; ok { 148 if err := rt.StateDecode(s); err != nil { 149 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 150 } 151 } 152 } else { 153 log.Printf("[DEBUG] No meta timeoutkey found in Apply()") 154 } 155 data.timeouts = &rt 156 157 if s == nil { 158 // The Terraform API dictates that this should never happen, but 159 // it doesn't hurt to be safe in this case. 160 s = new(terraform.InstanceState) 161 } 162 163 if d.Destroy || d.RequiresNew() { 164 if s.ID != "" { 165 // Destroy the resource since it is created 166 if err := r.Delete(data, meta); err != nil { 167 return r.recordCurrentSchemaVersion(data.State()), err 168 } 169 170 // Make sure the ID is gone. 171 data.SetId("") 172 } 173 174 // If we're only destroying, and not creating, then return 175 // now since we're done! 176 if !d.RequiresNew() { 177 return nil, nil 178 } 179 180 // Reset the data to be stateless since we just destroyed 181 data, err = schemaMap(r.Schema).Data(nil, d) 182 // data was reset, need to re-apply the parsed timeouts 183 data.timeouts = &rt 184 if err != nil { 185 return nil, err 186 } 187 } 188 189 err = nil 190 if data.Id() == "" { 191 // We're creating, it is a new resource. 192 data.MarkNewResource() 193 err = r.Create(data, meta) 194 } else { 195 if r.Update == nil { 196 return s, fmt.Errorf("doesn't support update") 197 } 198 199 err = r.Update(data, meta) 200 } 201 202 return r.recordCurrentSchemaVersion(data.State()), err 203 } 204 205 // Diff returns a diff of this resource and is API compatible with the 206 // ResourceProvider interface. 207 func (r *Resource) Diff( 208 s *terraform.InstanceState, 209 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 210 211 t := &ResourceTimeout{} 212 err := t.ConfigDecode(r, c) 213 214 if err != nil { 215 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) 216 } 217 218 instanceDiff, err := schemaMap(r.Schema).Diff(s, c) 219 if err != nil { 220 return instanceDiff, err 221 } 222 223 if instanceDiff != nil { 224 if err := t.DiffEncode(instanceDiff); err != nil { 225 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) 226 } 227 } else { 228 log.Printf("[DEBUG] Instance Diff is nil in Diff()") 229 } 230 231 return instanceDiff, err 232 } 233 234 // Validate validates the resource configuration against the schema. 235 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { 236 warns, errs := schemaMap(r.Schema).Validate(c) 237 238 if r.deprecationMessage != "" { 239 warns = append(warns, r.deprecationMessage) 240 } 241 242 return warns, errs 243 } 244 245 // ReadDataApply loads the data for a data source, given a diff that 246 // describes the configuration arguments and desired computed attributes. 247 func (r *Resource) ReadDataApply( 248 d *terraform.InstanceDiff, 249 meta interface{}, 250 ) (*terraform.InstanceState, error) { 251 252 // Data sources are always built completely from scratch 253 // on each read, so the source state is always nil. 254 data, err := schemaMap(r.Schema).Data(nil, d) 255 if err != nil { 256 return nil, err 257 } 258 259 err = r.Read(data, meta) 260 state := data.State() 261 if state != nil && state.ID == "" { 262 // Data sources can set an ID if they want, but they aren't 263 // required to; we'll provide a placeholder if they don't, 264 // to preserve the invariant that all resources have non-empty 265 // ids. 266 state.ID = "-" 267 } 268 269 return r.recordCurrentSchemaVersion(state), err 270 } 271 272 // Refresh refreshes the state of the resource. 273 func (r *Resource) Refresh( 274 s *terraform.InstanceState, 275 meta interface{}) (*terraform.InstanceState, error) { 276 // If the ID is already somehow blank, it doesn't exist 277 if s.ID == "" { 278 return nil, nil 279 } 280 281 rt := ResourceTimeout{} 282 if _, ok := s.Meta[TimeoutKey]; ok { 283 if err := rt.StateDecode(s); err != nil { 284 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 285 } 286 } 287 288 if r.Exists != nil { 289 // Make a copy of data so that if it is modified it doesn't 290 // affect our Read later. 291 data, err := schemaMap(r.Schema).Data(s, nil) 292 data.timeouts = &rt 293 294 if err != nil { 295 return s, err 296 } 297 298 exists, err := r.Exists(data, meta) 299 if err != nil { 300 return s, err 301 } 302 if !exists { 303 return nil, nil 304 } 305 } 306 307 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) 308 if needsMigration && r.MigrateState != nil { 309 s, err := r.MigrateState(stateSchemaVersion, s, meta) 310 if err != nil { 311 return s, err 312 } 313 } 314 315 data, err := schemaMap(r.Schema).Data(s, nil) 316 data.timeouts = &rt 317 if err != nil { 318 return s, err 319 } 320 321 err = r.Read(data, meta) 322 state := data.State() 323 if state != nil && state.ID == "" { 324 state = nil 325 } 326 327 return r.recordCurrentSchemaVersion(state), err 328 } 329 330 // InternalValidate should be called to validate the structure 331 // of the resource. 332 // 333 // This should be called in a unit test for any resource to verify 334 // before release that a resource is properly configured for use with 335 // this library. 336 // 337 // Provider.InternalValidate() will automatically call this for all of 338 // the resources it manages, so you don't need to call this manually if it 339 // is part of a Provider. 340 func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { 341 if r == nil { 342 return errors.New("resource is nil") 343 } 344 345 if !writable { 346 if r.Create != nil || r.Update != nil || r.Delete != nil { 347 return fmt.Errorf("must not implement Create, Update or Delete") 348 } 349 } 350 351 tsm := topSchemaMap 352 353 if r.isTopLevel() && writable { 354 // All non-Computed attributes must be ForceNew if Update is not defined 355 if r.Update == nil { 356 nonForceNewAttrs := make([]string, 0) 357 for k, v := range r.Schema { 358 if !v.ForceNew && !v.Computed { 359 nonForceNewAttrs = append(nonForceNewAttrs, k) 360 } 361 } 362 if len(nonForceNewAttrs) > 0 { 363 return fmt.Errorf( 364 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) 365 } 366 } else { 367 nonUpdateableAttrs := make([]string, 0) 368 for k, v := range r.Schema { 369 if v.ForceNew || v.Computed && !v.Optional { 370 nonUpdateableAttrs = append(nonUpdateableAttrs, k) 371 } 372 } 373 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) 374 if updateableAttrs == 0 { 375 return fmt.Errorf( 376 "All fields are ForceNew or Computed w/out Optional, Update is superfluous") 377 } 378 } 379 380 tsm = schemaMap(r.Schema) 381 382 // Destroy, and Read are required 383 if r.Read == nil { 384 return fmt.Errorf("Read must be implemented") 385 } 386 if r.Delete == nil { 387 return fmt.Errorf("Delete must be implemented") 388 } 389 390 // If we have an importer, we need to verify the importer. 391 if r.Importer != nil { 392 if err := r.Importer.InternalValidate(); err != nil { 393 return err 394 } 395 } 396 } 397 398 // Resource-specific checks 399 for k, _ := range tsm { 400 if isReservedResourceFieldName(k) { 401 return fmt.Errorf("%s is a reserved field name for a resource", k) 402 } 403 } 404 405 return schemaMap(r.Schema).InternalValidate(tsm) 406 } 407 408 func isReservedResourceFieldName(name string) bool { 409 for _, reservedName := range config.ReservedResourceFields { 410 if name == reservedName { 411 return true 412 } 413 } 414 return false 415 } 416 417 // Data returns a ResourceData struct for this Resource. Each return value 418 // is a separate copy and can be safely modified differently. 419 // 420 // The data returned from this function has no actual affect on the Resource 421 // itself (including the state given to this function). 422 // 423 // This function is useful for unit tests and ResourceImporter functions. 424 func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { 425 result, err := schemaMap(r.Schema).Data(s, nil) 426 if err != nil { 427 // At the time of writing, this isn't possible (Data never returns 428 // non-nil errors). We panic to find this in the future if we have to. 429 // I don't see a reason for Data to ever return an error. 430 panic(err) 431 } 432 433 // Set the schema version to latest by default 434 result.meta = map[string]interface{}{ 435 "schema_version": strconv.Itoa(r.SchemaVersion), 436 } 437 438 return result 439 } 440 441 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing 442 // 443 // TODO: May be able to be removed with the above ResourceData function. 444 func (r *Resource) TestResourceData() *ResourceData { 445 return &ResourceData{ 446 schema: r.Schema, 447 } 448 } 449 450 // Returns true if the resource is "top level" i.e. not a sub-resource. 451 func (r *Resource) isTopLevel() bool { 452 // TODO: This is a heuristic; replace with a definitive attribute? 453 return r.Create != nil 454 } 455 456 // Determines if a given InstanceState needs to be migrated by checking the 457 // stored version number with the current SchemaVersion 458 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { 459 // Get the raw interface{} value for the schema version. If it doesn't 460 // exist or is nil then set it to zero. 461 raw := is.Meta["schema_version"] 462 if raw == nil { 463 raw = "0" 464 } 465 466 // Try to convert it to a string. If it isn't a string then we pretend 467 // that it isn't set at all. It should never not be a string unless it 468 // was manually tampered with. 469 rawString, ok := raw.(string) 470 if !ok { 471 rawString = "0" 472 } 473 474 stateSchemaVersion, _ := strconv.Atoi(rawString) 475 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion 476 } 477 478 func (r *Resource) recordCurrentSchemaVersion( 479 state *terraform.InstanceState) *terraform.InstanceState { 480 if state != nil && r.SchemaVersion > 0 { 481 if state.Meta == nil { 482 state.Meta = make(map[string]interface{}) 483 } 484 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) 485 } 486 return state 487 } 488 489 // Noop is a convenience implementation of resource function which takes 490 // no action and returns no error. 491 func Noop(*ResourceData, interface{}) error { 492 return nil 493 } 494 495 // RemoveFromState is a convenience implementation of a resource function 496 // which sets the resource ID to empty string (to remove it from state) 497 // and returns no error. 498 func RemoveFromState(d *ResourceData, _ interface{}) error { 499 d.SetId("") 500 return nil 501 }