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