github.com/ggriffiths/terraform@v0.9.0-beta1.0.20170222213024-79c4935604cb/builtin/providers/aws/resource_aws_elasticsearch_domain.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"regexp"
     7  	"time"
     8  
     9  	"github.com/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/awserr"
    11  	elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice"
    12  	"github.com/hashicorp/errwrap"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  )
    16  
    17  func resourceAwsElasticSearchDomain() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsElasticSearchDomainCreate,
    20  		Read:   resourceAwsElasticSearchDomainRead,
    21  		Update: resourceAwsElasticSearchDomainUpdate,
    22  		Delete: resourceAwsElasticSearchDomainDelete,
    23  
    24  		Schema: map[string]*schema.Schema{
    25  			"access_policies": &schema.Schema{
    26  				Type:             schema.TypeString,
    27  				Optional:         true,
    28  				Computed:         true,
    29  				ValidateFunc:     validateJsonString,
    30  				DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs,
    31  			},
    32  			"advanced_options": &schema.Schema{
    33  				Type:     schema.TypeMap,
    34  				Optional: true,
    35  				Computed: true,
    36  			},
    37  			"domain_name": &schema.Schema{
    38  				Type:     schema.TypeString,
    39  				Required: true,
    40  				ForceNew: true,
    41  				ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
    42  					value := v.(string)
    43  					if !regexp.MustCompile(`^[a-z][0-9a-z\-]{2,27}$`).MatchString(value) {
    44  						errors = append(errors, fmt.Errorf(
    45  							"%q must start with a lowercase alphabet and be at least 3 and no more than 28 characters long. Valid characters are a-z (lowercase letters), 0-9, and - (hyphen).", k))
    46  					}
    47  					return
    48  				},
    49  			},
    50  			"arn": &schema.Schema{
    51  				Type:     schema.TypeString,
    52  				Computed: true,
    53  			},
    54  			"domain_id": &schema.Schema{
    55  				Type:     schema.TypeString,
    56  				Computed: true,
    57  			},
    58  			"endpoint": &schema.Schema{
    59  				Type:     schema.TypeString,
    60  				Computed: true,
    61  			},
    62  			"ebs_options": &schema.Schema{
    63  				Type:     schema.TypeList,
    64  				Optional: true,
    65  				Computed: true,
    66  				Elem: &schema.Resource{
    67  					Schema: map[string]*schema.Schema{
    68  						"ebs_enabled": &schema.Schema{
    69  							Type:     schema.TypeBool,
    70  							Required: true,
    71  						},
    72  						"iops": &schema.Schema{
    73  							Type:     schema.TypeInt,
    74  							Optional: true,
    75  						},
    76  						"volume_size": &schema.Schema{
    77  							Type:     schema.TypeInt,
    78  							Optional: true,
    79  						},
    80  						"volume_type": &schema.Schema{
    81  							Type:     schema.TypeString,
    82  							Optional: true,
    83  						},
    84  					},
    85  				},
    86  			},
    87  			"cluster_config": &schema.Schema{
    88  				Type:     schema.TypeList,
    89  				Optional: true,
    90  				Computed: true,
    91  				Elem: &schema.Resource{
    92  					Schema: map[string]*schema.Schema{
    93  						"dedicated_master_count": &schema.Schema{
    94  							Type:     schema.TypeInt,
    95  							Optional: true,
    96  						},
    97  						"dedicated_master_enabled": &schema.Schema{
    98  							Type:     schema.TypeBool,
    99  							Optional: true,
   100  							Default:  false,
   101  						},
   102  						"dedicated_master_type": &schema.Schema{
   103  							Type:     schema.TypeString,
   104  							Optional: true,
   105  						},
   106  						"instance_count": &schema.Schema{
   107  							Type:     schema.TypeInt,
   108  							Optional: true,
   109  							Default:  1,
   110  						},
   111  						"instance_type": &schema.Schema{
   112  							Type:     schema.TypeString,
   113  							Optional: true,
   114  							Default:  "m3.medium.elasticsearch",
   115  						},
   116  						"zone_awareness_enabled": &schema.Schema{
   117  							Type:     schema.TypeBool,
   118  							Optional: true,
   119  						},
   120  					},
   121  				},
   122  			},
   123  			"snapshot_options": &schema.Schema{
   124  				Type:     schema.TypeList,
   125  				Optional: true,
   126  				Elem: &schema.Resource{
   127  					Schema: map[string]*schema.Schema{
   128  						"automated_snapshot_start_hour": &schema.Schema{
   129  							Type:     schema.TypeInt,
   130  							Required: true,
   131  						},
   132  					},
   133  				},
   134  			},
   135  			"elasticsearch_version": &schema.Schema{
   136  				Type:     schema.TypeString,
   137  				Optional: true,
   138  				Default:  "1.5",
   139  				ForceNew: true,
   140  			},
   141  
   142  			"tags": tagsSchema(),
   143  		},
   144  	}
   145  }
   146  
   147  func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface{}) error {
   148  	conn := meta.(*AWSClient).esconn
   149  
   150  	input := elasticsearch.CreateElasticsearchDomainInput{
   151  		DomainName:           aws.String(d.Get("domain_name").(string)),
   152  		ElasticsearchVersion: aws.String(d.Get("elasticsearch_version").(string)),
   153  	}
   154  
   155  	if v, ok := d.GetOk("access_policies"); ok {
   156  		input.AccessPolicies = aws.String(v.(string))
   157  	}
   158  
   159  	if v, ok := d.GetOk("advanced_options"); ok {
   160  		input.AdvancedOptions = stringMapToPointers(v.(map[string]interface{}))
   161  	}
   162  
   163  	if v, ok := d.GetOk("ebs_options"); ok {
   164  		options := v.([]interface{})
   165  
   166  		if len(options) > 1 {
   167  			return fmt.Errorf("Only a single ebs_options block is expected")
   168  		} else if len(options) == 1 {
   169  			if options[0] == nil {
   170  				return fmt.Errorf("At least one field is expected inside ebs_options")
   171  			}
   172  
   173  			s := options[0].(map[string]interface{})
   174  			input.EBSOptions = expandESEBSOptions(s)
   175  		}
   176  	}
   177  
   178  	if v, ok := d.GetOk("cluster_config"); ok {
   179  		config := v.([]interface{})
   180  
   181  		if len(config) > 1 {
   182  			return fmt.Errorf("Only a single cluster_config block is expected")
   183  		} else if len(config) == 1 {
   184  			if config[0] == nil {
   185  				return fmt.Errorf("At least one field is expected inside cluster_config")
   186  			}
   187  			m := config[0].(map[string]interface{})
   188  			input.ElasticsearchClusterConfig = expandESClusterConfig(m)
   189  		}
   190  	}
   191  
   192  	if v, ok := d.GetOk("snapshot_options"); ok {
   193  		options := v.([]interface{})
   194  
   195  		if len(options) > 1 {
   196  			return fmt.Errorf("Only a single snapshot_options block is expected")
   197  		} else if len(options) == 1 {
   198  			if options[0] == nil {
   199  				return fmt.Errorf("At least one field is expected inside snapshot_options")
   200  			}
   201  
   202  			o := options[0].(map[string]interface{})
   203  
   204  			snapshotOptions := elasticsearch.SnapshotOptions{
   205  				AutomatedSnapshotStartHour: aws.Int64(int64(o["automated_snapshot_start_hour"].(int))),
   206  			}
   207  
   208  			input.SnapshotOptions = &snapshotOptions
   209  		}
   210  	}
   211  
   212  	log.Printf("[DEBUG] Creating ElasticSearch domain: %s", input)
   213  	out, err := conn.CreateElasticsearchDomain(&input)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	d.SetId(*out.DomainStatus.ARN)
   219  
   220  	log.Printf("[DEBUG] Waiting for ElasticSearch domain %q to be created", d.Id())
   221  	err = resource.Retry(60*time.Minute, func() *resource.RetryError {
   222  		out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{
   223  			DomainName: aws.String(d.Get("domain_name").(string)),
   224  		})
   225  		if err != nil {
   226  			return resource.NonRetryableError(err)
   227  		}
   228  
   229  		if !*out.DomainStatus.Processing && out.DomainStatus.Endpoint != nil {
   230  			return nil
   231  		}
   232  
   233  		return resource.RetryableError(
   234  			fmt.Errorf("%q: Timeout while waiting for the domain to be created", d.Id()))
   235  	})
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	tags := tagsFromMapElasticsearchService(d.Get("tags").(map[string]interface{}))
   241  
   242  	if err := setTagsElasticsearchService(conn, d, *out.DomainStatus.ARN); err != nil {
   243  		return err
   244  	}
   245  
   246  	d.Set("tags", tagsToMapElasticsearchService(tags))
   247  	d.SetPartial("tags")
   248  	d.Partial(false)
   249  
   250  	log.Printf("[DEBUG] ElasticSearch domain %q created", d.Id())
   251  
   252  	return resourceAwsElasticSearchDomainRead(d, meta)
   253  }
   254  
   255  func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{}) error {
   256  	conn := meta.(*AWSClient).esconn
   257  
   258  	out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{
   259  		DomainName: aws.String(d.Get("domain_name").(string)),
   260  	})
   261  	if err != nil {
   262  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ResourceNotFoundException" {
   263  			log.Printf("[INFO] ElasticSearch Domain %q not found", d.Get("domain_name").(string))
   264  			d.SetId("")
   265  			return nil
   266  		}
   267  		return err
   268  	}
   269  
   270  	log.Printf("[DEBUG] Received ElasticSearch domain: %s", out)
   271  
   272  	ds := out.DomainStatus
   273  
   274  	if ds.AccessPolicies != nil && *ds.AccessPolicies != "" {
   275  		policies, err := normalizeJsonString(*ds.AccessPolicies)
   276  		if err != nil {
   277  			return errwrap.Wrapf("access policies contain an invalid JSON: {{err}}", err)
   278  		}
   279  		d.Set("access_policies", policies)
   280  	}
   281  	err = d.Set("advanced_options", pointersMapToStringList(ds.AdvancedOptions))
   282  	if err != nil {
   283  		return err
   284  	}
   285  	d.Set("domain_id", ds.DomainId)
   286  	d.Set("domain_name", ds.DomainName)
   287  	d.Set("elasticsearch_version", ds.ElasticsearchVersion)
   288  	if ds.Endpoint != nil {
   289  		d.Set("endpoint", *ds.Endpoint)
   290  	}
   291  
   292  	err = d.Set("ebs_options", flattenESEBSOptions(ds.EBSOptions))
   293  	if err != nil {
   294  		return err
   295  	}
   296  	err = d.Set("cluster_config", flattenESClusterConfig(ds.ElasticsearchClusterConfig))
   297  	if err != nil {
   298  		return err
   299  	}
   300  	if ds.SnapshotOptions != nil {
   301  		d.Set("snapshot_options", map[string]interface{}{
   302  			"automated_snapshot_start_hour": *ds.SnapshotOptions.AutomatedSnapshotStartHour,
   303  		})
   304  	}
   305  
   306  	d.Set("arn", ds.ARN)
   307  
   308  	listOut, err := conn.ListTags(&elasticsearch.ListTagsInput{
   309  		ARN: ds.ARN,
   310  	})
   311  
   312  	if err != nil {
   313  		return err
   314  	}
   315  	var est []*elasticsearch.Tag
   316  	if len(listOut.TagList) > 0 {
   317  		est = listOut.TagList
   318  	}
   319  
   320  	d.Set("tags", tagsToMapElasticsearchService(est))
   321  
   322  	return nil
   323  }
   324  
   325  func resourceAwsElasticSearchDomainUpdate(d *schema.ResourceData, meta interface{}) error {
   326  	conn := meta.(*AWSClient).esconn
   327  
   328  	d.Partial(true)
   329  
   330  	if err := setTagsElasticsearchService(conn, d, d.Id()); err != nil {
   331  		return err
   332  	} else {
   333  		d.SetPartial("tags")
   334  	}
   335  
   336  	input := elasticsearch.UpdateElasticsearchDomainConfigInput{
   337  		DomainName: aws.String(d.Get("domain_name").(string)),
   338  	}
   339  
   340  	if d.HasChange("access_policies") {
   341  		input.AccessPolicies = aws.String(d.Get("access_policies").(string))
   342  	}
   343  
   344  	if d.HasChange("advanced_options") {
   345  		input.AdvancedOptions = stringMapToPointers(d.Get("advanced_options").(map[string]interface{}))
   346  	}
   347  
   348  	if d.HasChange("ebs_options") {
   349  		options := d.Get("ebs_options").([]interface{})
   350  
   351  		if len(options) > 1 {
   352  			return fmt.Errorf("Only a single ebs_options block is expected")
   353  		} else if len(options) == 1 {
   354  			s := options[0].(map[string]interface{})
   355  			input.EBSOptions = expandESEBSOptions(s)
   356  		}
   357  	}
   358  
   359  	if d.HasChange("cluster_config") {
   360  		config := d.Get("cluster_config").([]interface{})
   361  
   362  		if len(config) > 1 {
   363  			return fmt.Errorf("Only a single cluster_config block is expected")
   364  		} else if len(config) == 1 {
   365  			m := config[0].(map[string]interface{})
   366  			input.ElasticsearchClusterConfig = expandESClusterConfig(m)
   367  		}
   368  	}
   369  
   370  	if d.HasChange("snapshot_options") {
   371  		options := d.Get("snapshot_options").([]interface{})
   372  
   373  		if len(options) > 1 {
   374  			return fmt.Errorf("Only a single snapshot_options block is expected")
   375  		} else if len(options) == 1 {
   376  			o := options[0].(map[string]interface{})
   377  
   378  			snapshotOptions := elasticsearch.SnapshotOptions{
   379  				AutomatedSnapshotStartHour: aws.Int64(int64(o["automated_snapshot_start_hour"].(int))),
   380  			}
   381  
   382  			input.SnapshotOptions = &snapshotOptions
   383  		}
   384  	}
   385  
   386  	_, err := conn.UpdateElasticsearchDomainConfig(&input)
   387  	if err != nil {
   388  		return err
   389  	}
   390  
   391  	err = resource.Retry(60*time.Minute, func() *resource.RetryError {
   392  		out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{
   393  			DomainName: aws.String(d.Get("domain_name").(string)),
   394  		})
   395  		if err != nil {
   396  			return resource.NonRetryableError(err)
   397  		}
   398  
   399  		if *out.DomainStatus.Processing == false {
   400  			return nil
   401  		}
   402  
   403  		return resource.RetryableError(
   404  			fmt.Errorf("%q: Timeout while waiting for changes to be processed", d.Id()))
   405  	})
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	d.Partial(false)
   411  
   412  	return resourceAwsElasticSearchDomainRead(d, meta)
   413  }
   414  
   415  func resourceAwsElasticSearchDomainDelete(d *schema.ResourceData, meta interface{}) error {
   416  	conn := meta.(*AWSClient).esconn
   417  
   418  	log.Printf("[DEBUG] Deleting ElasticSearch domain: %q", d.Get("domain_name").(string))
   419  	_, err := conn.DeleteElasticsearchDomain(&elasticsearch.DeleteElasticsearchDomainInput{
   420  		DomainName: aws.String(d.Get("domain_name").(string)),
   421  	})
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	log.Printf("[DEBUG] Waiting for ElasticSearch domain %q to be deleted", d.Get("domain_name").(string))
   427  	err = resource.Retry(90*time.Minute, func() *resource.RetryError {
   428  		out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{
   429  			DomainName: aws.String(d.Get("domain_name").(string)),
   430  		})
   431  
   432  		if err != nil {
   433  			awsErr, ok := err.(awserr.Error)
   434  			if !ok {
   435  				return resource.NonRetryableError(err)
   436  			}
   437  
   438  			if awsErr.Code() == "ResourceNotFoundException" {
   439  				return nil
   440  			}
   441  
   442  			return resource.NonRetryableError(err)
   443  		}
   444  
   445  		if !*out.DomainStatus.Processing {
   446  			return nil
   447  		}
   448  
   449  		return resource.RetryableError(
   450  			fmt.Errorf("%q: Timeout while waiting for the domain to be deleted", d.Id()))
   451  	})
   452  
   453  	d.SetId("")
   454  
   455  	return err
   456  }