github.com/sarguru/terraform@v0.6.17-0.20160525232901-8fcdfd7e3dc9/builtin/providers/nsone/resource_record.go (about) 1 package nsone 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "github.com/bobtfish/go-nsone-api" 8 "github.com/hashicorp/terraform/helper/hashcode" 9 "github.com/hashicorp/terraform/helper/schema" 10 "log" 11 "regexp" 12 "sort" 13 "strconv" 14 "strings" 15 ) 16 17 func recordResource() *schema.Resource { 18 return &schema.Resource{ 19 Schema: map[string]*schema.Schema{ 20 "id": &schema.Schema{ 21 Type: schema.TypeString, 22 Computed: true, 23 }, 24 "zone": &schema.Schema{ 25 Type: schema.TypeString, 26 Required: true, 27 ForceNew: true, 28 }, 29 "domain": &schema.Schema{ 30 Type: schema.TypeString, 31 Required: true, 32 ForceNew: true, 33 }, 34 "ttl": &schema.Schema{ 35 Type: schema.TypeInt, 36 Optional: true, 37 Computed: true, 38 }, 39 "type": &schema.Schema{ 40 Type: schema.TypeString, 41 Required: true, 42 ForceNew: true, 43 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 44 value := v.(string) 45 if !regexp.MustCompile(`^(A|AAAA|ALIAS|AFSDB|CNAME|DNAME|HINFO|MX|NAPTR|NS|PTR|RP|SPF|SRV|TXT)$`).MatchString(value) { 46 es = append(es, fmt.Errorf( 47 "only A, AAAA, ALIAS, AFSDB, CNAME, DNAME, HINFO, MX, NAPTR, NS, PTR, RP, SPF, SRV, TXT allowed in %q", k)) 48 } 49 return 50 }, 51 }, 52 "meta": metaSchema(), 53 "link": &schema.Schema{ 54 Type: schema.TypeString, 55 Optional: true, 56 ForceNew: true, 57 }, 58 "use_client_subnet": &schema.Schema{ 59 Type: schema.TypeBool, 60 Optional: true, 61 Default: false, 62 }, 63 "answers": &schema.Schema{ 64 Type: schema.TypeSet, 65 Optional: true, 66 Elem: &schema.Resource{ 67 Schema: map[string]*schema.Schema{ 68 "answer": &schema.Schema{ 69 Type: schema.TypeString, 70 Optional: true, 71 }, 72 "region": &schema.Schema{ 73 Type: schema.TypeString, 74 Optional: true, 75 }, 76 "meta": &schema.Schema{ 77 Type: schema.TypeSet, 78 Optional: true, 79 Elem: &schema.Resource{ 80 Schema: map[string]*schema.Schema{ 81 "field": &schema.Schema{ 82 Type: schema.TypeString, 83 Optional: true, 84 }, 85 "feed": &schema.Schema{ 86 Type: schema.TypeString, 87 Optional: true, 88 //ConflictsWith: []string{"value"}, 89 }, 90 "value": &schema.Schema{ 91 Type: schema.TypeString, 92 Optional: true, 93 //ConflictsWith: []string{"feed"}, 94 }, 95 }, 96 }, 97 Set: metaToHash, 98 }, 99 }, 100 }, 101 Set: answersToHash, 102 }, 103 "regions": &schema.Schema{ 104 Type: schema.TypeSet, 105 Optional: true, 106 Elem: &schema.Resource{ 107 Schema: map[string]*schema.Schema{ 108 "name": &schema.Schema{ 109 Type: schema.TypeString, 110 Required: true, 111 }, 112 "georegion": &schema.Schema{ 113 Type: schema.TypeString, 114 Optional: true, 115 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 116 value := v.(string) 117 if !regexp.MustCompile(`^(US-WEST|US-EAST|US-CENTRAL|EUROPE|AFRICA|ASIAPAC|SOUTH-AMERICA)$`).MatchString(value) { 118 es = append(es, fmt.Errorf( 119 "only US-WEST, US-EAST, US-CENTRAL, EUROPE, AFRICA, ASIAPAC, SOUTH-AMERICA allowed in %q", k)) 120 } 121 return 122 }, 123 }, 124 "country": &schema.Schema{ 125 Type: schema.TypeString, 126 Optional: true, 127 }, 128 "us_state": &schema.Schema{ 129 Type: schema.TypeString, 130 Optional: true, 131 }, 132 "up": &schema.Schema{ 133 Type: schema.TypeBool, 134 Optional: true, 135 }, 136 }, 137 }, 138 Set: regionsToHash, 139 }, 140 "filters": &schema.Schema{ 141 Type: schema.TypeList, 142 Optional: true, 143 Elem: &schema.Resource{ 144 Schema: map[string]*schema.Schema{ 145 "filter": &schema.Schema{ 146 Type: schema.TypeString, 147 Required: true, 148 }, 149 "disabled": &schema.Schema{ 150 Type: schema.TypeBool, 151 Optional: true, 152 }, 153 "config": &schema.Schema{ 154 Type: schema.TypeMap, 155 Optional: true, 156 }, 157 }, 158 }, 159 }, 160 }, 161 Create: RecordCreate, 162 Read: RecordRead, 163 Update: RecordUpdate, 164 Delete: RecordDelete, 165 } 166 } 167 168 func regionsToHash(v interface{}) int { 169 var buf bytes.Buffer 170 r := v.(map[string]interface{}) 171 buf.WriteString(fmt.Sprintf("%s-", r["name"].(string))) 172 buf.WriteString(fmt.Sprintf("%s-", r["georegion"].(string))) 173 buf.WriteString(fmt.Sprintf("%s-", r["country"].(string))) 174 buf.WriteString(fmt.Sprintf("%s-", r["us_state"].(string))) 175 buf.WriteString(fmt.Sprintf("%t-", r["up"].(bool))) 176 return hashcode.String(buf.String()) 177 } 178 179 func answersToHash(v interface{}) int { 180 var buf bytes.Buffer 181 a := v.(map[string]interface{}) 182 buf.WriteString(fmt.Sprintf("%s-", a["answer"].(string))) 183 if a["region"] != nil { 184 buf.WriteString(fmt.Sprintf("%s-", a["region"].(string))) 185 } 186 var metas []int 187 switch t := a["meta"].(type) { 188 default: 189 panic(fmt.Sprintf("unexpected type %T", t)) 190 case *schema.Set: 191 for _, meta := range t.List() { 192 metas = append(metas, metaToHash(meta)) 193 } 194 case []map[string]interface{}: 195 for _, meta := range t { 196 metas = append(metas, metaToHash(meta)) 197 } 198 } 199 sort.Ints(metas) 200 for _, metahash := range metas { 201 buf.WriteString(fmt.Sprintf("%d-", metahash)) 202 } 203 hash := hashcode.String(buf.String()) 204 log.Printf("Generated answersToHash %d from %+v\n", hash, a) 205 return hash 206 } 207 208 func metaToHash(v interface{}) int { 209 var buf bytes.Buffer 210 s := v.(map[string]interface{}) 211 buf.WriteString(fmt.Sprintf("%s-", s["field"].(string))) 212 if v, ok := s["feed"]; ok && v.(string) != "" { 213 buf.WriteString(fmt.Sprintf("feed%s-", v.(string))) 214 } 215 if v, ok := s["value"]; ok && v.(string) != "" { 216 buf.WriteString(fmt.Sprintf("value%s-", v.(string))) 217 } 218 219 hash := hashcode.String(buf.String()) 220 log.Printf("Generated metaToHash %d from %+v\n", hash, s) 221 return hash 222 } 223 224 func recordToResourceData(d *schema.ResourceData, r *nsone.Record) error { 225 d.SetId(r.Id) 226 d.Set("domain", r.Domain) 227 d.Set("zone", r.Zone) 228 d.Set("type", r.Type) 229 d.Set("ttl", r.Ttl) 230 if r.Link != "" { 231 d.Set("link", r.Link) 232 } 233 if len(r.Filters) > 0 { 234 filters := make([]map[string]interface{}, len(r.Filters)) 235 for i, f := range r.Filters { 236 m := make(map[string]interface{}) 237 m["filter"] = f.Filter 238 if f.Disabled { 239 m["disabled"] = true 240 } 241 if f.Config != nil { 242 m["config"] = f.Config 243 } 244 filters[i] = m 245 } 246 d.Set("filters", filters) 247 } 248 if len(r.Answers) > 0 { 249 ans := &schema.Set{ 250 F: answersToHash, 251 } 252 log.Printf("Got back from nsone answers: %+v", r.Answers) 253 for _, answer := range r.Answers { 254 ans.Add(answerToMap(answer)) 255 } 256 log.Printf("Setting answers %+v", ans) 257 err := d.Set("answers", ans) 258 if err != nil { 259 return fmt.Errorf("[DEBUG] Error setting answers for: %s, error: %#v", r.Domain, err) 260 } 261 } 262 if len(r.Regions) > 0 { 263 regions := make([]map[string]interface{}, 0, len(r.Regions)) 264 for regionName, region := range r.Regions { 265 newRegion := make(map[string]interface{}) 266 newRegion["name"] = regionName 267 if len(region.Meta.GeoRegion) > 0 { 268 newRegion["georegion"] = region.Meta.GeoRegion[0] 269 } 270 if len(region.Meta.Country) > 0 { 271 newRegion["country"] = region.Meta.Country[0] 272 } 273 if len(region.Meta.USState) > 0 { 274 newRegion["us_state"] = region.Meta.USState[0] 275 } 276 if region.Meta.Up { 277 newRegion["up"] = region.Meta.Up 278 } else { 279 newRegion["up"] = false 280 } 281 regions = append(regions, newRegion) 282 } 283 log.Printf("Setting regions %+v", regions) 284 err := d.Set("regions", regions) 285 if err != nil { 286 return fmt.Errorf("[DEBUG] Error setting regions for: %s, error: %#v", r.Domain, err) 287 } 288 } 289 return nil 290 } 291 292 func answerToMap(a nsone.Answer) map[string]interface{} { 293 m := make(map[string]interface{}) 294 m["meta"] = make([]map[string]interface{}, 0) 295 m["answer"] = strings.Join(a.Answer, " ") 296 if a.Region != "" { 297 m["region"] = a.Region 298 } 299 if a.Meta != nil { 300 metas := &schema.Set{ 301 F: metaToHash, 302 } 303 for k, v := range a.Meta { 304 meta := make(map[string]interface{}) 305 meta["field"] = k 306 switch t := v.(type) { 307 case map[string]interface{}: 308 meta["feed"] = t["feed"].(string) 309 case string: 310 meta["value"] = t 311 case []interface{}: 312 var valArray []string 313 for _, pref := range t { 314 valArray = append(valArray, pref.(string)) 315 } 316 sort.Strings(valArray) 317 stringVal := strings.Join(valArray, ",") 318 meta["value"] = stringVal 319 case bool: 320 intVal := btoi(t) 321 meta["value"] = strconv.Itoa(intVal) 322 case float64: 323 intVal := int(t) 324 meta["value"] = strconv.Itoa(intVal) 325 } 326 metas.Add(meta) 327 } 328 m["meta"] = metas 329 } 330 return m 331 } 332 333 func btoi(b bool) int { 334 if b { 335 return 1 336 } 337 return 0 338 } 339 340 func resourceDataToRecord(r *nsone.Record, d *schema.ResourceData) error { 341 r.Id = d.Id() 342 if answers := d.Get("answers").(*schema.Set); answers.Len() > 0 { 343 al := make([]nsone.Answer, answers.Len()) 344 for i, answerRaw := range answers.List() { 345 answer := answerRaw.(map[string]interface{}) 346 a := nsone.NewAnswer() 347 v := answer["answer"].(string) 348 if d.Get("type") != "TXT" { 349 a.Answer = strings.Split(v, " ") 350 } else { 351 a.Answer = []string{v} 352 } 353 if v, ok := answer["region"]; ok { 354 a.Region = v.(string) 355 } 356 if metas := answer["meta"].(*schema.Set); metas.Len() > 0 { 357 for _, metaRaw := range metas.List() { 358 meta := metaRaw.(map[string]interface{}) 359 key := meta["field"].(string) 360 if value, ok := meta["feed"]; ok && value.(string) != "" { 361 a.Meta[key] = nsone.NewMetaFeed(value.(string)) 362 } 363 if value, ok := meta["value"]; ok && value.(string) != "" { 364 metaArray := strings.Split(value.(string), ",") 365 if len(metaArray) > 1 { 366 sort.Strings(metaArray) 367 a.Meta[key] = metaArray 368 } else { 369 a.Meta[key] = value.(string) 370 } 371 } 372 } 373 } 374 al[i] = a 375 } 376 r.Answers = al 377 if _, ok := d.GetOk("link"); ok { 378 return errors.New("Cannot have both link and answers in a record") 379 } 380 } 381 if v, ok := d.GetOk("ttl"); ok { 382 r.Ttl = v.(int) 383 } 384 if v, ok := d.GetOk("link"); ok { 385 r.LinkTo(v.(string)) 386 } 387 useClientSubnetVal := d.Get("use_client_subnet").(bool) 388 if v := strconv.FormatBool(useClientSubnetVal); v != "" { 389 r.UseClientSubnet = useClientSubnetVal 390 } 391 392 if rawFilters := d.Get("filters").([]interface{}); len(rawFilters) > 0 { 393 f := make([]nsone.Filter, len(rawFilters)) 394 for i, filterRaw := range rawFilters { 395 fi := filterRaw.(map[string]interface{}) 396 config := make(map[string]interface{}) 397 filter := nsone.Filter{ 398 Filter: fi["filter"].(string), 399 Config: config, 400 } 401 if disabled, ok := fi["disabled"]; ok { 402 filter.Disabled = disabled.(bool) 403 } 404 if rawConfig, ok := fi["config"]; ok { 405 for k, v := range rawConfig.(map[string]interface{}) { 406 if i, err := strconv.Atoi(v.(string)); err == nil { 407 filter.Config[k] = i 408 } else { 409 filter.Config[k] = v 410 } 411 } 412 } 413 f[i] = filter 414 } 415 r.Filters = f 416 } 417 if regions := d.Get("regions").(*schema.Set); regions.Len() > 0 { 418 rm := make(map[string]nsone.Region) 419 for _, regionRaw := range regions.List() { 420 region := regionRaw.(map[string]interface{}) 421 nsoneR := nsone.Region{ 422 Meta: nsone.RegionMeta{}, 423 } 424 if g := region["georegion"].(string); g != "" { 425 nsoneR.Meta.GeoRegion = []string{g} 426 } 427 if g := region["country"].(string); g != "" { 428 nsoneR.Meta.Country = []string{g} 429 } 430 if g := region["us_state"].(string); g != "" { 431 nsoneR.Meta.USState = []string{g} 432 } 433 if g := region["up"].(bool); g { 434 nsoneR.Meta.Up = g 435 } 436 437 rm[region["name"].(string)] = nsoneR 438 } 439 r.Regions = rm 440 } 441 return nil 442 } 443 444 func setToMapByKey(s *schema.Set, key string) map[string]interface{} { 445 result := make(map[string]interface{}) 446 for _, rawData := range s.List() { 447 data := rawData.(map[string]interface{}) 448 result[data[key].(string)] = data 449 } 450 451 return result 452 } 453 454 // RecordCreate creates DNS record in ns1 455 func RecordCreate(d *schema.ResourceData, meta interface{}) error { 456 client := meta.(*nsone.APIClient) 457 r := nsone.NewRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string)) 458 if err := resourceDataToRecord(r, d); err != nil { 459 return err 460 } 461 if err := client.CreateRecord(r); err != nil { 462 return err 463 } 464 return recordToResourceData(d, r) 465 } 466 467 // RecordRead reads the DNS record from ns1 468 func RecordRead(d *schema.ResourceData, meta interface{}) error { 469 client := meta.(*nsone.APIClient) 470 r, err := client.GetRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string)) 471 if err != nil { 472 return err 473 } 474 recordToResourceData(d, r) 475 return nil 476 } 477 478 // RecordDelete deltes the DNS record from ns1 479 func RecordDelete(d *schema.ResourceData, meta interface{}) error { 480 client := meta.(*nsone.APIClient) 481 err := client.DeleteRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string)) 482 d.SetId("") 483 return err 484 } 485 486 // RecordUpdate updates the given dns record in ns1 487 func RecordUpdate(d *schema.ResourceData, meta interface{}) error { 488 client := meta.(*nsone.APIClient) 489 r := nsone.NewRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string)) 490 if err := resourceDataToRecord(r, d); err != nil { 491 return err 492 } 493 if err := client.UpdateRecord(r); err != nil { 494 return err 495 } 496 recordToResourceData(d, r) 497 return nil 498 }