github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/builtin/providers/aws/resource_aws_redshift_cluster.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/redshift" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 ) 16 17 func resourceAwsRedshiftCluster() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsRedshiftClusterCreate, 20 Read: resourceAwsRedshiftClusterRead, 21 Update: resourceAwsRedshiftClusterUpdate, 22 Delete: resourceAwsRedshiftClusterDelete, 23 24 Schema: map[string]*schema.Schema{ 25 "database_name": &schema.Schema{ 26 Type: schema.TypeString, 27 Optional: true, 28 Computed: true, 29 ValidateFunc: validateRedshiftClusterDbName, 30 }, 31 32 "cluster_identifier": &schema.Schema{ 33 Type: schema.TypeString, 34 Required: true, 35 ForceNew: true, 36 ValidateFunc: validateRedshiftClusterIdentifier, 37 }, 38 "cluster_type": &schema.Schema{ 39 Type: schema.TypeString, 40 Optional: true, 41 Computed: true, 42 }, 43 44 "node_type": &schema.Schema{ 45 Type: schema.TypeString, 46 Required: true, 47 }, 48 49 "master_username": &schema.Schema{ 50 Type: schema.TypeString, 51 Required: true, 52 ValidateFunc: validateRedshiftClusterMasterUsername, 53 }, 54 55 "master_password": &schema.Schema{ 56 Type: schema.TypeString, 57 Required: true, 58 }, 59 60 "cluster_security_groups": &schema.Schema{ 61 Type: schema.TypeSet, 62 Optional: true, 63 Computed: true, 64 Elem: &schema.Schema{Type: schema.TypeString}, 65 Set: schema.HashString, 66 }, 67 68 "vpc_security_group_ids": &schema.Schema{ 69 Type: schema.TypeSet, 70 Optional: true, 71 Computed: true, 72 Elem: &schema.Schema{Type: schema.TypeString}, 73 Set: schema.HashString, 74 }, 75 76 "cluster_subnet_group_name": &schema.Schema{ 77 Type: schema.TypeString, 78 Optional: true, 79 ForceNew: true, 80 Computed: true, 81 }, 82 83 "availability_zone": &schema.Schema{ 84 Type: schema.TypeString, 85 Optional: true, 86 Computed: true, 87 }, 88 89 "preferred_maintenance_window": &schema.Schema{ 90 Type: schema.TypeString, 91 Optional: true, 92 Computed: true, 93 StateFunc: func(val interface{}) string { 94 if val == nil { 95 return "" 96 } 97 return strings.ToLower(val.(string)) 98 }, 99 }, 100 101 "cluster_parameter_group_name": &schema.Schema{ 102 Type: schema.TypeString, 103 Optional: true, 104 Computed: true, 105 }, 106 107 "automated_snapshot_retention_period": &schema.Schema{ 108 Type: schema.TypeInt, 109 Optional: true, 110 Default: 1, 111 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 112 value := v.(int) 113 if value > 35 { 114 es = append(es, fmt.Errorf( 115 "backup retention period cannot be more than 35 days")) 116 } 117 return 118 }, 119 }, 120 121 "port": &schema.Schema{ 122 Type: schema.TypeInt, 123 Optional: true, 124 Default: 5439, 125 }, 126 127 "cluster_version": &schema.Schema{ 128 Type: schema.TypeString, 129 Optional: true, 130 Default: "1.0", 131 }, 132 133 "allow_version_upgrade": &schema.Schema{ 134 Type: schema.TypeBool, 135 Optional: true, 136 Default: true, 137 }, 138 139 "number_of_nodes": &schema.Schema{ 140 Type: schema.TypeInt, 141 Optional: true, 142 Default: 1, 143 }, 144 145 "publicly_accessible": &schema.Schema{ 146 Type: schema.TypeBool, 147 Optional: true, 148 Default: true, 149 }, 150 151 "encrypted": &schema.Schema{ 152 Type: schema.TypeBool, 153 Optional: true, 154 Computed: true, 155 }, 156 157 "elastic_ip": &schema.Schema{ 158 Type: schema.TypeString, 159 Optional: true, 160 }, 161 162 "final_snapshot_identifier": &schema.Schema{ 163 Type: schema.TypeString, 164 Optional: true, 165 ValidateFunc: validateRedshiftClusterFinalSnapshotIdentifier, 166 }, 167 168 "skip_final_snapshot": &schema.Schema{ 169 Type: schema.TypeBool, 170 Optional: true, 171 Default: true, 172 }, 173 174 "endpoint": &schema.Schema{ 175 Type: schema.TypeString, 176 Optional: true, 177 Computed: true, 178 }, 179 180 "cluster_public_key": &schema.Schema{ 181 Type: schema.TypeString, 182 Optional: true, 183 Computed: true, 184 }, 185 186 "cluster_revision_number": &schema.Schema{ 187 Type: schema.TypeString, 188 Optional: true, 189 Computed: true, 190 }, 191 }, 192 } 193 } 194 195 func resourceAwsRedshiftClusterCreate(d *schema.ResourceData, meta interface{}) error { 196 conn := meta.(*AWSClient).redshiftconn 197 198 log.Printf("[INFO] Building Redshift Cluster Options") 199 createOpts := &redshift.CreateClusterInput{ 200 ClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)), 201 Port: aws.Int64(int64(d.Get("port").(int))), 202 MasterUserPassword: aws.String(d.Get("master_password").(string)), 203 MasterUsername: aws.String(d.Get("master_username").(string)), 204 ClusterVersion: aws.String(d.Get("cluster_version").(string)), 205 NodeType: aws.String(d.Get("node_type").(string)), 206 DBName: aws.String(d.Get("database_name").(string)), 207 AllowVersionUpgrade: aws.Bool(d.Get("allow_version_upgrade").(bool)), 208 PubliclyAccessible: aws.Bool(d.Get("publicly_accessible").(bool)), 209 } 210 211 if v := d.Get("number_of_nodes").(int); v > 1 { 212 createOpts.ClusterType = aws.String("multi-node") 213 createOpts.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int))) 214 } else { 215 createOpts.ClusterType = aws.String("single-node") 216 } 217 218 if v := d.Get("cluster_security_groups").(*schema.Set); v.Len() > 0 { 219 createOpts.ClusterSecurityGroups = expandStringList(v.List()) 220 } 221 222 if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { 223 createOpts.VpcSecurityGroupIds = expandStringList(v.List()) 224 } 225 226 if v, ok := d.GetOk("cluster_subnet_group_name"); ok { 227 createOpts.ClusterSubnetGroupName = aws.String(v.(string)) 228 } 229 230 if v, ok := d.GetOk("availability_zone"); ok { 231 createOpts.AvailabilityZone = aws.String(v.(string)) 232 } 233 234 if v, ok := d.GetOk("preferred_maintenance_window"); ok { 235 createOpts.PreferredMaintenanceWindow = aws.String(v.(string)) 236 } 237 238 if v, ok := d.GetOk("cluster_parameter_group_name"); ok { 239 createOpts.ClusterParameterGroupName = aws.String(v.(string)) 240 } 241 242 if v, ok := d.GetOk("automated_snapshot_retention_period"); ok { 243 createOpts.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(v.(int))) 244 } 245 246 if v, ok := d.GetOk("encrypted"); ok { 247 createOpts.Encrypted = aws.Bool(v.(bool)) 248 } 249 250 if v, ok := d.GetOk("elastic_ip"); ok { 251 createOpts.ElasticIp = aws.String(v.(string)) 252 } 253 254 log.Printf("[DEBUG] Redshift Cluster create options: %s", createOpts) 255 resp, err := conn.CreateCluster(createOpts) 256 if err != nil { 257 log.Printf("[ERROR] Error creating Redshift Cluster: %s", err) 258 return err 259 } 260 261 log.Printf("[DEBUG]: Cluster create response: %s", resp) 262 d.SetId(*resp.Cluster.ClusterIdentifier) 263 264 stateConf := &resource.StateChangeConf{ 265 Pending: []string{"creating", "backing-up", "modifying"}, 266 Target: []string{"available"}, 267 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 268 Timeout: 5 * time.Minute, 269 MinTimeout: 3 * time.Second, 270 } 271 272 _, err = stateConf.WaitForState() 273 if err != nil { 274 return fmt.Errorf("[WARN] Error waiting for Redshift Cluster state to be \"available\": %s", err) 275 } 276 277 return resourceAwsRedshiftClusterRead(d, meta) 278 } 279 280 func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) error { 281 conn := meta.(*AWSClient).redshiftconn 282 283 log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id()) 284 resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ 285 ClusterIdentifier: aws.String(d.Id()), 286 }) 287 288 if err != nil { 289 if awsErr, ok := err.(awserr.Error); ok { 290 if "ClusterNotFound" == awsErr.Code() { 291 d.SetId("") 292 log.Printf("[DEBUG] Redshift Cluster (%s) not found", d.Id()) 293 return nil 294 } 295 } 296 log.Printf("[DEBUG] Error describing Redshift Cluster (%s)", d.Id()) 297 return err 298 } 299 300 var rsc *redshift.Cluster 301 for _, c := range resp.Clusters { 302 if *c.ClusterIdentifier == d.Id() { 303 rsc = c 304 } 305 } 306 307 if rsc == nil { 308 log.Printf("[WARN] Redshift Cluster (%s) not found", d.Id()) 309 d.SetId("") 310 return nil 311 } 312 313 d.Set("database_name", rsc.DBName) 314 d.Set("cluster_subnet_group_name", rsc.ClusterSubnetGroupName) 315 d.Set("availability_zone", rsc.AvailabilityZone) 316 d.Set("encrypted", rsc.Encrypted) 317 d.Set("automated_snapshot_retention_period", rsc.AutomatedSnapshotRetentionPeriod) 318 d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow) 319 if rsc.Endpoint != nil && rsc.Endpoint.Address != nil { 320 endpoint := *rsc.Endpoint.Address 321 if rsc.Endpoint.Port != nil { 322 endpoint = fmt.Sprintf("%s:%d", endpoint, *rsc.Endpoint.Port) 323 } 324 d.Set("endpoint", endpoint) 325 } 326 d.Set("cluster_parameter_group_name", rsc.ClusterParameterGroups[0].ParameterGroupName) 327 if len(rsc.ClusterNodes) > 1 { 328 d.Set("cluster_type", "multi-node") 329 } else { 330 d.Set("cluster_type", "single-node") 331 } 332 333 var vpcg []string 334 for _, g := range rsc.VpcSecurityGroups { 335 vpcg = append(vpcg, *g.VpcSecurityGroupId) 336 } 337 if err := d.Set("vpc_security_group_ids", vpcg); err != nil { 338 return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for Redshift Cluster (%s): %s", d.Id(), err) 339 } 340 341 var csg []string 342 for _, g := range rsc.ClusterSecurityGroups { 343 csg = append(csg, *g.ClusterSecurityGroupName) 344 } 345 if err := d.Set("cluster_security_groups", csg); err != nil { 346 return fmt.Errorf("[DEBUG] Error saving Cluster Security Group Names to state for Redshift Cluster (%s): %s", d.Id(), err) 347 } 348 349 d.Set("cluster_public_key", rsc.ClusterPublicKey) 350 d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) 351 352 return nil 353 } 354 355 func resourceAwsRedshiftClusterUpdate(d *schema.ResourceData, meta interface{}) error { 356 conn := meta.(*AWSClient).redshiftconn 357 358 log.Printf("[INFO] Building Redshift Modify Cluster Options") 359 req := &redshift.ModifyClusterInput{ 360 ClusterIdentifier: aws.String(d.Id()), 361 } 362 363 if d.HasChange("cluster_type") { 364 req.ClusterType = aws.String(d.Get("cluster_type").(string)) 365 } 366 367 if d.HasChange("node_type") { 368 req.NodeType = aws.String(d.Get("node_type").(string)) 369 } 370 371 if d.HasChange("number_of_nodes") { 372 if v := d.Get("number_of_nodes").(int); v > 1 { 373 req.ClusterType = aws.String("multi-node") 374 req.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int))) 375 } else { 376 req.ClusterType = aws.String("single-node") 377 } 378 } 379 380 if d.HasChange("cluster_security_groups") { 381 req.ClusterSecurityGroups = expandStringList(d.Get("cluster_security_groups").(*schema.Set).List()) 382 } 383 384 if d.HasChange("vpc_security_group_ips") { 385 req.VpcSecurityGroupIds = expandStringList(d.Get("vpc_security_group_ips").(*schema.Set).List()) 386 } 387 388 if d.HasChange("master_password") { 389 req.MasterUserPassword = aws.String(d.Get("master_password").(string)) 390 } 391 392 if d.HasChange("cluster_parameter_group_name") { 393 req.ClusterParameterGroupName = aws.String(d.Get("cluster_parameter_group_name").(string)) 394 } 395 396 if d.HasChange("automated_snapshot_retention_period") { 397 req.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(d.Get("automated_snapshot_retention_period").(int))) 398 } 399 400 if d.HasChange("preferred_maintenance_window") { 401 req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) 402 } 403 404 if d.HasChange("cluster_version") { 405 req.ClusterVersion = aws.String(d.Get("cluster_version").(string)) 406 } 407 408 if d.HasChange("allow_version_upgrade") { 409 req.AllowVersionUpgrade = aws.Bool(d.Get("allow_version_upgrade").(bool)) 410 } 411 412 if d.HasChange("publicly_accessible") { 413 req.PubliclyAccessible = aws.Bool(d.Get("publicly_accessible").(bool)) 414 } 415 416 log.Printf("[INFO] Modifying Redshift Cluster: %s", d.Id()) 417 log.Printf("[DEBUG] Redshift Cluster Modify options: %s", req) 418 _, err := conn.ModifyCluster(req) 419 if err != nil { 420 return fmt.Errorf("[WARN] Error modifying Redshift Cluster (%s): %s", d.Id(), err) 421 } 422 423 stateConf := &resource.StateChangeConf{ 424 Pending: []string{"creating", "deleting", "rebooting", "resizing", "renaming", "modifying"}, 425 Target: []string{"available"}, 426 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 427 Timeout: 10 * time.Minute, 428 MinTimeout: 5 * time.Second, 429 } 430 431 // Wait, catching any errors 432 _, err = stateConf.WaitForState() 433 if err != nil { 434 return fmt.Errorf("[WARN] Error Modifying Redshift Cluster (%s): %s", d.Id(), err) 435 } 436 437 return resourceAwsRedshiftClusterRead(d, meta) 438 } 439 440 func resourceAwsRedshiftClusterDelete(d *schema.ResourceData, meta interface{}) error { 441 conn := meta.(*AWSClient).redshiftconn 442 log.Printf("[DEBUG] Destroying Redshift Cluster (%s)", d.Id()) 443 444 deleteOpts := redshift.DeleteClusterInput{ 445 ClusterIdentifier: aws.String(d.Id()), 446 } 447 448 skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) 449 deleteOpts.SkipFinalClusterSnapshot = aws.Bool(skipFinalSnapshot) 450 451 if !skipFinalSnapshot { 452 if name, present := d.GetOk("final_snapshot_identifier"); present { 453 deleteOpts.FinalClusterSnapshotIdentifier = aws.String(name.(string)) 454 } else { 455 return fmt.Errorf("Redshift Cluster Instance FinalSnapshotIdentifier is required when a final snapshot is required") 456 } 457 } 458 459 log.Printf("[DEBUG] Redshift Cluster delete options: %s", deleteOpts) 460 _, err := conn.DeleteCluster(&deleteOpts) 461 if err != nil { 462 return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err) 463 } 464 465 stateConf := &resource.StateChangeConf{ 466 Pending: []string{"available", "creating", "deleting", "rebooting", "resizing", "renaming"}, 467 Target: []string{"destroyed"}, 468 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 469 Timeout: 40 * time.Minute, 470 MinTimeout: 5 * time.Second, 471 } 472 473 // Wait, catching any errors 474 _, err = stateConf.WaitForState() 475 if err != nil { 476 return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err) 477 } 478 479 log.Printf("[INFO] Redshift Cluster %s successfully deleted", d.Id()) 480 481 return nil 482 } 483 484 func resourceAwsRedshiftClusterStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 485 return func() (interface{}, string, error) { 486 conn := meta.(*AWSClient).redshiftconn 487 488 log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id()) 489 resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ 490 ClusterIdentifier: aws.String(d.Id()), 491 }) 492 493 if err != nil { 494 if awsErr, ok := err.(awserr.Error); ok { 495 if "ClusterNotFound" == awsErr.Code() { 496 return 42, "destroyed", nil 497 } 498 } 499 log.Printf("[WARN] Error on retrieving Redshift Cluster (%s) when waiting: %s", d.Id(), err) 500 return nil, "", err 501 } 502 503 var rsc *redshift.Cluster 504 505 for _, c := range resp.Clusters { 506 if *c.ClusterIdentifier == d.Id() { 507 rsc = c 508 } 509 } 510 511 if rsc == nil { 512 return 42, "destroyed", nil 513 } 514 515 if rsc.ClusterStatus != nil { 516 log.Printf("[DEBUG] Redshift Cluster status (%s): %s", d.Id(), *rsc.ClusterStatus) 517 } 518 519 return rsc, *rsc.ClusterStatus, nil 520 } 521 } 522 523 func validateRedshiftClusterIdentifier(v interface{}, k string) (ws []string, errors []error) { 524 value := v.(string) 525 if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) { 526 errors = append(errors, fmt.Errorf( 527 "only lowercase alphanumeric characters and hyphens allowed in %q", k)) 528 } 529 if !regexp.MustCompile(`^[a-z]`).MatchString(value) { 530 errors = append(errors, fmt.Errorf( 531 "first character of %q must be a letter", k)) 532 } 533 if regexp.MustCompile(`--`).MatchString(value) { 534 errors = append(errors, fmt.Errorf( 535 "%q cannot contain two consecutive hyphens", k)) 536 } 537 if regexp.MustCompile(`-$`).MatchString(value) { 538 errors = append(errors, fmt.Errorf( 539 "%q cannot end with a hyphen", k)) 540 } 541 return 542 } 543 544 func validateRedshiftClusterDbName(v interface{}, k string) (ws []string, errors []error) { 545 value := v.(string) 546 if !regexp.MustCompile(`^[a-z]+$`).MatchString(value) { 547 errors = append(errors, fmt.Errorf( 548 "only lowercase letters characters allowed in %q", k)) 549 } 550 if len(value) > 64 { 551 errors = append(errors, fmt.Errorf( 552 "%q cannot be longer than 64 characters: %q", k, value)) 553 } 554 if value == "" { 555 errors = append(errors, fmt.Errorf( 556 "%q cannot be an empty string", k)) 557 } 558 559 return 560 } 561 562 func validateRedshiftClusterFinalSnapshotIdentifier(v interface{}, k string) (ws []string, errors []error) { 563 value := v.(string) 564 if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { 565 errors = append(errors, fmt.Errorf( 566 "only alphanumeric characters and hyphens allowed in %q", k)) 567 } 568 if regexp.MustCompile(`--`).MatchString(value) { 569 errors = append(errors, fmt.Errorf("%q cannot contain two consecutive hyphens", k)) 570 } 571 if regexp.MustCompile(`-$`).MatchString(value) { 572 errors = append(errors, fmt.Errorf("%q cannot end in a hyphen", k)) 573 } 574 if len(value) > 255 { 575 errors = append(errors, fmt.Errorf("%q cannot be more than 255 characters", k)) 576 } 577 return 578 } 579 580 func validateRedshiftClusterMasterUsername(v interface{}, k string) (ws []string, errors []error) { 581 value := v.(string) 582 if !regexp.MustCompile(`^\w+$`).MatchString(value) { 583 errors = append(errors, fmt.Errorf( 584 "only alphanumeric characters in %q", k)) 585 } 586 if !regexp.MustCompile(`^[A-Za-z]`).MatchString(value) { 587 errors = append(errors, fmt.Errorf( 588 "first character of %q must be a letter", k)) 589 } 590 if len(value) > 128 { 591 errors = append(errors, fmt.Errorf("%q cannot be more than 128 characters", k)) 592 } 593 return 594 }