github.com/anfernee/terraform@v0.6.16-0.20160430000239-06e5085a92f2/builtin/providers/aws/resource_aws_redshift_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/redshift"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  )
    16  
    17  func resourceAwsRedshiftCluster() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsRedshiftClusterCreate,
    20  		Read:   resourceAwsRedshiftClusterRead,
    21  		Update: resourceAwsRedshiftClusterUpdate,
    22  		Delete: resourceAwsRedshiftClusterDelete,
    23  
    24  		Schema: map[string]*schema.Schema{
    25  			"database_name": &schema.Schema{
    26  				Type:         schema.TypeString,
    27  				Optional:     true,
    28  				Computed:     true,
    29  				ValidateFunc: validateRedshiftClusterDbName,
    30  			},
    31  
    32  			"cluster_identifier": &schema.Schema{
    33  				Type:         schema.TypeString,
    34  				Required:     true,
    35  				ForceNew:     true,
    36  				ValidateFunc: validateRedshiftClusterIdentifier,
    37  			},
    38  			"cluster_type": &schema.Schema{
    39  				Type:     schema.TypeString,
    40  				Optional: true,
    41  				Computed: true,
    42  			},
    43  
    44  			"node_type": &schema.Schema{
    45  				Type:     schema.TypeString,
    46  				Required: true,
    47  			},
    48  
    49  			"master_username": &schema.Schema{
    50  				Type:         schema.TypeString,
    51  				Required:     true,
    52  				ValidateFunc: validateRedshiftClusterMasterUsername,
    53  			},
    54  
    55  			"master_password": &schema.Schema{
    56  				Type:     schema.TypeString,
    57  				Required: true,
    58  			},
    59  
    60  			"cluster_security_groups": &schema.Schema{
    61  				Type:     schema.TypeSet,
    62  				Optional: true,
    63  				Computed: true,
    64  				Elem:     &schema.Schema{Type: schema.TypeString},
    65  				Set:      schema.HashString,
    66  			},
    67  
    68  			"vpc_security_group_ids": &schema.Schema{
    69  				Type:     schema.TypeSet,
    70  				Optional: true,
    71  				Computed: true,
    72  				Elem:     &schema.Schema{Type: schema.TypeString},
    73  				Set:      schema.HashString,
    74  			},
    75  
    76  			"cluster_subnet_group_name": &schema.Schema{
    77  				Type:     schema.TypeString,
    78  				Optional: true,
    79  				ForceNew: true,
    80  				Computed: true,
    81  			},
    82  
    83  			"availability_zone": &schema.Schema{
    84  				Type:     schema.TypeString,
    85  				Optional: true,
    86  				Computed: true,
    87  			},
    88  
    89  			"preferred_maintenance_window": &schema.Schema{
    90  				Type:     schema.TypeString,
    91  				Optional: true,
    92  				Computed: true,
    93  				StateFunc: func(val interface{}) string {
    94  					if val == nil {
    95  						return ""
    96  					}
    97  					return strings.ToLower(val.(string))
    98  				},
    99  			},
   100  
   101  			"cluster_parameter_group_name": &schema.Schema{
   102  				Type:     schema.TypeString,
   103  				Optional: true,
   104  				Computed: true,
   105  			},
   106  
   107  			"automated_snapshot_retention_period": &schema.Schema{
   108  				Type:     schema.TypeInt,
   109  				Optional: true,
   110  				Default:  1,
   111  				ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
   112  					value := v.(int)
   113  					if value > 35 {
   114  						es = append(es, fmt.Errorf(
   115  							"backup retention period cannot be more than 35 days"))
   116  					}
   117  					return
   118  				},
   119  			},
   120  
   121  			"port": &schema.Schema{
   122  				Type:     schema.TypeInt,
   123  				Optional: true,
   124  				Default:  5439,
   125  			},
   126  
   127  			"cluster_version": &schema.Schema{
   128  				Type:     schema.TypeString,
   129  				Optional: true,
   130  				Default:  "1.0",
   131  			},
   132  
   133  			"allow_version_upgrade": &schema.Schema{
   134  				Type:     schema.TypeBool,
   135  				Optional: true,
   136  				Default:  true,
   137  			},
   138  
   139  			"number_of_nodes": &schema.Schema{
   140  				Type:     schema.TypeInt,
   141  				Optional: true,
   142  				Default:  1,
   143  			},
   144  
   145  			"publicly_accessible": &schema.Schema{
   146  				Type:     schema.TypeBool,
   147  				Optional: true,
   148  				Default:  true,
   149  			},
   150  
   151  			"encrypted": &schema.Schema{
   152  				Type:     schema.TypeBool,
   153  				Optional: true,
   154  				Computed: true,
   155  			},
   156  
   157  			"elastic_ip": &schema.Schema{
   158  				Type:     schema.TypeString,
   159  				Optional: true,
   160  			},
   161  
   162  			"final_snapshot_identifier": &schema.Schema{
   163  				Type:         schema.TypeString,
   164  				Optional:     true,
   165  				ValidateFunc: validateRedshiftClusterFinalSnapshotIdentifier,
   166  			},
   167  
   168  			"skip_final_snapshot": &schema.Schema{
   169  				Type:     schema.TypeBool,
   170  				Optional: true,
   171  				Default:  true,
   172  			},
   173  
   174  			"endpoint": &schema.Schema{
   175  				Type:     schema.TypeString,
   176  				Optional: true,
   177  				Computed: true,
   178  			},
   179  
   180  			"cluster_public_key": &schema.Schema{
   181  				Type:     schema.TypeString,
   182  				Optional: true,
   183  				Computed: true,
   184  			},
   185  
   186  			"cluster_revision_number": &schema.Schema{
   187  				Type:     schema.TypeString,
   188  				Optional: true,
   189  				Computed: true,
   190  			},
   191  		},
   192  	}
   193  }
   194  
   195  func resourceAwsRedshiftClusterCreate(d *schema.ResourceData, meta interface{}) error {
   196  	conn := meta.(*AWSClient).redshiftconn
   197  
   198  	log.Printf("[INFO] Building Redshift Cluster Options")
   199  	createOpts := &redshift.CreateClusterInput{
   200  		ClusterIdentifier:   aws.String(d.Get("cluster_identifier").(string)),
   201  		Port:                aws.Int64(int64(d.Get("port").(int))),
   202  		MasterUserPassword:  aws.String(d.Get("master_password").(string)),
   203  		MasterUsername:      aws.String(d.Get("master_username").(string)),
   204  		ClusterVersion:      aws.String(d.Get("cluster_version").(string)),
   205  		NodeType:            aws.String(d.Get("node_type").(string)),
   206  		DBName:              aws.String(d.Get("database_name").(string)),
   207  		AllowVersionUpgrade: aws.Bool(d.Get("allow_version_upgrade").(bool)),
   208  		PubliclyAccessible:  aws.Bool(d.Get("publicly_accessible").(bool)),
   209  	}
   210  
   211  	if v := d.Get("number_of_nodes").(int); v > 1 {
   212  		createOpts.ClusterType = aws.String("multi-node")
   213  		createOpts.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int)))
   214  	} else {
   215  		createOpts.ClusterType = aws.String("single-node")
   216  	}
   217  
   218  	if v := d.Get("cluster_security_groups").(*schema.Set); v.Len() > 0 {
   219  		createOpts.ClusterSecurityGroups = expandStringList(v.List())
   220  	}
   221  
   222  	if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
   223  		createOpts.VpcSecurityGroupIds = expandStringList(v.List())
   224  	}
   225  
   226  	if v, ok := d.GetOk("cluster_subnet_group_name"); ok {
   227  		createOpts.ClusterSubnetGroupName = aws.String(v.(string))
   228  	}
   229  
   230  	if v, ok := d.GetOk("availability_zone"); ok {
   231  		createOpts.AvailabilityZone = aws.String(v.(string))
   232  	}
   233  
   234  	if v, ok := d.GetOk("preferred_maintenance_window"); ok {
   235  		createOpts.PreferredMaintenanceWindow = aws.String(v.(string))
   236  	}
   237  
   238  	if v, ok := d.GetOk("cluster_parameter_group_name"); ok {
   239  		createOpts.ClusterParameterGroupName = aws.String(v.(string))
   240  	}
   241  
   242  	if v, ok := d.GetOk("automated_snapshot_retention_period"); ok {
   243  		createOpts.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(v.(int)))
   244  	}
   245  
   246  	if v, ok := d.GetOk("encrypted"); ok {
   247  		createOpts.Encrypted = aws.Bool(v.(bool))
   248  	}
   249  
   250  	if v, ok := d.GetOk("elastic_ip"); ok {
   251  		createOpts.ElasticIp = aws.String(v.(string))
   252  	}
   253  
   254  	log.Printf("[DEBUG] Redshift Cluster create options: %s", createOpts)
   255  	resp, err := conn.CreateCluster(createOpts)
   256  	if err != nil {
   257  		log.Printf("[ERROR] Error creating Redshift Cluster: %s", err)
   258  		return err
   259  	}
   260  
   261  	log.Printf("[DEBUG]: Cluster create response: %s", resp)
   262  	d.SetId(*resp.Cluster.ClusterIdentifier)
   263  
   264  	stateConf := &resource.StateChangeConf{
   265  		Pending:    []string{"creating", "backing-up", "modifying"},
   266  		Target:     []string{"available"},
   267  		Refresh:    resourceAwsRedshiftClusterStateRefreshFunc(d, meta),
   268  		Timeout:    40 * time.Minute,
   269  		MinTimeout: 10 * time.Second,
   270  	}
   271  
   272  	_, err = stateConf.WaitForState()
   273  	if err != nil {
   274  		return fmt.Errorf("[WARN] Error waiting for Redshift Cluster state to be \"available\": %s", err)
   275  	}
   276  
   277  	return resourceAwsRedshiftClusterRead(d, meta)
   278  }
   279  
   280  func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) error {
   281  	conn := meta.(*AWSClient).redshiftconn
   282  
   283  	log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id())
   284  	resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{
   285  		ClusterIdentifier: aws.String(d.Id()),
   286  	})
   287  
   288  	if err != nil {
   289  		if awsErr, ok := err.(awserr.Error); ok {
   290  			if "ClusterNotFound" == awsErr.Code() {
   291  				d.SetId("")
   292  				log.Printf("[DEBUG] Redshift Cluster (%s) not found", d.Id())
   293  				return nil
   294  			}
   295  		}
   296  		log.Printf("[DEBUG] Error describing Redshift Cluster (%s)", d.Id())
   297  		return err
   298  	}
   299  
   300  	var rsc *redshift.Cluster
   301  	for _, c := range resp.Clusters {
   302  		if *c.ClusterIdentifier == d.Id() {
   303  			rsc = c
   304  		}
   305  	}
   306  
   307  	if rsc == nil {
   308  		log.Printf("[WARN] Redshift Cluster (%s) not found", d.Id())
   309  		d.SetId("")
   310  		return nil
   311  	}
   312  
   313  	d.Set("database_name", rsc.DBName)
   314  	d.Set("cluster_subnet_group_name", rsc.ClusterSubnetGroupName)
   315  	d.Set("availability_zone", rsc.AvailabilityZone)
   316  	d.Set("encrypted", rsc.Encrypted)
   317  	d.Set("automated_snapshot_retention_period", rsc.AutomatedSnapshotRetentionPeriod)
   318  	d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow)
   319  	if rsc.Endpoint != nil && rsc.Endpoint.Address != nil {
   320  		endpoint := *rsc.Endpoint.Address
   321  		if rsc.Endpoint.Port != nil {
   322  			endpoint = fmt.Sprintf("%s:%d", endpoint, *rsc.Endpoint.Port)
   323  		}
   324  		d.Set("endpoint", endpoint)
   325  	}
   326  	d.Set("cluster_parameter_group_name", rsc.ClusterParameterGroups[0].ParameterGroupName)
   327  	if len(rsc.ClusterNodes) > 1 {
   328  		d.Set("cluster_type", "multi-node")
   329  	} else {
   330  		d.Set("cluster_type", "single-node")
   331  	}
   332  
   333  	var vpcg []string
   334  	for _, g := range rsc.VpcSecurityGroups {
   335  		vpcg = append(vpcg, *g.VpcSecurityGroupId)
   336  	}
   337  	if err := d.Set("vpc_security_group_ids", vpcg); err != nil {
   338  		return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for Redshift Cluster (%s): %s", d.Id(), err)
   339  	}
   340  
   341  	var csg []string
   342  	for _, g := range rsc.ClusterSecurityGroups {
   343  		csg = append(csg, *g.ClusterSecurityGroupName)
   344  	}
   345  	if err := d.Set("cluster_security_groups", csg); err != nil {
   346  		return fmt.Errorf("[DEBUG] Error saving Cluster Security Group Names to state for Redshift Cluster (%s): %s", d.Id(), err)
   347  	}
   348  
   349  	d.Set("cluster_public_key", rsc.ClusterPublicKey)
   350  	d.Set("cluster_revision_number", rsc.ClusterRevisionNumber)
   351  
   352  	return nil
   353  }
   354  
   355  func resourceAwsRedshiftClusterUpdate(d *schema.ResourceData, meta interface{}) error {
   356  	conn := meta.(*AWSClient).redshiftconn
   357  
   358  	log.Printf("[INFO] Building Redshift Modify Cluster Options")
   359  	req := &redshift.ModifyClusterInput{
   360  		ClusterIdentifier: aws.String(d.Id()),
   361  	}
   362  
   363  	if d.HasChange("cluster_type") {
   364  		req.ClusterType = aws.String(d.Get("cluster_type").(string))
   365  	}
   366  
   367  	if d.HasChange("node_type") {
   368  		req.NodeType = aws.String(d.Get("node_type").(string))
   369  	}
   370  
   371  	if d.HasChange("number_of_nodes") {
   372  		if v := d.Get("number_of_nodes").(int); v > 1 {
   373  			req.ClusterType = aws.String("multi-node")
   374  			req.NumberOfNodes = aws.Int64(int64(d.Get("number_of_nodes").(int)))
   375  		} else {
   376  			req.ClusterType = aws.String("single-node")
   377  		}
   378  		req.NodeType = aws.String(d.Get("node_type").(string))
   379  	}
   380  
   381  	if d.HasChange("cluster_security_groups") {
   382  		req.ClusterSecurityGroups = expandStringList(d.Get("cluster_security_groups").(*schema.Set).List())
   383  	}
   384  
   385  	if d.HasChange("vpc_security_group_ips") {
   386  		req.VpcSecurityGroupIds = expandStringList(d.Get("vpc_security_group_ips").(*schema.Set).List())
   387  	}
   388  
   389  	if d.HasChange("master_password") {
   390  		req.MasterUserPassword = aws.String(d.Get("master_password").(string))
   391  	}
   392  
   393  	if d.HasChange("cluster_parameter_group_name") {
   394  		req.ClusterParameterGroupName = aws.String(d.Get("cluster_parameter_group_name").(string))
   395  	}
   396  
   397  	if d.HasChange("automated_snapshot_retention_period") {
   398  		req.AutomatedSnapshotRetentionPeriod = aws.Int64(int64(d.Get("automated_snapshot_retention_period").(int)))
   399  	}
   400  
   401  	if d.HasChange("preferred_maintenance_window") {
   402  		req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string))
   403  	}
   404  
   405  	if d.HasChange("cluster_version") {
   406  		req.ClusterVersion = aws.String(d.Get("cluster_version").(string))
   407  	}
   408  
   409  	if d.HasChange("allow_version_upgrade") {
   410  		req.AllowVersionUpgrade = aws.Bool(d.Get("allow_version_upgrade").(bool))
   411  	}
   412  
   413  	if d.HasChange("publicly_accessible") {
   414  		req.PubliclyAccessible = aws.Bool(d.Get("publicly_accessible").(bool))
   415  	}
   416  
   417  	log.Printf("[INFO] Modifying Redshift Cluster: %s", d.Id())
   418  	log.Printf("[DEBUG] Redshift Cluster Modify options: %s", req)
   419  	_, err := conn.ModifyCluster(req)
   420  	if err != nil {
   421  		return fmt.Errorf("[WARN] Error modifying Redshift Cluster (%s): %s", d.Id(), err)
   422  	}
   423  
   424  	stateConf := &resource.StateChangeConf{
   425  		Pending:    []string{"creating", "deleting", "rebooting", "resizing", "renaming", "modifying"},
   426  		Target:     []string{"available"},
   427  		Refresh:    resourceAwsRedshiftClusterStateRefreshFunc(d, meta),
   428  		Timeout:    40 * time.Minute,
   429  		MinTimeout: 10 * time.Second,
   430  	}
   431  
   432  	// Wait, catching any errors
   433  	_, err = stateConf.WaitForState()
   434  	if err != nil {
   435  		return fmt.Errorf("[WARN] Error Modifying Redshift Cluster (%s): %s", d.Id(), err)
   436  	}
   437  
   438  	return resourceAwsRedshiftClusterRead(d, meta)
   439  }
   440  
   441  func resourceAwsRedshiftClusterDelete(d *schema.ResourceData, meta interface{}) error {
   442  	conn := meta.(*AWSClient).redshiftconn
   443  	log.Printf("[DEBUG] Destroying Redshift Cluster (%s)", d.Id())
   444  
   445  	deleteOpts := redshift.DeleteClusterInput{
   446  		ClusterIdentifier: aws.String(d.Id()),
   447  	}
   448  
   449  	skipFinalSnapshot := d.Get("skip_final_snapshot").(bool)
   450  	deleteOpts.SkipFinalClusterSnapshot = aws.Bool(skipFinalSnapshot)
   451  
   452  	if !skipFinalSnapshot {
   453  		if name, present := d.GetOk("final_snapshot_identifier"); present {
   454  			deleteOpts.FinalClusterSnapshotIdentifier = aws.String(name.(string))
   455  		} else {
   456  			return fmt.Errorf("Redshift Cluster Instance FinalSnapshotIdentifier is required when a final snapshot is required")
   457  		}
   458  	}
   459  
   460  	log.Printf("[DEBUG] Redshift Cluster delete options: %s", deleteOpts)
   461  	_, err := conn.DeleteCluster(&deleteOpts)
   462  	if err != nil {
   463  		return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err)
   464  	}
   465  
   466  	stateConf := &resource.StateChangeConf{
   467  		Pending:    []string{"available", "creating", "deleting", "rebooting", "resizing", "renaming"},
   468  		Target:     []string{"destroyed"},
   469  		Refresh:    resourceAwsRedshiftClusterStateRefreshFunc(d, meta),
   470  		Timeout:    40 * time.Minute,
   471  		MinTimeout: 5 * time.Second,
   472  	}
   473  
   474  	// Wait, catching any errors
   475  	_, err = stateConf.WaitForState()
   476  	if err != nil {
   477  		return fmt.Errorf("[ERROR] Error deleting Redshift Cluster (%s): %s", d.Id(), err)
   478  	}
   479  
   480  	log.Printf("[INFO] Redshift Cluster %s successfully deleted", d.Id())
   481  
   482  	return nil
   483  }
   484  
   485  func resourceAwsRedshiftClusterStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc {
   486  	return func() (interface{}, string, error) {
   487  		conn := meta.(*AWSClient).redshiftconn
   488  
   489  		log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id())
   490  		resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{
   491  			ClusterIdentifier: aws.String(d.Id()),
   492  		})
   493  
   494  		if err != nil {
   495  			if awsErr, ok := err.(awserr.Error); ok {
   496  				if "ClusterNotFound" == awsErr.Code() {
   497  					return 42, "destroyed", nil
   498  				}
   499  			}
   500  			log.Printf("[WARN] Error on retrieving Redshift Cluster (%s) when waiting: %s", d.Id(), err)
   501  			return nil, "", err
   502  		}
   503  
   504  		var rsc *redshift.Cluster
   505  
   506  		for _, c := range resp.Clusters {
   507  			if *c.ClusterIdentifier == d.Id() {
   508  				rsc = c
   509  			}
   510  		}
   511  
   512  		if rsc == nil {
   513  			return 42, "destroyed", nil
   514  		}
   515  
   516  		if rsc.ClusterStatus != nil {
   517  			log.Printf("[DEBUG] Redshift Cluster status (%s): %s", d.Id(), *rsc.ClusterStatus)
   518  		}
   519  
   520  		return rsc, *rsc.ClusterStatus, nil
   521  	}
   522  }
   523  
   524  func validateRedshiftClusterIdentifier(v interface{}, k string) (ws []string, errors []error) {
   525  	value := v.(string)
   526  	if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) {
   527  		errors = append(errors, fmt.Errorf(
   528  			"only lowercase alphanumeric characters and hyphens allowed in %q", k))
   529  	}
   530  	if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
   531  		errors = append(errors, fmt.Errorf(
   532  			"first character of %q must be a letter", k))
   533  	}
   534  	if regexp.MustCompile(`--`).MatchString(value) {
   535  		errors = append(errors, fmt.Errorf(
   536  			"%q cannot contain two consecutive hyphens", k))
   537  	}
   538  	if regexp.MustCompile(`-$`).MatchString(value) {
   539  		errors = append(errors, fmt.Errorf(
   540  			"%q cannot end with a hyphen", k))
   541  	}
   542  	return
   543  }
   544  
   545  func validateRedshiftClusterDbName(v interface{}, k string) (ws []string, errors []error) {
   546  	value := v.(string)
   547  	if !regexp.MustCompile(`^[a-z]+$`).MatchString(value) {
   548  		errors = append(errors, fmt.Errorf(
   549  			"only lowercase letters characters allowed in %q", k))
   550  	}
   551  	if len(value) > 64 {
   552  		errors = append(errors, fmt.Errorf(
   553  			"%q cannot be longer than 64 characters: %q", k, value))
   554  	}
   555  	if value == "" {
   556  		errors = append(errors, fmt.Errorf(
   557  			"%q cannot be an empty string", k))
   558  	}
   559  
   560  	return
   561  }
   562  
   563  func validateRedshiftClusterFinalSnapshotIdentifier(v interface{}, k string) (ws []string, errors []error) {
   564  	value := v.(string)
   565  	if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
   566  		errors = append(errors, fmt.Errorf(
   567  			"only alphanumeric characters and hyphens allowed in %q", k))
   568  	}
   569  	if regexp.MustCompile(`--`).MatchString(value) {
   570  		errors = append(errors, fmt.Errorf("%q cannot contain two consecutive hyphens", k))
   571  	}
   572  	if regexp.MustCompile(`-$`).MatchString(value) {
   573  		errors = append(errors, fmt.Errorf("%q cannot end in a hyphen", k))
   574  	}
   575  	if len(value) > 255 {
   576  		errors = append(errors, fmt.Errorf("%q cannot be more than 255 characters", k))
   577  	}
   578  	return
   579  }
   580  
   581  func validateRedshiftClusterMasterUsername(v interface{}, k string) (ws []string, errors []error) {
   582  	value := v.(string)
   583  	if !regexp.MustCompile(`^\w+$`).MatchString(value) {
   584  		errors = append(errors, fmt.Errorf(
   585  			"only alphanumeric characters in %q", k))
   586  	}
   587  	if !regexp.MustCompile(`^[A-Za-z]`).MatchString(value) {
   588  		errors = append(errors, fmt.Errorf(
   589  			"first character of %q must be a letter", k))
   590  	}
   591  	if len(value) > 128 {
   592  		errors = append(errors, fmt.Errorf("%q cannot be more than 128 characters", k))
   593  	}
   594  	return
   595  }