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