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