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 }