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