github.com/erriapo/terraform@v0.6.12-0.20160203182612-0340ea72354f/builtin/providers/aws/resource_aws_opsworks_stack.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/hashicorp/terraform/helper/resource"
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  
    12  	"github.com/aws/aws-sdk-go/aws"
    13  	"github.com/aws/aws-sdk-go/aws/awserr"
    14  	"github.com/aws/aws-sdk-go/service/opsworks"
    15  )
    16  
    17  func resourceAwsOpsworksStack() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsOpsworksStackCreate,
    20  		Read:   resourceAwsOpsworksStackRead,
    21  		Update: resourceAwsOpsworksStackUpdate,
    22  		Delete: resourceAwsOpsworksStackDelete,
    23  
    24  		Schema: map[string]*schema.Schema{
    25  			"id": &schema.Schema{
    26  				Type:     schema.TypeString,
    27  				Computed: true,
    28  			},
    29  
    30  			"name": &schema.Schema{
    31  				Type:     schema.TypeString,
    32  				Required: true,
    33  			},
    34  
    35  			"region": &schema.Schema{
    36  				Type:     schema.TypeString,
    37  				ForceNew: true,
    38  				Required: true,
    39  			},
    40  
    41  			"service_role_arn": &schema.Schema{
    42  				Type:     schema.TypeString,
    43  				Required: true,
    44  			},
    45  
    46  			"default_instance_profile_arn": &schema.Schema{
    47  				Type:     schema.TypeString,
    48  				Required: true,
    49  			},
    50  
    51  			"color": &schema.Schema{
    52  				Type:     schema.TypeString,
    53  				Optional: true,
    54  			},
    55  
    56  			"configuration_manager_name": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Optional: true,
    59  				Default:  "Chef",
    60  			},
    61  
    62  			"configuration_manager_version": &schema.Schema{
    63  				Type:     schema.TypeString,
    64  				Optional: true,
    65  				Default:  "11.4",
    66  			},
    67  
    68  			"manage_berkshelf": &schema.Schema{
    69  				Type:     schema.TypeBool,
    70  				Optional: true,
    71  				Default:  false,
    72  			},
    73  
    74  			"berkshelf_version": &schema.Schema{
    75  				Type:     schema.TypeString,
    76  				Optional: true,
    77  				Default:  "3.2.0",
    78  			},
    79  
    80  			"custom_cookbooks_source": &schema.Schema{
    81  				Type:     schema.TypeList,
    82  				Optional: true,
    83  				Computed: true,
    84  				Elem: &schema.Resource{
    85  					Schema: map[string]*schema.Schema{
    86  						"type": &schema.Schema{
    87  							Type:     schema.TypeString,
    88  							Required: true,
    89  						},
    90  
    91  						"url": &schema.Schema{
    92  							Type:     schema.TypeString,
    93  							Required: true,
    94  						},
    95  
    96  						"username": &schema.Schema{
    97  							Type:     schema.TypeString,
    98  							Optional: true,
    99  						},
   100  
   101  						"password": &schema.Schema{
   102  							Type:     schema.TypeString,
   103  							Optional: true,
   104  						},
   105  
   106  						"revision": &schema.Schema{
   107  							Type:     schema.TypeString,
   108  							Optional: true,
   109  						},
   110  
   111  						"ssh_key": &schema.Schema{
   112  							Type:     schema.TypeString,
   113  							Optional: true,
   114  						},
   115  					},
   116  				},
   117  			},
   118  
   119  			"custom_json": &schema.Schema{
   120  				Type:     schema.TypeString,
   121  				Optional: true,
   122  			},
   123  
   124  			"default_availability_zone": &schema.Schema{
   125  				Type:     schema.TypeString,
   126  				Optional: true,
   127  				Computed: true,
   128  			},
   129  
   130  			"default_os": &schema.Schema{
   131  				Type:     schema.TypeString,
   132  				Optional: true,
   133  				Default:  "Ubuntu 12.04 LTS",
   134  			},
   135  
   136  			"default_root_device_type": &schema.Schema{
   137  				Type:     schema.TypeString,
   138  				Optional: true,
   139  				Default:  "instance-store",
   140  			},
   141  
   142  			"default_ssh_key_name": &schema.Schema{
   143  				Type:     schema.TypeString,
   144  				Optional: true,
   145  			},
   146  
   147  			"default_subnet_id": &schema.Schema{
   148  				Type:     schema.TypeString,
   149  				Optional: true,
   150  			},
   151  
   152  			"hostname_theme": &schema.Schema{
   153  				Type:     schema.TypeString,
   154  				Optional: true,
   155  				Default:  "Layer_Dependent",
   156  			},
   157  
   158  			"use_custom_cookbooks": &schema.Schema{
   159  				Type:     schema.TypeBool,
   160  				Optional: true,
   161  				Default:  false,
   162  			},
   163  
   164  			"use_opsworks_security_groups": &schema.Schema{
   165  				Type:     schema.TypeBool,
   166  				Optional: true,
   167  				Default:  true,
   168  			},
   169  
   170  			"vpc_id": &schema.Schema{
   171  				Type:     schema.TypeString,
   172  				ForceNew: true,
   173  				Optional: true,
   174  			},
   175  		},
   176  	}
   177  }
   178  
   179  func resourceAwsOpsworksStackValidate(d *schema.ResourceData) error {
   180  	cookbooksSourceCount := d.Get("custom_cookbooks_source.#").(int)
   181  	if cookbooksSourceCount > 1 {
   182  		return fmt.Errorf("Only one custom_cookbooks_source is permitted")
   183  	}
   184  
   185  	vpcId := d.Get("vpc_id").(string)
   186  	if vpcId != "" {
   187  		if d.Get("default_subnet_id").(string) == "" {
   188  			return fmt.Errorf("default_subnet_id must be set if vpc_id is set")
   189  		}
   190  	} else {
   191  		if d.Get("default_availability_zone").(string) == "" {
   192  			return fmt.Errorf("either vpc_id or default_availability_zone must be set")
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func resourceAwsOpsworksStackCustomCookbooksSource(d *schema.ResourceData) *opsworks.Source {
   200  	count := d.Get("custom_cookbooks_source.#").(int)
   201  	if count == 0 {
   202  		return nil
   203  	}
   204  
   205  	return &opsworks.Source{
   206  		Type:     aws.String(d.Get("custom_cookbooks_source.0.type").(string)),
   207  		Url:      aws.String(d.Get("custom_cookbooks_source.0.url").(string)),
   208  		Username: aws.String(d.Get("custom_cookbooks_source.0.username").(string)),
   209  		Password: aws.String(d.Get("custom_cookbooks_source.0.password").(string)),
   210  		Revision: aws.String(d.Get("custom_cookbooks_source.0.revision").(string)),
   211  		SshKey:   aws.String(d.Get("custom_cookbooks_source.0.ssh_key").(string)),
   212  	}
   213  }
   214  
   215  func resourceAwsOpsworksSetStackCustomCookbooksSource(d *schema.ResourceData, v *opsworks.Source) {
   216  	nv := make([]interface{}, 0, 1)
   217  	if v != nil {
   218  		m := make(map[string]interface{})
   219  		if v.Type != nil {
   220  			m["type"] = *v.Type
   221  		}
   222  		if v.Url != nil {
   223  			m["url"] = *v.Url
   224  		}
   225  		if v.Username != nil {
   226  			m["username"] = *v.Username
   227  		}
   228  		if v.Password != nil {
   229  			m["password"] = *v.Password
   230  		}
   231  		if v.Revision != nil {
   232  			m["revision"] = *v.Revision
   233  		}
   234  		nv = append(nv, m)
   235  	}
   236  
   237  	err := d.Set("custom_cookbooks_source", nv)
   238  	if err != nil {
   239  		// should never happen
   240  		panic(err)
   241  	}
   242  }
   243  
   244  func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error {
   245  	client := meta.(*AWSClient).opsworksconn
   246  
   247  	req := &opsworks.DescribeStacksInput{
   248  		StackIds: []*string{
   249  			aws.String(d.Id()),
   250  		},
   251  	}
   252  
   253  	log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id())
   254  
   255  	resp, err := client.DescribeStacks(req)
   256  	if err != nil {
   257  		if awserr, ok := err.(awserr.Error); ok {
   258  			if awserr.Code() == "ResourceNotFoundException" {
   259  				log.Printf("[DEBUG] OpsWorks stack (%s) not found", d.Id())
   260  				d.SetId("")
   261  				return nil
   262  			}
   263  		}
   264  		return err
   265  	}
   266  
   267  	stack := resp.Stacks[0]
   268  	d.Set("name", stack.Name)
   269  	d.Set("region", stack.Region)
   270  	d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn)
   271  	d.Set("service_role_arn", stack.ServiceRoleArn)
   272  	d.Set("default_availability_zone", stack.DefaultAvailabilityZone)
   273  	d.Set("default_os", stack.DefaultOs)
   274  	d.Set("default_root_device_type", stack.DefaultRootDeviceType)
   275  	d.Set("default_ssh_key_name", stack.DefaultSshKeyName)
   276  	d.Set("default_subnet_id", stack.DefaultSubnetId)
   277  	d.Set("hostname_theme", stack.HostnameTheme)
   278  	d.Set("use_custom_cookbooks", stack.UseCustomCookbooks)
   279  	d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups)
   280  	d.Set("vpc_id", stack.VpcId)
   281  	if color, ok := stack.Attributes["Color"]; ok {
   282  		d.Set("color", color)
   283  	}
   284  	if stack.ConfigurationManager != nil {
   285  		d.Set("configuration_manager_name", stack.ConfigurationManager.Name)
   286  		d.Set("configuration_manager_version", stack.ConfigurationManager.Version)
   287  	}
   288  	if stack.ChefConfiguration != nil {
   289  		d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion)
   290  		d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf)
   291  	}
   292  	resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource)
   293  
   294  	return nil
   295  }
   296  
   297  func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error {
   298  	client := meta.(*AWSClient).opsworksconn
   299  
   300  	err := resourceAwsOpsworksStackValidate(d)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	req := &opsworks.CreateStackInput{
   306  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   307  		Name:                      aws.String(d.Get("name").(string)),
   308  		Region:                    aws.String(d.Get("region").(string)),
   309  		ServiceRoleArn:            aws.String(d.Get("service_role_arn").(string)),
   310  		UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)),
   311  	}
   312  	inVpc := false
   313  	if vpcId, ok := d.GetOk("vpc_id"); ok {
   314  		req.VpcId = aws.String(vpcId.(string))
   315  		inVpc = true
   316  	}
   317  	if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok {
   318  		req.DefaultSubnetId = aws.String(defaultSubnetId.(string))
   319  	}
   320  	if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok {
   321  		req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string))
   322  	}
   323  
   324  	log.Printf("[DEBUG] Creating OpsWorks stack: %s", req)
   325  
   326  	var resp *opsworks.CreateStackOutput
   327  	err = resource.Retry(20*time.Minute, func() error {
   328  		var cerr error
   329  		resp, cerr = client.CreateStack(req)
   330  		if cerr != nil {
   331  			if opserr, ok := cerr.(awserr.Error); ok {
   332  				// If Terraform is also managing the service IAM role,
   333  				// it may have just been created and not yet be
   334  				// propagated.
   335  				// AWS doesn't provide a machine-readable code for this
   336  				// specific error, so we're forced to do fragile message
   337  				// matching.
   338  				// The full error we're looking for looks something like
   339  				// the following:
   340  				// Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes
   341  				propErr := "not yet propagated"
   342  				trustErr := "not the necessary trust relationship"
   343  				if opserr.Code() == "ValidationException" && (strings.Contains(opserr.Message(), trustErr) || strings.Contains(opserr.Message(), propErr)) {
   344  					log.Printf("[INFO] Waiting for service IAM role to propagate")
   345  					return cerr
   346  				}
   347  			}
   348  			return resource.RetryError{Err: cerr}
   349  		}
   350  		return nil
   351  	})
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	stackId := *resp.StackId
   357  	d.SetId(stackId)
   358  	d.Set("id", stackId)
   359  
   360  	if inVpc && *req.UseOpsworksSecurityGroups {
   361  		// For VPC-based stacks, OpsWorks asynchronously creates some default
   362  		// security groups which must exist before layers can be created.
   363  		// Unfortunately it doesn't tell us what the ids of these are, so
   364  		// we can't actually check for them. Instead, we just wait a nominal
   365  		// amount of time for their creation to complete.
   366  		log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created")
   367  		time.Sleep(30 * time.Second)
   368  	}
   369  
   370  	return resourceAwsOpsworksStackUpdate(d, meta)
   371  }
   372  
   373  func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error {
   374  	client := meta.(*AWSClient).opsworksconn
   375  
   376  	err := resourceAwsOpsworksStackValidate(d)
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	req := &opsworks.UpdateStackInput{
   382  		CustomJson:                aws.String(d.Get("custom_json").(string)),
   383  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   384  		DefaultRootDeviceType:     aws.String(d.Get("default_root_device_type").(string)),
   385  		DefaultSshKeyName:         aws.String(d.Get("default_ssh_key_name").(string)),
   386  		Name:                      aws.String(d.Get("name").(string)),
   387  		ServiceRoleArn:            aws.String(d.Get("service_role_arn").(string)),
   388  		StackId:                   aws.String(d.Id()),
   389  		UseCustomCookbooks:        aws.Bool(d.Get("use_custom_cookbooks").(bool)),
   390  		UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)),
   391  		Attributes:                make(map[string]*string),
   392  		CustomCookbooksSource:     resourceAwsOpsworksStackCustomCookbooksSource(d),
   393  	}
   394  	if v, ok := d.GetOk("default_os"); ok {
   395  		req.DefaultOs = aws.String(v.(string))
   396  	}
   397  	if v, ok := d.GetOk("default_subnet_id"); ok {
   398  		req.DefaultSubnetId = aws.String(v.(string))
   399  	}
   400  	if v, ok := d.GetOk("default_availability_zone"); ok {
   401  		req.DefaultAvailabilityZone = aws.String(v.(string))
   402  	}
   403  	if v, ok := d.GetOk("hostname_theme"); ok {
   404  		req.HostnameTheme = aws.String(v.(string))
   405  	}
   406  	if v, ok := d.GetOk("color"); ok {
   407  		req.Attributes["Color"] = aws.String(v.(string))
   408  	}
   409  	req.ChefConfiguration = &opsworks.ChefConfiguration{
   410  		BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)),
   411  		ManageBerkshelf:  aws.Bool(d.Get("manage_berkshelf").(bool)),
   412  	}
   413  	req.ConfigurationManager = &opsworks.StackConfigurationManager{
   414  		Name:    aws.String(d.Get("configuration_manager_name").(string)),
   415  		Version: aws.String(d.Get("configuration_manager_version").(string)),
   416  	}
   417  
   418  	log.Printf("[DEBUG] Updating OpsWorks stack: %s", req)
   419  
   420  	_, err = client.UpdateStack(req)
   421  	if err != nil {
   422  		return err
   423  	}
   424  
   425  	return resourceAwsOpsworksStackRead(d, meta)
   426  }
   427  
   428  func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error {
   429  	client := meta.(*AWSClient).opsworksconn
   430  
   431  	req := &opsworks.DeleteStackInput{
   432  		StackId: aws.String(d.Id()),
   433  	}
   434  
   435  	log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id())
   436  
   437  	_, err := client.DeleteStack(req)
   438  	if err != nil {
   439  		return err
   440  	}
   441  
   442  	// For a stack in a VPC, OpsWorks has created some default security groups
   443  	// in the VPC, which it will now delete.
   444  	// Unfortunately, the security groups are deleted asynchronously and there
   445  	// is no robust way for us to determine when it is done. The VPC itself
   446  	// isn't deletable until the security groups are cleaned up, so this could
   447  	// make 'terraform destroy' fail if the VPC is also managed and we don't
   448  	// wait for the security groups to be deleted.
   449  	// There is no robust way to check for this, so we'll just wait a
   450  	// nominal amount of time.
   451  	_, inVpc := d.GetOk("vpc_id")
   452  	_, useOpsworksDefaultSg := d.GetOk("use_opsworks_security_group")
   453  
   454  	if inVpc && useOpsworksDefaultSg {
   455  		log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted")
   456  		time.Sleep(30 * time.Second)
   457  	}
   458  
   459  	return nil
   460  }