github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/ultradns/resource_ultradns_dirpool.go (about) 1 package ultradns 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "log" 8 "strings" 9 10 "github.com/Ensighten/udnssdk" 11 "github.com/fatih/structs" 12 "github.com/hashicorp/terraform/helper/hashcode" 13 "github.com/hashicorp/terraform/helper/schema" 14 "github.com/mitchellh/mapstructure" 15 ) 16 17 func resourceUltradnsDirpool() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceUltradnsDirpoolCreate, 20 Read: resourceUltradnsDirpoolRead, 21 Update: resourceUltradnsDirpoolUpdate, 22 Delete: resourceUltradnsDirpoolDelete, 23 24 Schema: map[string]*schema.Schema{ 25 // Required 26 "zone": &schema.Schema{ 27 Type: schema.TypeString, 28 Required: true, 29 ForceNew: true, 30 }, 31 "name": &schema.Schema{ 32 Type: schema.TypeString, 33 Required: true, 34 ForceNew: true, 35 }, 36 "type": &schema.Schema{ 37 Type: schema.TypeString, 38 Required: true, 39 ForceNew: true, 40 }, 41 "description": &schema.Schema{ 42 Type: schema.TypeString, 43 Required: true, 44 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 45 value := v.(string) 46 if len(value) > 255 { 47 errors = append(errors, fmt.Errorf( 48 "'description' too long, must be less than 255 characters")) 49 } 50 return 51 }, 52 }, 53 "rdata": &schema.Schema{ 54 // UltraDNS API does not respect rdata ordering 55 Type: schema.TypeSet, 56 Set: hashRdatas, 57 Required: true, 58 // Valid: len(rdataInfo) == len(rdata) 59 Elem: &schema.Resource{ 60 Schema: map[string]*schema.Schema{ 61 // Required 62 "host": &schema.Schema{ 63 Type: schema.TypeString, 64 Required: true, 65 }, 66 "all_non_configured": &schema.Schema{ 67 Type: schema.TypeBool, 68 Optional: true, 69 Default: false, 70 }, 71 "geo_info": &schema.Schema{ 72 Type: schema.TypeList, 73 Optional: true, 74 Elem: &schema.Resource{ 75 Schema: map[string]*schema.Schema{ 76 "name": &schema.Schema{ 77 Type: schema.TypeString, 78 Optional: true, 79 }, 80 "is_account_level": &schema.Schema{ 81 Type: schema.TypeBool, 82 Optional: true, 83 Default: false, 84 }, 85 "codes": &schema.Schema{ 86 Type: schema.TypeSet, 87 Optional: true, 88 Elem: &schema.Schema{Type: schema.TypeString}, 89 Set: schema.HashString, 90 }, 91 }, 92 }, 93 }, 94 "ip_info": &schema.Schema{ 95 Type: schema.TypeList, 96 Optional: true, 97 Elem: &schema.Resource{ 98 Schema: map[string]*schema.Schema{ 99 "name": &schema.Schema{ 100 Type: schema.TypeString, 101 Optional: true, 102 }, 103 "is_account_level": &schema.Schema{ 104 Type: schema.TypeBool, 105 Optional: true, 106 Default: false, 107 }, 108 "ips": &schema.Schema{ 109 Type: schema.TypeSet, 110 Optional: true, 111 Set: hashIPInfoIPs, 112 Elem: &schema.Resource{ 113 Schema: map[string]*schema.Schema{ 114 "start": &schema.Schema{ 115 Type: schema.TypeString, 116 Optional: true, 117 // ConflictsWith: []string{"cidr", "address"}, 118 }, 119 "end": &schema.Schema{ 120 Type: schema.TypeString, 121 Optional: true, 122 // ConflictsWith: []string{"cidr", "address"}, 123 }, 124 "cidr": &schema.Schema{ 125 Type: schema.TypeString, 126 Optional: true, 127 // ConflictsWith: []string{"start", "end", "address"}, 128 }, 129 "address": &schema.Schema{ 130 Type: schema.TypeString, 131 Optional: true, 132 // ConflictsWith: []string{"start", "end", "cidr"}, 133 }, 134 }, 135 }, 136 }, 137 }, 138 }, 139 }, 140 }, 141 }, 142 }, 143 // Optional 144 "ttl": &schema.Schema{ 145 Type: schema.TypeInt, 146 Optional: true, 147 Default: 3600, 148 }, 149 "conflict_resolve": &schema.Schema{ 150 Type: schema.TypeString, 151 Optional: true, 152 Default: "GEO", 153 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 154 value := v.(string) 155 if value != "GEO" && value != "IP" { 156 errors = append(errors, fmt.Errorf( 157 "only 'GEO', and 'IP' are supported values for 'conflict_resolve'")) 158 } 159 return 160 }, 161 }, 162 "no_response": &schema.Schema{ 163 Type: schema.TypeList, 164 Optional: true, 165 Elem: &schema.Resource{ 166 Schema: map[string]*schema.Schema{ 167 "all_non_configured": &schema.Schema{ 168 Type: schema.TypeBool, 169 Optional: true, 170 Default: false, 171 }, 172 "geo_info": &schema.Schema{ 173 Type: schema.TypeList, 174 Optional: true, 175 Elem: &schema.Resource{ 176 Schema: map[string]*schema.Schema{ 177 "name": &schema.Schema{ 178 Type: schema.TypeString, 179 Optional: true, 180 }, 181 "is_account_level": &schema.Schema{ 182 Type: schema.TypeBool, 183 Optional: true, 184 Default: false, 185 }, 186 "codes": &schema.Schema{ 187 Type: schema.TypeSet, 188 Optional: true, 189 Elem: &schema.Schema{Type: schema.TypeString}, 190 Set: schema.HashString, 191 }, 192 }, 193 }, 194 }, 195 "ip_info": &schema.Schema{ 196 Type: schema.TypeList, 197 Optional: true, 198 Elem: &schema.Resource{ 199 Schema: map[string]*schema.Schema{ 200 "name": &schema.Schema{ 201 Type: schema.TypeString, 202 Optional: true, 203 }, 204 "is_account_level": &schema.Schema{ 205 Type: schema.TypeBool, 206 Optional: true, 207 Default: false, 208 }, 209 "ips": &schema.Schema{ 210 Type: schema.TypeSet, 211 Optional: true, 212 Set: hashIPInfoIPs, 213 Elem: &schema.Resource{ 214 Schema: map[string]*schema.Schema{ 215 "start": &schema.Schema{ 216 Type: schema.TypeString, 217 Optional: true, 218 // ConflictsWith: []string{"cidr", "address"}, 219 }, 220 "end": &schema.Schema{ 221 Type: schema.TypeString, 222 Optional: true, 223 // ConflictsWith: []string{"cidr", "address"}, 224 }, 225 "cidr": &schema.Schema{ 226 Type: schema.TypeString, 227 Optional: true, 228 // ConflictsWith: []string{"start", "end", "address"}, 229 }, 230 "address": &schema.Schema{ 231 Type: schema.TypeString, 232 Optional: true, 233 // ConflictsWith: []string{"start", "end", "cidr"}, 234 }, 235 }, 236 }, 237 }, 238 }, 239 }, 240 }, 241 }, 242 }, 243 }, 244 // Computed 245 "hostname": &schema.Schema{ 246 Type: schema.TypeString, 247 Computed: true, 248 }, 249 }, 250 } 251 } 252 253 // CRUD Operations 254 255 func resourceUltradnsDirpoolCreate(d *schema.ResourceData, meta interface{}) error { 256 client := meta.(*udnssdk.Client) 257 258 r, err := makeDirpoolRRSetResource(d) 259 if err != nil { 260 return err 261 } 262 263 log.Printf("[INFO] ultradns_dirpool create: %#v", r) 264 _, err = client.RRSets.Create(r.RRSetKey(), r.RRSet()) 265 if err != nil { 266 // FIXME: remove the json from log 267 marshalled, _ := json.Marshal(r) 268 ms := string(marshalled) 269 return fmt.Errorf("create failed: %#v [[[[ %v ]]]] -> %v", r, ms, err) 270 } 271 272 d.SetId(r.ID()) 273 log.Printf("[INFO] ultradns_dirpool.id: %v", d.Id()) 274 275 return resourceUltradnsDirpoolRead(d, meta) 276 } 277 278 func resourceUltradnsDirpoolRead(d *schema.ResourceData, meta interface{}) error { 279 client := meta.(*udnssdk.Client) 280 281 rr, err := makeDirpoolRRSetResource(d) 282 if err != nil { 283 return err 284 } 285 286 rrsets, err := client.RRSets.Select(rr.RRSetKey()) 287 if err != nil { 288 uderr, ok := err.(*udnssdk.ErrorResponseList) 289 if ok { 290 for _, resps := range uderr.Responses { 291 // 70002 means Records Not Found 292 if resps.ErrorCode == 70002 { 293 d.SetId("") 294 return nil 295 } 296 return fmt.Errorf("resource not found: %v", err) 297 } 298 } 299 return fmt.Errorf("resource not found: %v", err) 300 } 301 302 r := rrsets[0] 303 304 return populateResourceFromDirpool(d, &r) 305 } 306 307 func resourceUltradnsDirpoolUpdate(d *schema.ResourceData, meta interface{}) error { 308 client := meta.(*udnssdk.Client) 309 310 r, err := makeDirpoolRRSetResource(d) 311 if err != nil { 312 return err 313 } 314 315 log.Printf("[INFO] ultradns_dirpool update: %+v", r) 316 _, err = client.RRSets.Update(r.RRSetKey(), r.RRSet()) 317 if err != nil { 318 return fmt.Errorf("resource update failed: %v", err) 319 } 320 321 return resourceUltradnsDirpoolRead(d, meta) 322 } 323 324 func resourceUltradnsDirpoolDelete(d *schema.ResourceData, meta interface{}) error { 325 client := meta.(*udnssdk.Client) 326 327 r, err := makeDirpoolRRSetResource(d) 328 if err != nil { 329 return err 330 } 331 332 log.Printf("[INFO] ultradns_dirpool delete: %+v", r) 333 _, err = client.RRSets.Delete(r.RRSetKey()) 334 if err != nil { 335 return fmt.Errorf("resource delete failed: %v", err) 336 } 337 338 return nil 339 } 340 341 // Resource Helpers 342 343 // makeDirpoolRRSetResource converts ResourceData into an rRSetResource 344 // ready for use in any CRUD operation 345 func makeDirpoolRRSetResource(d *schema.ResourceData) (rRSetResource, error) { 346 rDataRaw := d.Get("rdata").(*schema.Set).List() 347 res := rRSetResource{ 348 RRType: d.Get("type").(string), 349 Zone: d.Get("zone").(string), 350 OwnerName: d.Get("name").(string), 351 TTL: d.Get("ttl").(int), 352 RData: unzipRdataHosts(rDataRaw), 353 } 354 355 profile := udnssdk.DirPoolProfile{ 356 Context: udnssdk.DirPoolSchema, 357 Description: d.Get("description").(string), 358 ConflictResolve: d.Get("conflict_resolve").(string), 359 } 360 361 ri, err := makeDirpoolRdataInfos(rDataRaw) 362 if err != nil { 363 return res, err 364 } 365 profile.RDataInfo = ri 366 367 noResponseRaw := d.Get("no_response").([]interface{}) 368 if len(noResponseRaw) >= 1 { 369 if len(noResponseRaw) > 1 { 370 return res, fmt.Errorf("no_response: only 0 or 1 blocks alowed, got: %#v", len(noResponseRaw)) 371 } 372 nr, err := makeDirpoolRdataInfo(noResponseRaw[0]) 373 if err != nil { 374 return res, err 375 } 376 profile.NoResponse = nr 377 } 378 379 res.Profile = profile.RawProfile() 380 381 return res, nil 382 } 383 384 // populateResourceFromDirpool takes an RRSet and populates the ResourceData 385 func populateResourceFromDirpool(d *schema.ResourceData, r *udnssdk.RRSet) error { 386 // TODO: fix from tcpool to dirpool 387 zone := d.Get("zone") 388 // ttl 389 d.Set("ttl", r.TTL) 390 // hostname 391 if r.OwnerName == "" { 392 d.Set("hostname", zone) 393 } else { 394 if strings.HasSuffix(r.OwnerName, ".") { 395 d.Set("hostname", r.OwnerName) 396 } else { 397 d.Set("hostname", fmt.Sprintf("%s.%s", r.OwnerName, zone)) 398 } 399 } 400 401 // And now... the Profile! 402 if r.Profile == nil { 403 return fmt.Errorf("RRSet.profile missing: invalid DirPool schema in: %#v", r) 404 } 405 p, err := r.Profile.DirPoolProfile() 406 if err != nil { 407 return fmt.Errorf("RRSet.profile could not be unmarshalled: %v\n", err) 408 } 409 410 // Set simple values 411 d.Set("description", p.Description) 412 413 // Ensure default looks like "GEO", even when nothing is returned 414 if p.ConflictResolve == "" { 415 d.Set("conflict_resolve", "GEO") 416 } else { 417 d.Set("conflict_resolve", p.ConflictResolve) 418 } 419 420 rd := makeSetFromDirpoolRdata(r.RData, p.RDataInfo) 421 err = d.Set("rdata", rd) 422 if err != nil { 423 return fmt.Errorf("rdata set failed: %v, from %#v", err, rd) 424 } 425 return nil 426 } 427 428 // makeDirpoolRdataInfos converts []map[string]interface{} from rdata 429 // blocks into []DPRDataInfo 430 func makeDirpoolRdataInfos(configured []interface{}) ([]udnssdk.DPRDataInfo, error) { 431 res := make([]udnssdk.DPRDataInfo, 0, len(configured)) 432 for _, r := range configured { 433 ri, err := makeDirpoolRdataInfo(r) 434 if err != nil { 435 return res, err 436 } 437 res = append(res, ri) 438 } 439 return res, nil 440 } 441 442 // makeDirpoolRdataInfo converts a map[string]interface{} from 443 // an rdata or no_response block into an DPRDataInfo 444 func makeDirpoolRdataInfo(configured interface{}) (udnssdk.DPRDataInfo, error) { 445 data := configured.(map[string]interface{}) 446 res := udnssdk.DPRDataInfo{ 447 AllNonConfigured: data["all_non_configured"].(bool), 448 } 449 // IPInfo 450 ipInfo := data["ip_info"].([]interface{}) 451 if len(ipInfo) >= 1 { 452 if len(ipInfo) > 1 { 453 return res, fmt.Errorf("ip_info: only 0 or 1 blocks alowed, got: %#v", len(ipInfo)) 454 } 455 ii, err := makeIPInfo(ipInfo[0]) 456 if err != nil { 457 return res, fmt.Errorf("%v ip_info: %#v", err, ii) 458 } 459 res.IPInfo = &ii 460 } 461 // GeoInfo 462 geoInfo := data["geo_info"].([]interface{}) 463 if len(geoInfo) >= 1 { 464 if len(geoInfo) > 1 { 465 return res, fmt.Errorf("geo_info: only 0 or 1 blocks alowed, got: %#v", len(geoInfo)) 466 } 467 gi, err := makeGeoInfo(geoInfo[0]) 468 if err != nil { 469 return res, fmt.Errorf("%v geo_info: %#v GeoInfo: %#v", err, geoInfo[0], gi) 470 } 471 res.GeoInfo = &gi 472 } 473 return res, nil 474 } 475 476 // makeGeoInfo converts a map[string]interface{} from an geo_info block 477 // into an GeoInfo 478 func makeGeoInfo(configured interface{}) (udnssdk.GeoInfo, error) { 479 var res udnssdk.GeoInfo 480 c := configured.(map[string]interface{}) 481 err := mapDecode(c, &res) 482 if err != nil { 483 return res, err 484 } 485 486 rawCodes := c["codes"].(*schema.Set).List() 487 res.Codes = make([]string, 0, len(rawCodes)) 488 for _, i := range rawCodes { 489 res.Codes = append(res.Codes, i.(string)) 490 } 491 return res, err 492 } 493 494 // makeIPInfo converts a map[string]interface{} from an ip_info block 495 // into an IPInfo 496 func makeIPInfo(configured interface{}) (udnssdk.IPInfo, error) { 497 var res udnssdk.IPInfo 498 c := configured.(map[string]interface{}) 499 err := mapDecode(c, &res) 500 if err != nil { 501 return res, err 502 } 503 504 rawIps := c["ips"].(*schema.Set).List() 505 res.Ips = make([]udnssdk.IPAddrDTO, 0, len(rawIps)) 506 for _, rawIa := range rawIps { 507 var i udnssdk.IPAddrDTO 508 err = mapDecode(rawIa, &i) 509 if err != nil { 510 return res, err 511 } 512 res.Ips = append(res.Ips, i) 513 } 514 return res, nil 515 } 516 517 // collate and zip RData and RDataInfo into []map[string]interface{} 518 func zipDirpoolRData(rds []string, rdis []udnssdk.DPRDataInfo) []map[string]interface{} { 519 result := make([]map[string]interface{}, 0, len(rds)) 520 for i, rdi := range rdis { 521 r := map[string]interface{}{ 522 "host": rds[i], 523 "all_non_configured": rdi.AllNonConfigured, 524 "ip_info": mapFromIPInfos(rdi.IPInfo), 525 "geo_info": mapFromGeoInfos(rdi.GeoInfo), 526 } 527 result = append(result, r) 528 } 529 return result 530 } 531 532 // makeSetFromDirpoolRdata encodes an array of Rdata into a 533 // *schema.Set in the appropriate structure for the schema 534 func makeSetFromDirpoolRdata(rds []string, rdis []udnssdk.DPRDataInfo) *schema.Set { 535 s := &schema.Set{F: hashRdatas} 536 rs := zipDirpoolRData(rds, rdis) 537 for _, r := range rs { 538 s.Add(r) 539 } 540 return s 541 } 542 543 // mapFromIPInfos encodes 0 or 1 IPInfos into a []map[string]interface{} 544 // in the appropriate structure for the schema 545 func mapFromIPInfos(rdi *udnssdk.IPInfo) []map[string]interface{} { 546 res := make([]map[string]interface{}, 0, 1) 547 if rdi != nil { 548 m := map[string]interface{}{ 549 "name": rdi.Name, 550 "is_account_level": rdi.IsAccountLevel, 551 "ips": makeSetFromIPAddrDTOs(rdi.Ips), 552 } 553 res = append(res, m) 554 } 555 return res 556 } 557 558 // makeSetFromIPAddrDTOs encodes an array of IPAddrDTO into a 559 // *schema.Set in the appropriate structure for the schema 560 func makeSetFromIPAddrDTOs(ias []udnssdk.IPAddrDTO) *schema.Set { 561 s := &schema.Set{F: hashIPInfoIPs} 562 for _, ia := range ias { 563 s.Add(mapEncode(ia)) 564 } 565 return s 566 } 567 568 // mapFromGeoInfos encodes 0 or 1 GeoInfos into a []map[string]interface{} 569 // in the appropriate structure for the schema 570 func mapFromGeoInfos(gi *udnssdk.GeoInfo) []map[string]interface{} { 571 res := make([]map[string]interface{}, 0, 1) 572 if gi != nil { 573 m := mapEncode(gi) 574 m["codes"] = makeSetFromStrings(gi.Codes) 575 res = append(res, m) 576 } 577 return res 578 } 579 580 // hashIPInfoIPs generates a hashcode for an ip_info.ips block 581 func hashIPInfoIPs(v interface{}) int { 582 var buf bytes.Buffer 583 m := v.(map[string]interface{}) 584 buf.WriteString(fmt.Sprintf("%s-", m["start"].(string))) 585 buf.WriteString(fmt.Sprintf("%s-", m["end"].(string))) 586 buf.WriteString(fmt.Sprintf("%s-", m["cidr"].(string))) 587 buf.WriteString(fmt.Sprintf("%s", m["address"].(string))) 588 589 h := hashcode.String(buf.String()) 590 log.Printf("[DEBUG] hashIPInfoIPs(): %v -> %v", buf.String(), h) 591 return h 592 } 593 594 // Map <-> Struct transcoding 595 // Ideally, we sould be able to handle almost all the type conversion 596 // in this resource using the following helpers. Unfortunately, some 597 // issues remain: 598 // - schema.Set values cannot be naively assigned, and must be 599 // manually converted 600 // - ip_info and geo_info come in as []map[string]interface{}, but are 601 // in DPRDataInfo as singluar. 602 603 // mapDecode takes a map[string]interface{} and uses reflection to 604 // convert it into the given Go native structure. val must be a pointer 605 // to a struct. This is identical to mapstructure.Decode, but uses the 606 // `terraform:` tag instead of `mapstructure:` 607 func mapDecode(m interface{}, rawVal interface{}) error { 608 config := &mapstructure.DecoderConfig{ 609 Metadata: nil, 610 TagName: "terraform", 611 Result: rawVal, 612 WeaklyTypedInput: true, 613 } 614 615 decoder, err := mapstructure.NewDecoder(config) 616 if err != nil { 617 return err 618 } 619 620 return decoder.Decode(m) 621 } 622 623 func mapEncode(rawVal interface{}) map[string]interface{} { 624 s := structs.New(rawVal) 625 s.TagName = "terraform" 626 return s.Map() 627 }