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