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