github.com/gabrielperezs/terraform@v0.7.0-rc2.0.20160715084931-f7da2612946f/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  				Computed: true,
   125  				Optional: true,
   126  				ForceNew: true,
   127  			},
   128  
   129  			"master_password": &schema.Schema{
   130  				Type:     schema.TypeString,
   131  				Optional: true,
   132  			},
   133  
   134  			"snapshot_identifier": &schema.Schema{
   135  				Type:     schema.TypeString,
   136  				Computed: false,
   137  				Optional: true,
   138  				Elem:     &schema.Schema{Type: schema.TypeString},
   139  			},
   140  
   141  			"port": &schema.Schema{
   142  				Type:     schema.TypeInt,
   143  				Optional: true,
   144  				Computed: true,
   145  			},
   146  
   147  			// apply_immediately is used to determine when the update modifications
   148  			// take place.
   149  			// See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html
   150  			"apply_immediately": &schema.Schema{
   151  				Type:     schema.TypeBool,
   152  				Optional: true,
   153  				Computed: true,
   154  			},
   155  
   156  			"vpc_security_group_ids": &schema.Schema{
   157  				Type:     schema.TypeSet,
   158  				Optional: true,
   159  				Computed: true,
   160  				Elem:     &schema.Schema{Type: schema.TypeString},
   161  				Set:      schema.HashString,
   162  			},
   163  
   164  			"preferred_backup_window": &schema.Schema{
   165  				Type:     schema.TypeString,
   166  				Optional: true,
   167  				Computed: true,
   168  			},
   169  
   170  			"preferred_maintenance_window": &schema.Schema{
   171  				Type:     schema.TypeString,
   172  				Optional: true,
   173  				Computed: true,
   174  				StateFunc: func(val interface{}) string {
   175  					if val == nil {
   176  						return ""
   177  					}
   178  					return strings.ToLower(val.(string))
   179  				},
   180  			},
   181  
   182  			"backup_retention_period": &schema.Schema{
   183  				Type:     schema.TypeInt,
   184  				Optional: true,
   185  				Default:  1,
   186  				ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
   187  					value := v.(int)
   188  					if value > 35 {
   189  						es = append(es, fmt.Errorf(
   190  							"backup retention period cannot be more than 35 days"))
   191  					}
   192  					return
   193  				},
   194  			},
   195  		},
   196  	}
   197  }
   198  
   199  func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error {
   200  	conn := meta.(*AWSClient).rdsconn
   201  
   202  	if _, ok := d.GetOk("snapshot_identifier"); ok {
   203  		opts := rds.RestoreDBClusterFromSnapshotInput{
   204  			DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)),
   205  			SnapshotIdentifier:  aws.String(d.Get("snapshot_identifier").(string)),
   206  			Engine:              aws.String("aurora"),
   207  		}
   208  
   209  		if attr := d.Get("availability_zones").(*schema.Set); attr.Len() > 0 {
   210  			opts.AvailabilityZones = expandStringList(attr.List())
   211  		}
   212  
   213  		if attr, ok := d.GetOk("db_subnet_group_name"); ok {
   214  			opts.DBSubnetGroupName = aws.String(attr.(string))
   215  		}
   216  
   217  		if attr, ok := d.GetOk("database_name"); ok {
   218  			opts.DatabaseName = aws.String(attr.(string))
   219  		}
   220  
   221  		if attr, ok := d.GetOk("option_group_name"); ok {
   222  			opts.OptionGroupName = aws.String(attr.(string))
   223  		}
   224  
   225  		if attr, ok := d.GetOk("port"); ok {
   226  			opts.Port = aws.Int64(int64(attr.(int)))
   227  		}
   228  
   229  		var sgUpdate bool
   230  		if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
   231  			sgUpdate = true
   232  			opts.VpcSecurityGroupIds = expandStringList(attr.List())
   233  		}
   234  
   235  		log.Printf("[DEBUG] RDS Cluster restore from snapshot configuration: %s", opts)
   236  		_, err := conn.RestoreDBClusterFromSnapshot(&opts)
   237  		if err != nil {
   238  			return fmt.Errorf("Error creating RDS Cluster: %s", err)
   239  		}
   240  
   241  		if sgUpdate {
   242  			log.Printf("[INFO] RDS Cluster is restoring from snapshot with default security, but custom security should be set, will now update after snapshot is restored!")
   243  
   244  			d.SetId(d.Get("cluster_identifier").(string))
   245  
   246  			log.Printf("[INFO] RDS Cluster Instance ID: %s", d.Id())
   247  
   248  			log.Println("[INFO] Waiting for RDS Cluster to be available")
   249  
   250  			stateConf := &resource.StateChangeConf{
   251  				Pending:    []string{"creating", "backing-up", "modifying"},
   252  				Target:     []string{"available"},
   253  				Refresh:    resourceAwsRDSClusterStateRefreshFunc(d, meta),
   254  				Timeout:    5 * time.Minute,
   255  				MinTimeout: 3 * time.Second,
   256  				Delay:      30 * time.Second, // Wait 30 secs before starting
   257  			}
   258  
   259  			// Wait, catching any errors
   260  			_, err := stateConf.WaitForState()
   261  			if err != nil {
   262  				return err
   263  			}
   264  
   265  			err = resourceAwsRDSClusterInstanceUpdate(d, meta)
   266  			if err != nil {
   267  				return err
   268  			}
   269  		}
   270  	} else {
   271  		if _, ok := d.GetOk("master_password"); !ok {
   272  			return fmt.Errorf(`provider.aws: aws_rds_cluster: %s: "master_password": required field is not set`, d.Get("name").(string))
   273  		}
   274  
   275  		if _, ok := d.GetOk("master_username"); !ok {
   276  			return fmt.Errorf(`provider.aws: aws_rds_cluster: %s: "master_username": required field is not set`, d.Get("name").(string))
   277  		}
   278  
   279  		createOpts := &rds.CreateDBClusterInput{
   280  			DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)),
   281  			Engine:              aws.String("aurora"),
   282  			MasterUserPassword:  aws.String(d.Get("master_password").(string)),
   283  			MasterUsername:      aws.String(d.Get("master_username").(string)),
   284  			StorageEncrypted:    aws.Bool(d.Get("storage_encrypted").(bool)),
   285  		}
   286  
   287  		if v := d.Get("database_name"); v.(string) != "" {
   288  			createOpts.DatabaseName = aws.String(v.(string))
   289  		}
   290  
   291  		if attr, ok := d.GetOk("port"); ok {
   292  			createOpts.Port = aws.Int64(int64(attr.(int)))
   293  		}
   294  
   295  		if attr, ok := d.GetOk("db_subnet_group_name"); ok {
   296  			createOpts.DBSubnetGroupName = aws.String(attr.(string))
   297  		}
   298  
   299  		if attr, ok := d.GetOk("parameter_group_name"); ok {
   300  			createOpts.DBClusterParameterGroupName = aws.String(attr.(string))
   301  		}
   302  
   303  		if attr, ok := d.GetOk("db_cluster_parameter_group_name"); ok {
   304  			createOpts.DBClusterParameterGroupName = aws.String(attr.(string))
   305  		}
   306  
   307  		if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
   308  			createOpts.VpcSecurityGroupIds = expandStringList(attr.List())
   309  		}
   310  
   311  		if attr := d.Get("availability_zones").(*schema.Set); attr.Len() > 0 {
   312  			createOpts.AvailabilityZones = expandStringList(attr.List())
   313  		}
   314  
   315  		if v, ok := d.GetOk("backup_retention_period"); ok {
   316  			createOpts.BackupRetentionPeriod = aws.Int64(int64(v.(int)))
   317  		}
   318  
   319  		if v, ok := d.GetOk("preferred_backup_window"); ok {
   320  			createOpts.PreferredBackupWindow = aws.String(v.(string))
   321  		}
   322  
   323  		if v, ok := d.GetOk("preferred_maintenance_window"); ok {
   324  			createOpts.PreferredMaintenanceWindow = aws.String(v.(string))
   325  		}
   326  
   327  		log.Printf("[DEBUG] RDS Cluster create options: %s", createOpts)
   328  		resp, err := conn.CreateDBCluster(createOpts)
   329  		if err != nil {
   330  			log.Printf("[ERROR] Error creating RDS Cluster: %s", err)
   331  			return err
   332  		}
   333  
   334  		log.Printf("[DEBUG]: RDS Cluster create response: %s", resp)
   335  	}
   336  
   337  	d.SetId(d.Get("cluster_identifier").(string))
   338  
   339  	log.Printf("[INFO] RDS Cluster ID: %s", d.Id())
   340  
   341  	log.Println(
   342  		"[INFO] Waiting for RDS Cluster to be available")
   343  
   344  	stateConf := &resource.StateChangeConf{
   345  		Pending:    []string{"creating", "backing-up", "modifying"},
   346  		Target:     []string{"available"},
   347  		Refresh:    resourceAwsRDSClusterStateRefreshFunc(d, meta),
   348  		Timeout:    5 * time.Minute,
   349  		MinTimeout: 3 * time.Second,
   350  	}
   351  
   352  	// Wait, catching any errors
   353  	_, err := stateConf.WaitForState()
   354  	if err != nil {
   355  		return fmt.Errorf("[WARN] Error waiting for RDS Cluster state to be \"available\": %s", err)
   356  	}
   357  
   358  	return resourceAwsRDSClusterRead(d, meta)
   359  }
   360  
   361  func resourceAwsRDSClusterRead(d *schema.ResourceData, meta interface{}) error {
   362  	conn := meta.(*AWSClient).rdsconn
   363  
   364  	resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{
   365  		DBClusterIdentifier: aws.String(d.Id()),
   366  	})
   367  
   368  	if err != nil {
   369  		if awsErr, ok := err.(awserr.Error); ok {
   370  			if "DBClusterNotFoundFault" == awsErr.Code() {
   371  				d.SetId("")
   372  				log.Printf("[DEBUG] RDS Cluster (%s) not found", d.Id())
   373  				return nil
   374  			}
   375  		}
   376  		log.Printf("[DEBUG] Error describing RDS Cluster (%s)", d.Id())
   377  		return err
   378  	}
   379  
   380  	var dbc *rds.DBCluster
   381  	for _, c := range resp.DBClusters {
   382  		if *c.DBClusterIdentifier == d.Id() {
   383  			dbc = c
   384  		}
   385  	}
   386  
   387  	if dbc == nil {
   388  		log.Printf("[WARN] RDS Cluster (%s) not found", d.Id())
   389  		d.SetId("")
   390  		return nil
   391  	}
   392  
   393  	if err := d.Set("availability_zones", aws.StringValueSlice(dbc.AvailabilityZones)); err != nil {
   394  		return fmt.Errorf("[DEBUG] Error saving AvailabilityZones to state for RDS Cluster (%s): %s", d.Id(), err)
   395  	}
   396  
   397  	// Only set the DatabaseName if it is not nil. There is a known API bug where
   398  	// RDS accepts a DatabaseName but does not return it, causing a perpetual
   399  	// diff.
   400  	//	See https://github.com/hashicorp/terraform/issues/4671 for backstory
   401  	if dbc.DatabaseName != nil {
   402  		d.Set("database_name", dbc.DatabaseName)
   403  	}
   404  
   405  	d.Set("db_subnet_group_name", dbc.DBSubnetGroup)
   406  	d.Set("parameter_group_name", dbc.DBClusterParameterGroup)
   407  	d.Set("db_cluster_parameter_group_name", dbc.DBClusterParameterGroup)
   408  	d.Set("endpoint", dbc.Endpoint)
   409  	d.Set("engine", dbc.Engine)
   410  	d.Set("master_username", dbc.MasterUsername)
   411  	d.Set("port", dbc.Port)
   412  	d.Set("storage_encrypted", dbc.StorageEncrypted)
   413  	d.Set("backup_retention_period", dbc.BackupRetentionPeriod)
   414  	d.Set("preferred_backup_window", dbc.PreferredBackupWindow)
   415  	d.Set("preferred_maintenance_window", dbc.PreferredMaintenanceWindow)
   416  
   417  	var vpcg []string
   418  	for _, g := range dbc.VpcSecurityGroups {
   419  		vpcg = append(vpcg, *g.VpcSecurityGroupId)
   420  	}
   421  	if err := d.Set("vpc_security_group_ids", vpcg); err != nil {
   422  		return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for RDS Cluster (%s): %s", d.Id(), err)
   423  	}
   424  
   425  	var cm []string
   426  	for _, m := range dbc.DBClusterMembers {
   427  		cm = append(cm, *m.DBInstanceIdentifier)
   428  	}
   429  	if err := d.Set("cluster_members", cm); err != nil {
   430  		return fmt.Errorf("[DEBUG] Error saving RDS Cluster Members to state for RDS Cluster (%s): %s", d.Id(), err)
   431  	}
   432  
   433  	return nil
   434  }
   435  
   436  func resourceAwsRDSClusterUpdate(d *schema.ResourceData, meta interface{}) error {
   437  	conn := meta.(*AWSClient).rdsconn
   438  
   439  	req := &rds.ModifyDBClusterInput{
   440  		ApplyImmediately:    aws.Bool(d.Get("apply_immediately").(bool)),
   441  		DBClusterIdentifier: aws.String(d.Id()),
   442  	}
   443  
   444  	if d.HasChange("master_password") {
   445  		req.MasterUserPassword = aws.String(d.Get("master_password").(string))
   446  	}
   447  
   448  	if d.HasChange("vpc_security_group_ids") {
   449  		if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
   450  			req.VpcSecurityGroupIds = expandStringList(attr.List())
   451  		} else {
   452  			req.VpcSecurityGroupIds = []*string{}
   453  		}
   454  	}
   455  
   456  	if d.HasChange("preferred_backup_window") {
   457  		req.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string))
   458  	}
   459  
   460  	if d.HasChange("preferred_maintenance_window") {
   461  		req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string))
   462  	}
   463  
   464  	if d.HasChange("backup_retention_period") {
   465  		req.BackupRetentionPeriod = aws.Int64(int64(d.Get("backup_retention_period").(int)))
   466  	}
   467  
   468  	if d.HasChange("parameter_group_name") {
   469  		d.SetPartial("parameter_group_name")
   470  		req.DBClusterParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
   471  	}
   472  
   473  	if d.HasChange("db_cluster_parameter_group_name") {
   474  		d.SetPartial("db_cluster_parameter_group_name")
   475  		req.DBClusterParameterGroupName = aws.String(d.Get("db_cluster_parameter_group_name").(string))
   476  	}
   477  
   478  	_, err := conn.ModifyDBCluster(req)
   479  	if err != nil {
   480  		return fmt.Errorf("[WARN] Error modifying RDS Cluster (%s): %s", d.Id(), err)
   481  	}
   482  
   483  	return resourceAwsRDSClusterRead(d, meta)
   484  }
   485  
   486  func resourceAwsRDSClusterDelete(d *schema.ResourceData, meta interface{}) error {
   487  	conn := meta.(*AWSClient).rdsconn
   488  	log.Printf("[DEBUG] Destroying RDS Cluster (%s)", d.Id())
   489  
   490  	deleteOpts := rds.DeleteDBClusterInput{
   491  		DBClusterIdentifier: aws.String(d.Id()),
   492  	}
   493  
   494  	skipFinalSnapshot := d.Get("skip_final_snapshot").(bool)
   495  	deleteOpts.SkipFinalSnapshot = aws.Bool(skipFinalSnapshot)
   496  
   497  	if skipFinalSnapshot == false {
   498  		if name, present := d.GetOk("final_snapshot_identifier"); present {
   499  			deleteOpts.FinalDBSnapshotIdentifier = aws.String(name.(string))
   500  		} else {
   501  			return fmt.Errorf("RDS Cluster FinalSnapshotIdentifier is required when a final snapshot is required")
   502  		}
   503  	}
   504  
   505  	log.Printf("[DEBUG] RDS Cluster delete options: %s", deleteOpts)
   506  	_, err := conn.DeleteDBCluster(&deleteOpts)
   507  
   508  	stateConf := &resource.StateChangeConf{
   509  		Pending:    []string{"available", "deleting", "backing-up", "modifying"},
   510  		Target:     []string{"destroyed"},
   511  		Refresh:    resourceAwsRDSClusterStateRefreshFunc(d, meta),
   512  		Timeout:    5 * time.Minute,
   513  		MinTimeout: 3 * time.Second,
   514  	}
   515  
   516  	// Wait, catching any errors
   517  	_, err = stateConf.WaitForState()
   518  	if err != nil {
   519  		return fmt.Errorf("[WARN] Error deleting RDS Cluster (%s): %s", d.Id(), err)
   520  	}
   521  
   522  	return nil
   523  }
   524  
   525  func resourceAwsRDSClusterStateRefreshFunc(
   526  	d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc {
   527  	return func() (interface{}, string, error) {
   528  		conn := meta.(*AWSClient).rdsconn
   529  
   530  		resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{
   531  			DBClusterIdentifier: aws.String(d.Id()),
   532  		})
   533  
   534  		if err != nil {
   535  			if awsErr, ok := err.(awserr.Error); ok {
   536  				if "DBClusterNotFoundFault" == awsErr.Code() {
   537  					return 42, "destroyed", nil
   538  				}
   539  			}
   540  			log.Printf("[WARN] Error on retrieving DB Cluster (%s) when waiting: %s", d.Id(), err)
   541  			return nil, "", err
   542  		}
   543  
   544  		var dbc *rds.DBCluster
   545  
   546  		for _, c := range resp.DBClusters {
   547  			if *c.DBClusterIdentifier == d.Id() {
   548  				dbc = c
   549  			}
   550  		}
   551  
   552  		if dbc == nil {
   553  			return 42, "destroyed", nil
   554  		}
   555  
   556  		if dbc.Status != nil {
   557  			log.Printf("[DEBUG] DB Cluster status (%s): %s", d.Id(), *dbc.Status)
   558  		}
   559  
   560  		return dbc, *dbc.Status, nil
   561  	}
   562  }