github.com/articulate/terraform@v0.6.13-0.20160303003731-8d31c93862de/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 ForceNew: true, 149 Default: true, 150 }, 151 152 "encrypted": &schema.Schema{ 153 Type: schema.TypeBool, 154 Optional: true, 155 Computed: true, 156 }, 157 158 "elastic_ip": &schema.Schema{ 159 Type: schema.TypeString, 160 Optional: true, 161 }, 162 163 "final_snapshot_identifier": &schema.Schema{ 164 Type: schema.TypeString, 165 Optional: true, 166 ValidateFunc: validateRedshiftClusterFinalSnapshotIdentifier, 167 }, 168 169 "skip_final_snapshot": &schema.Schema{ 170 Type: schema.TypeBool, 171 Optional: true, 172 Default: true, 173 }, 174 175 "endpoint": &schema.Schema{ 176 Type: schema.TypeString, 177 Optional: true, 178 Computed: true, 179 }, 180 181 "cluster_public_key": &schema.Schema{ 182 Type: schema.TypeString, 183 Optional: true, 184 Computed: true, 185 }, 186 187 "cluster_revision_number": &schema.Schema{ 188 Type: schema.TypeString, 189 Optional: true, 190 Computed: true, 191 }, 192 }, 193 } 194 } 195 196 func resourceAwsRedshiftClusterCreate(d *schema.ResourceData, meta interface{}) error { 197 conn := meta.(*AWSClient).redshiftconn 198 199 log.Printf("[INFO] Building Redshift Cluster Options") 200 createOpts := &redshift.CreateClusterInput{ 201 ClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)), 202 Port: aws.Int64(int64(d.Get("port").(int))), 203 MasterUserPassword: aws.String(d.Get("master_password").(string)), 204 MasterUsername: aws.String(d.Get("master_username").(string)), 205 ClusterVersion: aws.String(d.Get("cluster_version").(string)), 206 NodeType: aws.String(d.Get("node_type").(string)), 207 DBName: aws.String(d.Get("database_name").(string)), 208 AllowVersionUpgrade: aws.Bool(d.Get("allow_version_upgrade").(bool)), 209 PubliclyAccessible: aws.Bool(d.Get("publicly_accessible").(bool)), 210 } 211 212 if v := d.Get("number_of_nodes").(int); v > 1 { 213 createOpts.ClusterType = aws.String("multi-node") 214 createOpts.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int))) 215 } else { 216 createOpts.ClusterType = aws.String("single-node") 217 } 218 219 if v := d.Get("cluster_security_groups").(*schema.Set); v.Len() > 0 { 220 createOpts.ClusterSecurityGroups = expandStringList(v.List()) 221 } 222 223 if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { 224 createOpts.VpcSecurityGroupIds = expandStringList(v.List()) 225 } 226 227 if v, ok := d.GetOk("cluster_subnet_group_name"); ok { 228 createOpts.ClusterSubnetGroupName = aws.String(v.(string)) 229 } 230 231 if v, ok := d.GetOk("availability_zone"); ok { 232 createOpts.AvailabilityZone = aws.String(v.(string)) 233 } 234 235 if v, ok := d.GetOk("preferred_maintenance_window"); ok { 236 createOpts.PreferredMaintenanceWindow = aws.String(v.(string)) 237 } 238 239 if v, ok := d.GetOk("cluster_parameter_group_name"); ok { 240 createOpts.ClusterParameterGroupName = aws.String(v.(string)) 241 } 242 243 if v, ok := d.GetOk("automated_snapshot_retention_period"); ok { 244 createOpts.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(v.(int))) 245 } 246 247 if v, ok := d.GetOk("encrypted"); ok { 248 createOpts.Encrypted = aws.Bool(v.(bool)) 249 } 250 251 if v, ok := d.GetOk("elastic_ip"); ok { 252 createOpts.ElasticIp = aws.String(v.(string)) 253 } 254 255 log.Printf("[DEBUG] Redshift Cluster create options: %s", createOpts) 256 resp, err := conn.CreateCluster(createOpts) 257 if err != nil { 258 log.Printf("[ERROR] Error creating Redshift Cluster: %s", err) 259 return err 260 } 261 262 log.Printf("[DEBUG]: Cluster create response: %s", resp) 263 d.SetId(*resp.Cluster.ClusterIdentifier) 264 265 stateConf := &resource.StateChangeConf{ 266 Pending: []string{"creating", "backing-up", "modifying"}, 267 Target: []string{"available"}, 268 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 269 Timeout: 5 * time.Minute, 270 MinTimeout: 3 * time.Second, 271 } 272 273 _, err = stateConf.WaitForState() 274 if err != nil { 275 return fmt.Errorf("[WARN] Error waiting for Redshift Cluster state to be \"available\": %s", err) 276 } 277 278 return resourceAwsRedshiftClusterRead(d, meta) 279 } 280 281 func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) error { 282 conn := meta.(*AWSClient).redshiftconn 283 284 log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id()) 285 resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ 286 ClusterIdentifier: aws.String(d.Id()), 287 }) 288 289 if err != nil { 290 if awsErr, ok := err.(awserr.Error); ok { 291 if "ClusterNotFound" == awsErr.Code() { 292 d.SetId("") 293 log.Printf("[DEBUG] Redshift Cluster (%s) not found", d.Id()) 294 return nil 295 } 296 } 297 log.Printf("[DEBUG] Error describing Redshift Cluster (%s)", d.Id()) 298 return err 299 } 300 301 var rsc *redshift.Cluster 302 for _, c := range resp.Clusters { 303 if *c.ClusterIdentifier == d.Id() { 304 rsc = c 305 } 306 } 307 308 if rsc == nil { 309 log.Printf("[WARN] Redshift Cluster (%s) not found", d.Id()) 310 d.SetId("") 311 return nil 312 } 313 314 d.Set("database_name", rsc.DBName) 315 d.Set("cluster_subnet_group_name", rsc.ClusterSubnetGroupName) 316 d.Set("availability_zone", rsc.AvailabilityZone) 317 d.Set("encrypted", rsc.Encrypted) 318 d.Set("automated_snapshot_retention_period", rsc.AutomatedSnapshotRetentionPeriod) 319 d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow) 320 d.Set("endpoint", aws.String(fmt.Sprintf("%s:%d", *rsc.Endpoint.Address, *rsc.Endpoint.Port))) 321 d.Set("cluster_parameter_group_name", rsc.ClusterParameterGroups[0].ParameterGroupName) 322 if len(rsc.ClusterNodes) > 1 { 323 d.Set("cluster_type", "multi-node") 324 } else { 325 d.Set("cluster_type", "single-node") 326 } 327 328 var vpcg []string 329 for _, g := range rsc.VpcSecurityGroups { 330 vpcg = append(vpcg, *g.VpcSecurityGroupId) 331 } 332 if err := d.Set("vpc_security_group_ids", vpcg); err != nil { 333 return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for Redshift Cluster (%s): %s", d.Id(), err) 334 } 335 336 var csg []string 337 for _, g := range rsc.ClusterSecurityGroups { 338 csg = append(csg, *g.ClusterSecurityGroupName) 339 } 340 if err := d.Set("cluster_security_groups", csg); err != nil { 341 return fmt.Errorf("[DEBUG] Error saving Cluster Security Group Names to state for Redshift Cluster (%s): %s", d.Id(), err) 342 } 343 344 d.Set("cluster_public_key", rsc.ClusterPublicKey) 345 d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) 346 347 return nil 348 } 349 350 func resourceAwsRedshiftClusterUpdate(d *schema.ResourceData, meta interface{}) error { 351 conn := meta.(*AWSClient).redshiftconn 352 353 log.Printf("[INFO] Building Redshift Modify Cluster Options") 354 req := &redshift.ModifyClusterInput{ 355 ClusterIdentifier: aws.String(d.Id()), 356 } 357 358 if d.HasChange("cluster_type") { 359 req.ClusterType = aws.String(d.Get("cluster_type").(string)) 360 } 361 362 if d.HasChange("node_type") { 363 req.NodeType = aws.String(d.Get("node_type").(string)) 364 } 365 366 if d.HasChange("number_of_nodes") { 367 if v := d.Get("number_of_nodes").(int); v > 1 { 368 req.ClusterType = aws.String("multi-node") 369 req.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int))) 370 } else { 371 req.ClusterType = aws.String("single-node") 372 } 373 } 374 375 if d.HasChange("cluster_security_groups") { 376 req.ClusterSecurityGroups = expandStringList(d.Get("cluster_security_groups").(*schema.Set).List()) 377 } 378 379 if d.HasChange("vpc_security_group_ips") { 380 req.VpcSecurityGroupIds = expandStringList(d.Get("vpc_security_group_ips").(*schema.Set).List()) 381 } 382 383 if d.HasChange("master_password") { 384 req.MasterUserPassword = aws.String(d.Get("master_password").(string)) 385 } 386 387 if d.HasChange("cluster_parameter_group_name") { 388 req.ClusterParameterGroupName = aws.String(d.Get("cluster_parameter_group_name").(string)) 389 } 390 391 if d.HasChange("automated_snapshot_retention_period") { 392 req.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(d.Get("automated_snapshot_retention_period").(int))) 393 } 394 395 if d.HasChange("preferred_maintenance_window") { 396 req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) 397 } 398 399 if d.HasChange("cluster_version") { 400 req.ClusterVersion = aws.String(d.Get("cluster_version").(string)) 401 } 402 403 if d.HasChange("allow_version_upgrade") { 404 req.AllowVersionUpgrade = aws.Bool(d.Get("allow_version_upgrade").(bool)) 405 } 406 407 log.Printf("[INFO] Modifying Redshift Cluster: %s", d.Id()) 408 log.Printf("[DEBUG] Redshift Cluster Modify options: %s", req) 409 _, err := conn.ModifyCluster(req) 410 if err != nil { 411 return fmt.Errorf("[WARN] Error modifying Redshift Cluster (%s): %s", d.Id(), err) 412 } 413 414 stateConf := &resource.StateChangeConf{ 415 Pending: []string{"creating", "deleting", "rebooting", "resizing", "renaming"}, 416 Target: []string{"available"}, 417 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 418 Timeout: 10 * time.Minute, 419 MinTimeout: 5 * time.Second, 420 } 421 422 // Wait, catching any errors 423 _, err = stateConf.WaitForState() 424 if err != nil { 425 return fmt.Errorf("[WARN] Error Modifying Redshift Cluster (%s): %s", d.Id(), err) 426 } 427 428 return resourceAwsRedshiftClusterRead(d, meta) 429 } 430 431 func resourceAwsRedshiftClusterDelete(d *schema.ResourceData, meta interface{}) error { 432 conn := meta.(*AWSClient).redshiftconn 433 log.Printf("[DEBUG] Destroying Redshift Cluster (%s)", d.Id()) 434 435 deleteOpts := redshift.DeleteClusterInput{ 436 ClusterIdentifier: aws.String(d.Id()), 437 } 438 439 skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) 440 deleteOpts.SkipFinalClusterSnapshot = aws.Bool(skipFinalSnapshot) 441 442 if !skipFinalSnapshot { 443 if name, present := d.GetOk("final_snapshot_identifier"); present { 444 deleteOpts.FinalClusterSnapshotIdentifier = aws.String(name.(string)) 445 } else { 446 return fmt.Errorf("Redshift Cluster Instance FinalSnapshotIdentifier is required when a final snapshot is required") 447 } 448 } 449 450 log.Printf("[DEBUG] Redshift Cluster delete options: %s", deleteOpts) 451 _, err := conn.DeleteCluster(&deleteOpts) 452 if err != nil { 453 return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err) 454 } 455 456 stateConf := &resource.StateChangeConf{ 457 Pending: []string{"available", "creating", "deleting", "rebooting", "resizing", "renaming"}, 458 Target: []string{"destroyed"}, 459 Refresh: resourceAwsRedshiftClusterStateRefreshFunc(d, meta), 460 Timeout: 40 * time.Minute, 461 MinTimeout: 5 * time.Second, 462 } 463 464 // Wait, catching any errors 465 _, err = stateConf.WaitForState() 466 if err != nil { 467 return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err) 468 } 469 470 log.Printf("[INFO] Redshift Cluster %s successfully deleted", d.Id()) 471 472 return nil 473 } 474 475 func resourceAwsRedshiftClusterStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 476 return func() (interface{}, string, error) { 477 conn := meta.(*AWSClient).redshiftconn 478 479 log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id()) 480 resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ 481 ClusterIdentifier: aws.String(d.Id()), 482 }) 483 484 if err != nil { 485 if awsErr, ok := err.(awserr.Error); ok { 486 if "ClusterNotFound" == awsErr.Code() { 487 return 42, "destroyed", nil 488 } 489 } 490 log.Printf("[WARN] Error on retrieving Redshift Cluster (%s) when waiting: %s", d.Id(), err) 491 return nil, "", err 492 } 493 494 var rsc *redshift.Cluster 495 496 for _, c := range resp.Clusters { 497 if *c.ClusterIdentifier == d.Id() { 498 rsc = c 499 } 500 } 501 502 if rsc == nil { 503 return 42, "destroyed", nil 504 } 505 506 if rsc.ClusterStatus != nil { 507 log.Printf("[DEBUG] Redshift Cluster status (%s): %s", d.Id(), *rsc.ClusterStatus) 508 } 509 510 return rsc, *rsc.ClusterStatus, nil 511 } 512 } 513 514 func validateRedshiftClusterIdentifier(v interface{}, k string) (ws []string, errors []error) { 515 value := v.(string) 516 if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) { 517 errors = append(errors, fmt.Errorf( 518 "only lowercase alphanumeric characters and hyphens allowed in %q", k)) 519 } 520 if !regexp.MustCompile(`^[a-z]`).MatchString(value) { 521 errors = append(errors, fmt.Errorf( 522 "first character of %q must be a letter", k)) 523 } 524 if regexp.MustCompile(`--`).MatchString(value) { 525 errors = append(errors, fmt.Errorf( 526 "%q cannot contain two consecutive hyphens", k)) 527 } 528 if regexp.MustCompile(`-$`).MatchString(value) { 529 errors = append(errors, fmt.Errorf( 530 "%q cannot end with a hyphen", k)) 531 } 532 return 533 } 534 535 func validateRedshiftClusterDbName(v interface{}, k string) (ws []string, errors []error) { 536 value := v.(string) 537 if !regexp.MustCompile(`^[a-z]+$`).MatchString(value) { 538 errors = append(errors, fmt.Errorf( 539 "only lowercase letters characters allowed in %q", k)) 540 } 541 if len(value) > 64 { 542 errors = append(errors, fmt.Errorf( 543 "%q cannot be longer than 64 characters: %q", k, value)) 544 } 545 if value == "" { 546 errors = append(errors, fmt.Errorf( 547 "%q cannot be an empty string", k)) 548 } 549 550 return 551 } 552 553 func validateRedshiftClusterFinalSnapshotIdentifier(v interface{}, k string) (ws []string, errors []error) { 554 value := v.(string) 555 if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { 556 errors = append(errors, fmt.Errorf( 557 "only alphanumeric characters and hyphens allowed in %q", k)) 558 } 559 if regexp.MustCompile(`--`).MatchString(value) { 560 errors = append(errors, fmt.Errorf("%q cannot contain two consecutive hyphens", k)) 561 } 562 if regexp.MustCompile(`-$`).MatchString(value) { 563 errors = append(errors, fmt.Errorf("%q cannot end in a hyphen", k)) 564 } 565 if len(value) > 255 { 566 errors = append(errors, fmt.Errorf("%q cannot be more than 255 characters", k)) 567 } 568 return 569 } 570 571 func validateRedshiftClusterMasterUsername(v interface{}, k string) (ws []string, errors []error) { 572 value := v.(string) 573 if !regexp.MustCompile(`^[A-Za-z0-9]+$`).MatchString(value) { 574 errors = append(errors, fmt.Errorf( 575 "only alphanumeric characters in %q", k)) 576 } 577 if !regexp.MustCompile(`^[A-Za-z]`).MatchString(value) { 578 errors = append(errors, fmt.Errorf( 579 "first character of %q must be a letter", k)) 580 } 581 if len(value) > 128 { 582 errors = append(errors, fmt.Errorf("%q cannot be more than 128 characters", k)) 583 } 584 return 585 }