github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/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 type Resource struct { 18 // Schema is the schema for the configuration of this resource. 19 // 20 // The keys of this map are the configuration keys, and the values 21 // describe the schema of the configuration value. 22 // 23 // The schema is used to represent both configurable data as well 24 // as data that might be computed in the process of creating this 25 // resource. 26 Schema map[string]*Schema 27 28 // SchemaVersion is the version number for this resource's Schema 29 // definition. The current SchemaVersion stored in the state for each 30 // resource. Provider authors can increment this version number 31 // when Schema semantics change. If the State's SchemaVersion is less than 32 // the current SchemaVersion, the InstanceState is yielded to the 33 // MigrateState callback, where the provider can make whatever changes it 34 // needs to update the state to be compatible to the latest version of the 35 // Schema. 36 // 37 // When unset, SchemaVersion defaults to 0, so provider authors can start 38 // their Versioning at any integer >= 1 39 SchemaVersion int 40 41 // MigrateState is responsible for updating an InstanceState with an old 42 // version to the format expected by the current version of the Schema. 43 // 44 // It is called during Refresh if the State's stored SchemaVersion is less 45 // than the current SchemaVersion of the Resource. 46 // 47 // The function is yielded the state's stored SchemaVersion and a pointer to 48 // the InstanceState that needs updating, as well as the configured 49 // provider's configured meta interface{}, in case the migration process 50 // needs to make any remote API calls. 51 MigrateState StateMigrateFunc 52 53 // The functions below are the CRUD operations for this resource. 54 // 55 // The only optional operation is Update. If Update is not implemented, 56 // then updates will not be supported for this resource. 57 // 58 // The ResourceData parameter in the functions below are used to 59 // query configuration and changes for the resource as well as to set 60 // the ID, computed data, etc. 61 // 62 // The interface{} parameter is the result of the ConfigureFunc in 63 // the provider for this resource. If the provider does not define 64 // a ConfigureFunc, this will be nil. This parameter should be used 65 // to store API clients, configuration structures, etc. 66 // 67 // If any errors occur during each of the operation, an error should be 68 // returned. If a resource was partially updated, be careful to enable 69 // partial state mode for ResourceData and use it accordingly. 70 // 71 // Exists is a function that is called to check if a resource still 72 // exists. If this returns false, then this will affect the diff 73 // accordingly. If this function isn't set, it will not be called. It 74 // is highly recommended to set it. The *ResourceData passed to Exists 75 // should _not_ be modified. 76 Create CreateFunc 77 Read ReadFunc 78 Update UpdateFunc 79 Delete DeleteFunc 80 Exists ExistsFunc 81 } 82 83 // See Resource documentation. 84 type CreateFunc func(*ResourceData, interface{}) error 85 86 // See Resource documentation. 87 type ReadFunc func(*ResourceData, interface{}) error 88 89 // See Resource documentation. 90 type UpdateFunc func(*ResourceData, interface{}) error 91 92 // See Resource documentation. 93 type DeleteFunc func(*ResourceData, interface{}) error 94 95 // See Resource documentation. 96 type ExistsFunc func(*ResourceData, interface{}) (bool, error) 97 98 // See Resource documentation. 99 type StateMigrateFunc func( 100 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) 101 102 // Apply creates, updates, and/or deletes a resource. 103 func (r *Resource) Apply( 104 s *terraform.InstanceState, 105 d *terraform.InstanceDiff, 106 meta interface{}) (*terraform.InstanceState, error) { 107 data, err := schemaMap(r.Schema).Data(s, d) 108 if err != nil { 109 return s, err 110 } 111 112 if s == nil { 113 // The Terraform API dictates that this should never happen, but 114 // it doesn't hurt to be safe in this case. 115 s = new(terraform.InstanceState) 116 } 117 118 if d.Destroy || d.RequiresNew() { 119 if s.ID != "" { 120 // Destroy the resource since it is created 121 if err := r.Delete(data, meta); err != nil { 122 return r.recordCurrentSchemaVersion(data.State()), err 123 } 124 125 // Make sure the ID is gone. 126 data.SetId("") 127 } 128 129 // If we're only destroying, and not creating, then return 130 // now since we're done! 131 if !d.RequiresNew() { 132 return nil, nil 133 } 134 135 // Reset the data to be stateless since we just destroyed 136 data, err = schemaMap(r.Schema).Data(nil, d) 137 if err != nil { 138 return nil, err 139 } 140 } 141 142 err = nil 143 if data.Id() == "" { 144 // We're creating, it is a new resource. 145 data.MarkNewResource() 146 err = r.Create(data, meta) 147 } else { 148 if r.Update == nil { 149 return s, fmt.Errorf("doesn't support update") 150 } 151 152 err = r.Update(data, meta) 153 } 154 155 return r.recordCurrentSchemaVersion(data.State()), err 156 } 157 158 // Diff returns a diff of this resource and is API compatible with the 159 // ResourceProvider interface. 160 func (r *Resource) Diff( 161 s *terraform.InstanceState, 162 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 163 return schemaMap(r.Schema).Diff(s, c) 164 } 165 166 // Validate validates the resource configuration against the schema. 167 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { 168 return schemaMap(r.Schema).Validate(c) 169 } 170 171 // Refresh refreshes the state of the resource. 172 func (r *Resource) Refresh( 173 s *terraform.InstanceState, 174 meta interface{}) (*terraform.InstanceState, error) { 175 // If the ID is already somehow blank, it doesn't exist 176 if s.ID == "" { 177 return nil, nil 178 } 179 180 if r.Exists != nil { 181 // Make a copy of data so that if it is modified it doesn't 182 // affect our Read later. 183 data, err := schemaMap(r.Schema).Data(s, nil) 184 if err != nil { 185 return s, err 186 } 187 188 exists, err := r.Exists(data, meta) 189 if err != nil { 190 return s, err 191 } 192 if !exists { 193 return nil, nil 194 } 195 } 196 197 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) 198 if needsMigration && r.MigrateState != nil { 199 s, err := r.MigrateState(stateSchemaVersion, s, meta) 200 if err != nil { 201 return s, err 202 } 203 } 204 205 data, err := schemaMap(r.Schema).Data(s, nil) 206 if err != nil { 207 return s, err 208 } 209 210 err = r.Read(data, meta) 211 state := data.State() 212 if state != nil && state.ID == "" { 213 state = nil 214 } 215 216 return r.recordCurrentSchemaVersion(state), err 217 } 218 219 // InternalValidate should be called to validate the structure 220 // of the resource. 221 // 222 // This should be called in a unit test for any resource to verify 223 // before release that a resource is properly configured for use with 224 // this library. 225 // 226 // Provider.InternalValidate() will automatically call this for all of 227 // the resources it manages, so you don't need to call this manually if it 228 // is part of a Provider. 229 func (r *Resource) InternalValidate(topSchemaMap schemaMap) error { 230 if r == nil { 231 return errors.New("resource is nil") 232 } 233 tsm := topSchemaMap 234 235 if r.isTopLevel() { 236 // All non-Computed attributes must be ForceNew if Update is not defined 237 if r.Update == nil { 238 nonForceNewAttrs := make([]string, 0) 239 for k, v := range r.Schema { 240 if !v.ForceNew && !v.Computed { 241 nonForceNewAttrs = append(nonForceNewAttrs, k) 242 } 243 } 244 if len(nonForceNewAttrs) > 0 { 245 return fmt.Errorf( 246 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) 247 } 248 } else { 249 nonUpdateableAttrs := make([]string, 0) 250 for k, v := range r.Schema { 251 if v.ForceNew || v.Computed && !v.Optional { 252 nonUpdateableAttrs = append(nonUpdateableAttrs, k) 253 } 254 } 255 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) 256 if updateableAttrs == 0 { 257 return fmt.Errorf( 258 "All fields are ForceNew or Computed w/out Optional, Update is superfluous") 259 } 260 } 261 262 tsm = schemaMap(r.Schema) 263 } 264 265 return schemaMap(r.Schema).InternalValidate(tsm) 266 } 267 268 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing 269 func (r *Resource) TestResourceData() *ResourceData { 270 return &ResourceData{ 271 schema: r.Schema, 272 } 273 } 274 275 // Returns true if the resource is "top level" i.e. not a sub-resource. 276 func (r *Resource) isTopLevel() bool { 277 // TODO: This is a heuristic; replace with a definitive attribute? 278 return r.Create != nil 279 } 280 281 // Determines if a given InstanceState needs to be migrated by checking the 282 // stored version number with the current SchemaVersion 283 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { 284 stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"]) 285 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion 286 } 287 288 func (r *Resource) recordCurrentSchemaVersion( 289 state *terraform.InstanceState) *terraform.InstanceState { 290 if state != nil && r.SchemaVersion > 0 { 291 if state.Meta == nil { 292 state.Meta = make(map[string]string) 293 } 294 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) 295 } 296 return state 297 }