github.com/danrjohnson/terraform@v0.7.0-rc2.0.20160627135212-d0fc1fa086ff/builtin/providers/aws/resource_aws_rds_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/rds" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 ) 16 17 func resourceAwsRDSCluster() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsRDSClusterCreate, 20 Read: resourceAwsRDSClusterRead, 21 Update: resourceAwsRDSClusterUpdate, 22 Delete: resourceAwsRDSClusterDelete, 23 24 Schema: map[string]*schema.Schema{ 25 26 "availability_zones": &schema.Schema{ 27 Type: schema.TypeSet, 28 Elem: &schema.Schema{Type: schema.TypeString}, 29 Optional: true, 30 ForceNew: true, 31 Computed: true, 32 Set: schema.HashString, 33 }, 34 35 "cluster_identifier": &schema.Schema{ 36 Type: schema.TypeString, 37 Required: true, 38 ForceNew: true, 39 ValidateFunc: validateRdsId, 40 }, 41 42 "cluster_members": &schema.Schema{ 43 Type: schema.TypeSet, 44 Elem: &schema.Schema{Type: schema.TypeString}, 45 Optional: true, 46 Computed: true, 47 Set: schema.HashString, 48 }, 49 50 "database_name": &schema.Schema{ 51 Type: schema.TypeString, 52 Optional: true, 53 Computed: true, 54 ForceNew: true, 55 }, 56 57 "db_subnet_group_name": &schema.Schema{ 58 Type: schema.TypeString, 59 Optional: true, 60 ForceNew: true, 61 Computed: true, 62 }, 63 64 // TODO: remove parameter_group_name 65 // See https://github.com/hashicorp/terraform/issues/7046 66 // Likely need migration to remove from state 67 "parameter_group_name": &schema.Schema{ 68 Type: schema.TypeString, 69 Optional: true, 70 Computed: true, 71 Deprecated: "Use db_cluster_parameter_group_name instead. This attribute will be removed in a future version", 72 }, 73 74 "db_cluster_parameter_group_name": &schema.Schema{ 75 Type: schema.TypeString, 76 Optional: true, 77 Computed: true, 78 }, 79 80 "endpoint": &schema.Schema{ 81 Type: schema.TypeString, 82 Computed: true, 83 }, 84 85 "engine": &schema.Schema{ 86 Type: schema.TypeString, 87 Computed: true, 88 }, 89 90 "storage_encrypted": &schema.Schema{ 91 Type: schema.TypeBool, 92 Optional: true, 93 Default: false, 94 ForceNew: true, 95 }, 96 97 "final_snapshot_identifier": &schema.Schema{ 98 Type: schema.TypeString, 99 Optional: true, 100 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 101 value := v.(string) 102 if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { 103 es = append(es, fmt.Errorf( 104 "only alphanumeric characters and hyphens allowed in %q", k)) 105 } 106 if regexp.MustCompile(`--`).MatchString(value) { 107 es = append(es, fmt.Errorf("%q cannot contain two consecutive hyphens", k)) 108 } 109 if regexp.MustCompile(`-$`).MatchString(value) { 110 es = append(es, fmt.Errorf("%q cannot end in a hyphen", k)) 111 } 112 return 113 }, 114 }, 115 116 "skip_final_snapshot": &schema.Schema{ 117 Type: schema.TypeBool, 118 Optional: true, 119 Default: true, 120 }, 121 122 "master_username": &schema.Schema{ 123 Type: schema.TypeString, 124 Required: true, 125 ForceNew: true, 126 }, 127 128 "master_password": &schema.Schema{ 129 Type: schema.TypeString, 130 Required: true, 131 }, 132 133 "port": &schema.Schema{ 134 Type: schema.TypeInt, 135 Optional: true, 136 Computed: true, 137 }, 138 139 // apply_immediately is used to determine when the update modifications 140 // take place. 141 // See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html 142 "apply_immediately": &schema.Schema{ 143 Type: schema.TypeBool, 144 Optional: true, 145 Computed: true, 146 }, 147 148 "vpc_security_group_ids": &schema.Schema{ 149 Type: schema.TypeSet, 150 Optional: true, 151 Computed: true, 152 Elem: &schema.Schema{Type: schema.TypeString}, 153 Set: schema.HashString, 154 }, 155 156 "preferred_backup_window": &schema.Schema{ 157 Type: schema.TypeString, 158 Optional: true, 159 Computed: true, 160 }, 161 162 "preferred_maintenance_window": &schema.Schema{ 163 Type: schema.TypeString, 164 Optional: true, 165 Computed: true, 166 StateFunc: func(val interface{}) string { 167 if val == nil { 168 return "" 169 } 170 return strings.ToLower(val.(string)) 171 }, 172 }, 173 174 "backup_retention_period": &schema.Schema{ 175 Type: schema.TypeInt, 176 Optional: true, 177 Default: 1, 178 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 179 value := v.(int) 180 if value > 35 { 181 es = append(es, fmt.Errorf( 182 "backup retention period cannot be more than 35 days")) 183 } 184 return 185 }, 186 }, 187 }, 188 } 189 } 190 191 func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error { 192 conn := meta.(*AWSClient).rdsconn 193 194 createOpts := &rds.CreateDBClusterInput{ 195 DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)), 196 Engine: aws.String("aurora"), 197 MasterUserPassword: aws.String(d.Get("master_password").(string)), 198 MasterUsername: aws.String(d.Get("master_username").(string)), 199 StorageEncrypted: aws.Bool(d.Get("storage_encrypted").(bool)), 200 } 201 202 if v := d.Get("database_name"); v.(string) != "" { 203 createOpts.DatabaseName = aws.String(v.(string)) 204 } 205 206 if attr, ok := d.GetOk("port"); ok { 207 createOpts.Port = aws.Int64(int64(attr.(int))) 208 } 209 210 if attr, ok := d.GetOk("db_subnet_group_name"); ok { 211 createOpts.DBSubnetGroupName = aws.String(attr.(string)) 212 } 213 214 if attr, ok := d.GetOk("parameter_group_name"); ok { 215 createOpts.DBClusterParameterGroupName = aws.String(attr.(string)) 216 } 217 218 if attr, ok := d.GetOk("db_cluster_parameter_group_name"); ok { 219 createOpts.DBClusterParameterGroupName = aws.String(attr.(string)) 220 } 221 222 if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { 223 createOpts.VpcSecurityGroupIds = expandStringList(attr.List()) 224 } 225 226 if attr := d.Get("availability_zones").(*schema.Set); attr.Len() > 0 { 227 createOpts.AvailabilityZones = expandStringList(attr.List()) 228 } 229 230 if v, ok := d.GetOk("backup_retention_period"); ok { 231 createOpts.BackupRetentionPeriod = aws.Int64(int64(v.(int))) 232 } 233 234 if v, ok := d.GetOk("preferred_backup_window"); ok { 235 createOpts.PreferredBackupWindow = aws.String(v.(string)) 236 } 237 238 if v, ok := d.GetOk("preferred_maintenance_window"); ok { 239 createOpts.PreferredMaintenanceWindow = aws.String(v.(string)) 240 } 241 242 log.Printf("[DEBUG] RDS Cluster create options: %s", createOpts) 243 resp, err := conn.CreateDBCluster(createOpts) 244 if err != nil { 245 log.Printf("[ERROR] Error creating RDS Cluster: %s", err) 246 return err 247 } 248 249 log.Printf("[DEBUG]: Cluster create response: %s", resp) 250 d.SetId(*resp.DBCluster.DBClusterIdentifier) 251 stateConf := &resource.StateChangeConf{ 252 Pending: []string{"creating", "backing-up", "modifying"}, 253 Target: []string{"available"}, 254 Refresh: resourceAwsRDSClusterStateRefreshFunc(d, meta), 255 Timeout: 5 * time.Minute, 256 MinTimeout: 3 * time.Second, 257 } 258 259 // Wait, catching any errors 260 _, err = stateConf.WaitForState() 261 if err != nil { 262 return fmt.Errorf("[WARN] Error waiting for RDS Cluster state to be \"available\": %s", err) 263 } 264 265 return resourceAwsRDSClusterRead(d, meta) 266 } 267 268 func resourceAwsRDSClusterRead(d *schema.ResourceData, meta interface{}) error { 269 conn := meta.(*AWSClient).rdsconn 270 271 resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{ 272 DBClusterIdentifier: aws.String(d.Id()), 273 }) 274 275 if err != nil { 276 if awsErr, ok := err.(awserr.Error); ok { 277 if "DBClusterNotFoundFault" == awsErr.Code() { 278 d.SetId("") 279 log.Printf("[DEBUG] RDS Cluster (%s) not found", d.Id()) 280 return nil 281 } 282 } 283 log.Printf("[DEBUG] Error describing RDS Cluster (%s)", d.Id()) 284 return err 285 } 286 287 var dbc *rds.DBCluster 288 for _, c := range resp.DBClusters { 289 if *c.DBClusterIdentifier == d.Id() { 290 dbc = c 291 } 292 } 293 294 if dbc == nil { 295 log.Printf("[WARN] RDS Cluster (%s) not found", d.Id()) 296 d.SetId("") 297 return nil 298 } 299 300 if err := d.Set("availability_zones", aws.StringValueSlice(dbc.AvailabilityZones)); err != nil { 301 return fmt.Errorf("[DEBUG] Error saving AvailabilityZones to state for RDS Cluster (%s): %s", d.Id(), err) 302 } 303 304 // Only set the DatabaseName if it is not nil. There is a known API bug where 305 // RDS accepts a DatabaseName but does not return it, causing a perpetual 306 // diff. 307 // See https://github.com/hashicorp/terraform/issues/4671 for backstory 308 if dbc.DatabaseName != nil { 309 d.Set("database_name", dbc.DatabaseName) 310 } 311 312 d.Set("db_subnet_group_name", dbc.DBSubnetGroup) 313 d.Set("parameter_group_name", dbc.DBClusterParameterGroup) 314 d.Set("db_cluster_parameter_group_name", dbc.DBClusterParameterGroup) 315 d.Set("endpoint", dbc.Endpoint) 316 d.Set("engine", dbc.Engine) 317 d.Set("master_username", dbc.MasterUsername) 318 d.Set("port", dbc.Port) 319 d.Set("storage_encrypted", dbc.StorageEncrypted) 320 d.Set("backup_retention_period", dbc.BackupRetentionPeriod) 321 d.Set("preferred_backup_window", dbc.PreferredBackupWindow) 322 d.Set("preferred_maintenance_window", dbc.PreferredMaintenanceWindow) 323 324 var vpcg []string 325 for _, g := range dbc.VpcSecurityGroups { 326 vpcg = append(vpcg, *g.VpcSecurityGroupId) 327 } 328 if err := d.Set("vpc_security_group_ids", vpcg); err != nil { 329 return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for RDS Cluster (%s): %s", d.Id(), err) 330 } 331 332 var cm []string 333 for _, m := range dbc.DBClusterMembers { 334 cm = append(cm, *m.DBInstanceIdentifier) 335 } 336 if err := d.Set("cluster_members", cm); err != nil { 337 return fmt.Errorf("[DEBUG] Error saving RDS Cluster Members to state for RDS Cluster (%s): %s", d.Id(), err) 338 } 339 340 return nil 341 } 342 343 func resourceAwsRDSClusterUpdate(d *schema.ResourceData, meta interface{}) error { 344 conn := meta.(*AWSClient).rdsconn 345 346 req := &rds.ModifyDBClusterInput{ 347 ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), 348 DBClusterIdentifier: aws.String(d.Id()), 349 } 350 351 if d.HasChange("master_password") { 352 req.MasterUserPassword = aws.String(d.Get("master_password").(string)) 353 } 354 355 if d.HasChange("vpc_security_group_ids") { 356 if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { 357 req.VpcSecurityGroupIds = expandStringList(attr.List()) 358 } else { 359 req.VpcSecurityGroupIds = []*string{} 360 } 361 } 362 363 if d.HasChange("preferred_backup_window") { 364 req.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string)) 365 } 366 367 if d.HasChange("preferred_maintenance_window") { 368 req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) 369 } 370 371 if d.HasChange("backup_retention_period") { 372 req.BackupRetentionPeriod = aws.Int64(int64(d.Get("backup_retention_period").(int))) 373 } 374 375 if d.HasChange("parameter_group_name") { 376 d.SetPartial("parameter_group_name") 377 req.DBClusterParameterGroupName = aws.String(d.Get("parameter_group_name").(string)) 378 } 379 380 if d.HasChange("db_cluster_parameter_group_name") { 381 d.SetPartial("db_cluster_parameter_group_name") 382 req.DBClusterParameterGroupName = aws.String(d.Get("db_cluster_parameter_group_name").(string)) 383 } 384 385 _, err := conn.ModifyDBCluster(req) 386 if err != nil { 387 return fmt.Errorf("[WARN] Error modifying RDS Cluster (%s): %s", d.Id(), err) 388 } 389 390 return resourceAwsRDSClusterRead(d, meta) 391 } 392 393 func resourceAwsRDSClusterDelete(d *schema.ResourceData, meta interface{}) error { 394 conn := meta.(*AWSClient).rdsconn 395 log.Printf("[DEBUG] Destroying RDS Cluster (%s)", d.Id()) 396 397 deleteOpts := rds.DeleteDBClusterInput{ 398 DBClusterIdentifier: aws.String(d.Id()), 399 } 400 401 skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) 402 deleteOpts.SkipFinalSnapshot = aws.Bool(skipFinalSnapshot) 403 404 if skipFinalSnapshot == false { 405 if name, present := d.GetOk("final_snapshot_identifier"); present { 406 deleteOpts.FinalDBSnapshotIdentifier = aws.String(name.(string)) 407 } else { 408 return fmt.Errorf("RDS Cluster FinalSnapshotIdentifier is required when a final snapshot is required") 409 } 410 } 411 412 log.Printf("[DEBUG] RDS Cluster delete options: %s", deleteOpts) 413 _, err := conn.DeleteDBCluster(&deleteOpts) 414 415 stateConf := &resource.StateChangeConf{ 416 Pending: []string{"available", "deleting", "backing-up", "modifying"}, 417 Target: []string{"destroyed"}, 418 Refresh: resourceAwsRDSClusterStateRefreshFunc(d, meta), 419 Timeout: 5 * time.Minute, 420 MinTimeout: 3 * time.Second, 421 } 422 423 // Wait, catching any errors 424 _, err = stateConf.WaitForState() 425 if err != nil { 426 return fmt.Errorf("[WARN] Error deleting RDS Cluster (%s): %s", d.Id(), err) 427 } 428 429 return nil 430 } 431 432 func resourceAwsRDSClusterStateRefreshFunc( 433 d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 434 return func() (interface{}, string, error) { 435 conn := meta.(*AWSClient).rdsconn 436 437 resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{ 438 DBClusterIdentifier: aws.String(d.Id()), 439 }) 440 441 if err != nil { 442 if awsErr, ok := err.(awserr.Error); ok { 443 if "DBClusterNotFoundFault" == awsErr.Code() { 444 return 42, "destroyed", nil 445 } 446 } 447 log.Printf("[WARN] Error on retrieving DB Cluster (%s) when waiting: %s", d.Id(), err) 448 return nil, "", err 449 } 450 451 var dbc *rds.DBCluster 452 453 for _, c := range resp.DBClusters { 454 if *c.DBClusterIdentifier == d.Id() { 455 dbc = c 456 } 457 } 458 459 if dbc == nil { 460 return 42, "destroyed", nil 461 } 462 463 if dbc.Status != nil { 464 log.Printf("[DEBUG] DB Cluster status (%s): %s", d.Id(), *dbc.Status) 465 } 466 467 return dbc, *dbc.Status, nil 468 } 469 }