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