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  }