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