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 }