terraform/builtin/providers/ultradns/resource_ultradns_dirpool.go

628 lines
17 KiB
Go

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()
}