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  }