github.com/bigkraig/terraform@v0.6.4-0.20151219155159-c90d1b074e31/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 err = r.Create(data, meta) 146 } else { 147 if r.Update == nil { 148 return s, fmt.Errorf("doesn't support update") 149 } 150 151 err = r.Update(data, meta) 152 } 153 154 return r.recordCurrentSchemaVersion(data.State()), err 155 } 156 157 // Diff returns a diff of this resource and is API compatible with the 158 // ResourceProvider interface. 159 func (r *Resource) Diff( 160 s *terraform.InstanceState, 161 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 162 return schemaMap(r.Schema).Diff(s, c) 163 } 164 165 // Validate validates the resource configuration against the schema. 166 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { 167 return schemaMap(r.Schema).Validate(c) 168 } 169 170 // Refresh refreshes the state of the resource. 171 func (r *Resource) Refresh( 172 s *terraform.InstanceState, 173 meta interface{}) (*terraform.InstanceState, error) { 174 // If the ID is already somehow blank, it doesn't exist 175 if s.ID == "" { 176 return nil, nil 177 } 178 179 if r.Exists != nil { 180 // Make a copy of data so that if it is modified it doesn't 181 // affect our Read later. 182 data, err := schemaMap(r.Schema).Data(s, nil) 183 if err != nil { 184 return s, err 185 } 186 187 exists, err := r.Exists(data, meta) 188 if err != nil { 189 return s, err 190 } 191 if !exists { 192 return nil, nil 193 } 194 } 195 196 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) 197 if needsMigration && r.MigrateState != nil { 198 s, err := r.MigrateState(stateSchemaVersion, s, meta) 199 if err != nil { 200 return s, err 201 } 202 } 203 204 data, err := schemaMap(r.Schema).Data(s, nil) 205 if err != nil { 206 return s, err 207 } 208 209 err = r.Read(data, meta) 210 state := data.State() 211 if state != nil && state.ID == "" { 212 state = nil 213 } 214 215 return r.recordCurrentSchemaVersion(state), err 216 } 217 218 // InternalValidate should be called to validate the structure 219 // of the resource. 220 // 221 // This should be called in a unit test for any resource to verify 222 // before release that a resource is properly configured for use with 223 // this library. 224 // 225 // Provider.InternalValidate() will automatically call this for all of 226 // the resources it manages, so you don't need to call this manually if it 227 // is part of a Provider. 228 func (r *Resource) InternalValidate(topSchemaMap schemaMap) error { 229 if r == nil { 230 return errors.New("resource is nil") 231 } 232 tsm := topSchemaMap 233 234 if r.isTopLevel() { 235 // All non-Computed attributes must be ForceNew if Update is not defined 236 if r.Update == nil { 237 nonForceNewAttrs := make([]string, 0) 238 for k, v := range r.Schema { 239 if !v.ForceNew && !v.Computed { 240 nonForceNewAttrs = append(nonForceNewAttrs, k) 241 } 242 } 243 if len(nonForceNewAttrs) > 0 { 244 return fmt.Errorf( 245 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) 246 } 247 } else { 248 nonUpdateableAttrs := make([]string, 0) 249 for k, v := range r.Schema { 250 if v.ForceNew || v.Computed && !v.Optional { 251 nonUpdateableAttrs = append(nonUpdateableAttrs, k) 252 } 253 } 254 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) 255 if updateableAttrs == 0 { 256 return fmt.Errorf( 257 "All fields are ForceNew or Computed w/out Optional, Update is superfluous") 258 } 259 } 260 261 tsm = schemaMap(r.Schema) 262 } 263 264 return schemaMap(r.Schema).InternalValidate(tsm) 265 } 266 267 // Returns true if the resource is "top level" i.e. not a sub-resource. 268 func (r *Resource) isTopLevel() bool { 269 // TODO: This is a heuristic; replace with a definitive attribute? 270 return r.Create != nil 271 } 272 273 // Determines if a given InstanceState needs to be migrated by checking the 274 // stored version number with the current SchemaVersion 275 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { 276 stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"]) 277 return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion 278 } 279 280 func (r *Resource) recordCurrentSchemaVersion( 281 state *terraform.InstanceState) *terraform.InstanceState { 282 if state != nil && r.SchemaVersion > 0 { 283 if state.Meta == nil { 284 state.Meta = make(map[string]string) 285 } 286 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) 287 } 288 return state 289 }