package ultradns import ( "bytes" "encoding/json" "fmt" "log" "strings" "github.com/Ensighten/udnssdk" "github.com/fatih/structs" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" "github.com/mitchellh/mapstructure" ) func resourceUltradnsDirpool() *schema.Resource { return &schema.Resource{ Create: resourceUltradnsDirpoolCreate, Read: resourceUltradnsDirpoolRead, Update: resourceUltradnsDirpoolUpdate, Delete: resourceUltradnsDirpoolDelete, Schema: map[string]*schema.Schema{ // Required "zone": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "name": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "type": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "description": &schema.Schema{ Type: schema.TypeString, Required: true, ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if len(value) > 255 { errors = append(errors, fmt.Errorf( "'description' too long, must be less than 255 characters")) } return }, }, "rdata": &schema.Schema{ // UltraDNS API does not respect rdata ordering Type: schema.TypeSet, Set: hashRdatas, Required: true, // Valid: len(rdataInfo) == len(rdata) Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ // Required "host": &schema.Schema{ Type: schema.TypeString, Required: true, }, "all_non_configured": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "geo_info": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "is_account_level": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "codes": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, }, }, }, "ip_info": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "is_account_level": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "ips": &schema.Schema{ Type: schema.TypeSet, Optional: true, Set: hashIPInfoIPs, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "start": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"cidr", "address"}, }, "end": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"cidr", "address"}, }, "cidr": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"start", "end", "address"}, }, "address": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"start", "end", "cidr"}, }, }, }, }, }, }, }, }, }, }, // Optional "ttl": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: 3600, }, "conflict_resolve": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "GEO", ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if value != "GEO" && value != "IP" { errors = append(errors, fmt.Errorf( "only 'GEO', and 'IP' are supported values for 'conflict_resolve'")) } return }, }, "no_response": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "all_non_configured": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "geo_info": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "is_account_level": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "codes": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, }, }, }, "ip_info": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "is_account_level": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "ips": &schema.Schema{ Type: schema.TypeSet, Optional: true, Set: hashIPInfoIPs, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "start": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"cidr", "address"}, }, "end": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"cidr", "address"}, }, "cidr": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"start", "end", "address"}, }, "address": &schema.Schema{ Type: schema.TypeString, Optional: true, // ConflictsWith: []string{"start", "end", "cidr"}, }, }, }, }, }, }, }, }, }, }, // Computed "hostname": &schema.Schema{ Type: schema.TypeString, Computed: true, }, }, } } // CRUD Operations func resourceUltradnsDirpoolCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*udnssdk.Client) r, err := makeDirpoolRRSetResource(d) if err != nil { return err } log.Printf("[INFO] ultradns_dirpool create: %#v", r) _, err = client.RRSets.Create(r.RRSetKey(), r.RRSet()) if err != nil { // FIXME: remove the json from log marshalled, _ := json.Marshal(r) ms := string(marshalled) return fmt.Errorf("create failed: %#v [[[[ %v ]]]] -> %v", r, ms, err) } d.SetId(r.ID()) log.Printf("[INFO] ultradns_dirpool.id: %v", d.Id()) return resourceUltradnsDirpoolRead(d, meta) } func resourceUltradnsDirpoolRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*udnssdk.Client) rr, err := makeDirpoolRRSetResource(d) if err != nil { return err } rrsets, err := client.RRSets.Select(rr.RRSetKey()) if err != nil { uderr, ok := err.(*udnssdk.ErrorResponseList) if ok { for _, resps := range uderr.Responses { // 70002 means Records Not Found if resps.ErrorCode == 70002 { d.SetId("") return nil } return fmt.Errorf("resource not found: %v", err) } } return fmt.Errorf("resource not found: %v", err) } r := rrsets[0] return populateResourceFromDirpool(d, &r) } func resourceUltradnsDirpoolUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*udnssdk.Client) r, err := makeDirpoolRRSetResource(d) if err != nil { return err } log.Printf("[INFO] ultradns_dirpool update: %+v", r) _, err = client.RRSets.Update(r.RRSetKey(), r.RRSet()) if err != nil { return fmt.Errorf("resource update failed: %v", err) } return resourceUltradnsDirpoolRead(d, meta) } func resourceUltradnsDirpoolDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*udnssdk.Client) r, err := makeDirpoolRRSetResource(d) if err != nil { return err } log.Printf("[INFO] ultradns_dirpool delete: %+v", r) _, err = client.RRSets.Delete(r.RRSetKey()) if err != nil { return fmt.Errorf("resource delete failed: %v", err) } return nil } // Resource Helpers // makeDirpoolRRSetResource converts ResourceData into an rRSetResource // ready for use in any CRUD operation func makeDirpoolRRSetResource(d *schema.ResourceData) (rRSetResource, error) { rDataRaw := d.Get("rdata").(*schema.Set).List() res := rRSetResource{ RRType: d.Get("type").(string), Zone: d.Get("zone").(string), OwnerName: d.Get("name").(string), TTL: d.Get("ttl").(int), RData: unzipRdataHosts(rDataRaw), } profile := udnssdk.DirPoolProfile{ Context: udnssdk.DirPoolSchema, Description: d.Get("description").(string), ConflictResolve: d.Get("conflict_resolve").(string), } ri, err := makeDirpoolRdataInfos(rDataRaw) if err != nil { return res, err } profile.RDataInfo = ri noResponseRaw := d.Get("no_response").([]interface{}) if len(noResponseRaw) >= 1 { if len(noResponseRaw) > 1 { return res, fmt.Errorf("no_response: only 0 or 1 blocks alowed, got: %#v", len(noResponseRaw)) } nr, err := makeDirpoolRdataInfo(noResponseRaw[0]) if err != nil { return res, err } profile.NoResponse = nr } res.Profile = profile.RawProfile() return res, nil } // populateResourceFromDirpool takes an RRSet and populates the ResourceData func populateResourceFromDirpool(d *schema.ResourceData, r *udnssdk.RRSet) error { // TODO: fix from tcpool to dirpool zone := d.Get("zone") // ttl d.Set("ttl", r.TTL) // hostname if r.OwnerName == "" { d.Set("hostname", zone) } else { if strings.HasSuffix(r.OwnerName, ".") { d.Set("hostname", r.OwnerName) } else { d.Set("hostname", fmt.Sprintf("%s.%s", r.OwnerName, zone)) } } // And now... the Profile! if r.Profile == nil { return fmt.Errorf("RRSet.profile missing: invalid DirPool schema in: %#v", r) } p, err := r.Profile.DirPoolProfile() if err != nil { return fmt.Errorf("RRSet.profile could not be unmarshalled: %v\n", err) } // Set simple values d.Set("description", p.Description) // Ensure default looks like "GEO", even when nothing is returned if p.ConflictResolve == "" { d.Set("conflict_resolve", "GEO") } else { d.Set("conflict_resolve", p.ConflictResolve) } rd := makeSetFromDirpoolRdata(r.RData, p.RDataInfo) err = d.Set("rdata", rd) if err != nil { return fmt.Errorf("rdata set failed: %v, from %#v", err, rd) } return nil } // makeDirpoolRdataInfos converts []map[string]interface{} from rdata // blocks into []DPRDataInfo func makeDirpoolRdataInfos(configured []interface{}) ([]udnssdk.DPRDataInfo, error) { res := make([]udnssdk.DPRDataInfo, 0, len(configured)) for _, r := range configured { ri, err := makeDirpoolRdataInfo(r) if err != nil { return res, err } res = append(res, ri) } return res, nil } // makeDirpoolRdataInfo converts a map[string]interface{} from // an rdata or no_response block into an DPRDataInfo func makeDirpoolRdataInfo(configured interface{}) (udnssdk.DPRDataInfo, error) { data := configured.(map[string]interface{}) res := udnssdk.DPRDataInfo{ AllNonConfigured: data["all_non_configured"].(bool), } // IPInfo ipInfo := data["ip_info"].([]interface{}) if len(ipInfo) >= 1 { if len(ipInfo) > 1 { return res, fmt.Errorf("ip_info: only 0 or 1 blocks alowed, got: %#v", len(ipInfo)) } ii, err := makeIPInfo(ipInfo[0]) if err != nil { return res, fmt.Errorf("%v ip_info: %#v", err, ii) } res.IPInfo = &ii } // GeoInfo geoInfo := data["geo_info"].([]interface{}) if len(geoInfo) >= 1 { if len(geoInfo) > 1 { return res, fmt.Errorf("geo_info: only 0 or 1 blocks alowed, got: %#v", len(geoInfo)) } gi, err := makeGeoInfo(geoInfo[0]) if err != nil { return res, fmt.Errorf("%v geo_info: %#v GeoInfo: %#v", err, geoInfo[0], gi) } res.GeoInfo = &gi } return res, nil } // makeGeoInfo converts a map[string]interface{} from an geo_info block // into an GeoInfo func makeGeoInfo(configured interface{}) (udnssdk.GeoInfo, error) { var res udnssdk.GeoInfo c := configured.(map[string]interface{}) err := mapDecode(c, &res) if err != nil { return res, err } rawCodes := c["codes"].(*schema.Set).List() res.Codes = make([]string, 0, len(rawCodes)) for _, i := range rawCodes { res.Codes = append(res.Codes, i.(string)) } return res, err } // makeIPInfo converts a map[string]interface{} from an ip_info block // into an IPInfo func makeIPInfo(configured interface{}) (udnssdk.IPInfo, error) { var res udnssdk.IPInfo c := configured.(map[string]interface{}) err := mapDecode(c, &res) if err != nil { return res, err } rawIps := c["ips"].(*schema.Set).List() res.Ips = make([]udnssdk.IPAddrDTO, 0, len(rawIps)) for _, rawIa := range rawIps { var i udnssdk.IPAddrDTO err = mapDecode(rawIa, &i) if err != nil { return res, err } res.Ips = append(res.Ips, i) } return res, nil } // collate and zip RData and RDataInfo into []map[string]interface{} func zipDirpoolRData(rds []string, rdis []udnssdk.DPRDataInfo) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(rds)) for i, rdi := range rdis { r := map[string]interface{}{ "host": rds[i], "all_non_configured": rdi.AllNonConfigured, "ip_info": mapFromIPInfos(rdi.IPInfo), "geo_info": mapFromGeoInfos(rdi.GeoInfo), } result = append(result, r) } return result } // makeSetFromDirpoolRdata encodes an array of Rdata into a // *schema.Set in the appropriate structure for the schema func makeSetFromDirpoolRdata(rds []string, rdis []udnssdk.DPRDataInfo) *schema.Set { s := &schema.Set{F: hashRdatas} rs := zipDirpoolRData(rds, rdis) for _, r := range rs { s.Add(r) } return s } // mapFromIPInfos encodes 0 or 1 IPInfos into a []map[string]interface{} // in the appropriate structure for the schema func mapFromIPInfos(rdi *udnssdk.IPInfo) []map[string]interface{} { res := make([]map[string]interface{}, 0, 1) if rdi != nil { m := map[string]interface{}{ "name": rdi.Name, "is_account_level": rdi.IsAccountLevel, "ips": makeSetFromIPAddrDTOs(rdi.Ips), } res = append(res, m) } return res } // makeSetFromIPAddrDTOs encodes an array of IPAddrDTO into a // *schema.Set in the appropriate structure for the schema func makeSetFromIPAddrDTOs(ias []udnssdk.IPAddrDTO) *schema.Set { s := &schema.Set{F: hashIPInfoIPs} for _, ia := range ias { s.Add(mapEncode(ia)) } return s } // mapFromGeoInfos encodes 0 or 1 GeoInfos into a []map[string]interface{} // in the appropriate structure for the schema func mapFromGeoInfos(gi *udnssdk.GeoInfo) []map[string]interface{} { res := make([]map[string]interface{}, 0, 1) if gi != nil { m := mapEncode(gi) m["codes"] = makeSetFromStrings(gi.Codes) res = append(res, m) } return res } // hashIPInfoIPs generates a hashcode for an ip_info.ips block func hashIPInfoIPs(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["start"].(string))) buf.WriteString(fmt.Sprintf("%s-", m["end"].(string))) buf.WriteString(fmt.Sprintf("%s-", m["cidr"].(string))) buf.WriteString(fmt.Sprintf("%s", m["address"].(string))) h := hashcode.String(buf.String()) log.Printf("[DEBUG] hashIPInfoIPs(): %v -> %v", buf.String(), h) return h } // Map <-> Struct transcoding // Ideally, we sould be able to handle almost all the type conversion // in this resource using the following helpers. Unfortunately, some // issues remain: // - schema.Set values cannot be naively assigned, and must be // manually converted // - ip_info and geo_info come in as []map[string]interface{}, but are // in DPRDataInfo as singluar. // mapDecode takes a map[string]interface{} and uses reflection to // convert it into the given Go native structure. val must be a pointer // to a struct. This is identical to mapstructure.Decode, but uses the // `terraform:` tag instead of `mapstructure:` func mapDecode(m interface{}, rawVal interface{}) error { config := &mapstructure.DecoderConfig{ Metadata: nil, TagName: "terraform", Result: rawVal, WeaklyTypedInput: true, } decoder, err := mapstructure.NewDecoder(config) if err != nil { return err } return decoder.Decode(m) } func mapEncode(rawVal interface{}) map[string]interface{} { s := structs.New(rawVal) s.TagName = "terraform" return s.Map() }