github.com/sathiyas/terraform@v0.6.9-0.20151210233947-3330da00b997/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  				d.SetId("")
   260  				return nil
   261  			}
   262  		}
   263  		return err
   264  	}
   265  
   266  	stack := resp.Stacks[0]
   267  	d.Set("name", stack.Name)
   268  	d.Set("region", stack.Region)
   269  	d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn)
   270  	d.Set("service_role_arn", stack.ServiceRoleArn)
   271  	d.Set("default_availability_zone", stack.DefaultAvailabilityZone)
   272  	d.Set("default_os", stack.DefaultOs)
   273  	d.Set("default_root_device_type", stack.DefaultRootDeviceType)
   274  	d.Set("default_ssh_key_name", stack.DefaultSshKeyName)
   275  	d.Set("default_subnet_id", stack.DefaultSubnetId)
   276  	d.Set("hostname_theme", stack.HostnameTheme)
   277  	d.Set("use_custom_cookbooks", stack.UseCustomCookbooks)
   278  	d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups)
   279  	d.Set("vpc_id", stack.VpcId)
   280  	if color, ok := stack.Attributes["Color"]; ok {
   281  		d.Set("color", color)
   282  	}
   283  	if stack.ConfigurationManager != nil {
   284  		d.Set("configuration_manager_name", stack.ConfigurationManager.Name)
   285  		d.Set("configuration_manager_version", stack.ConfigurationManager.Version)
   286  	}
   287  	if stack.ChefConfiguration != nil {
   288  		d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion)
   289  		d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf)
   290  	}
   291  	resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource)
   292  
   293  	return nil
   294  }
   295  
   296  func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error {
   297  	client := meta.(*AWSClient).opsworksconn
   298  
   299  	err := resourceAwsOpsworksStackValidate(d)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	req := &opsworks.CreateStackInput{
   305  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   306  		Name:           aws.String(d.Get("name").(string)),
   307  		Region:         aws.String(d.Get("region").(string)),
   308  		ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)),
   309  	}
   310  	inVpc := false
   311  	if vpcId, ok := d.GetOk("vpc_id"); ok {
   312  		req.VpcId = aws.String(vpcId.(string))
   313  		inVpc = true
   314  	}
   315  	if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok {
   316  		req.DefaultSubnetId = aws.String(defaultSubnetId.(string))
   317  	}
   318  	if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok {
   319  		req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string))
   320  	}
   321  
   322  	log.Printf("[DEBUG] Creating OpsWorks stack: %s", *req.Name)
   323  
   324  	var resp *opsworks.CreateStackOutput
   325  	err = resource.Retry(20*time.Minute, func() error {
   326  		var cerr error
   327  		resp, cerr = client.CreateStack(req)
   328  		if cerr != nil {
   329  			if opserr, ok := cerr.(awserr.Error); ok {
   330  				// If Terraform is also managing the service IAM role,
   331  				// it may have just been created and not yet be
   332  				// propagated.
   333  				// AWS doesn't provide a machine-readable code for this
   334  				// specific error, so we're forced to do fragile message
   335  				// matching.
   336  				// The full error we're looking for looks something like
   337  				// the following:
   338  				// Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes
   339  				if opserr.Code() == "ValidationException" && strings.Contains(opserr.Message(), "not yet propagated") {
   340  					log.Printf("[INFO] Waiting for service IAM role to propagate")
   341  					return cerr
   342  				}
   343  			}
   344  			return resource.RetryError{Err: cerr}
   345  		}
   346  		return nil
   347  	})
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	stackId := *resp.StackId
   353  	d.SetId(stackId)
   354  	d.Set("id", stackId)
   355  
   356  	if inVpc {
   357  		// For VPC-based stacks, OpsWorks asynchronously creates some default
   358  		// security groups which must exist before layers can be created.
   359  		// Unfortunately it doesn't tell us what the ids of these are, so
   360  		// we can't actually check for them. Instead, we just wait a nominal
   361  		// amount of time for their creation to complete.
   362  		log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created")
   363  		time.Sleep(30 * time.Second)
   364  	}
   365  
   366  	return resourceAwsOpsworksStackUpdate(d, meta)
   367  }
   368  
   369  func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error {
   370  	client := meta.(*AWSClient).opsworksconn
   371  
   372  	err := resourceAwsOpsworksStackValidate(d)
   373  	if err != nil {
   374  		return err
   375  	}
   376  
   377  	req := &opsworks.UpdateStackInput{
   378  		CustomJson:                aws.String(d.Get("custom_json").(string)),
   379  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   380  		DefaultRootDeviceType:     aws.String(d.Get("default_root_device_type").(string)),
   381  		DefaultSshKeyName:         aws.String(d.Get("default_ssh_key_name").(string)),
   382  		Name:                      aws.String(d.Get("name").(string)),
   383  		ServiceRoleArn:            aws.String(d.Get("service_role_arn").(string)),
   384  		StackId:                   aws.String(d.Id()),
   385  		UseCustomCookbooks:        aws.Bool(d.Get("use_custom_cookbooks").(bool)),
   386  		UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)),
   387  		Attributes:                make(map[string]*string),
   388  		CustomCookbooksSource:     resourceAwsOpsworksStackCustomCookbooksSource(d),
   389  	}
   390  	if v, ok := d.GetOk("default_os"); ok {
   391  		req.DefaultOs = aws.String(v.(string))
   392  	}
   393  	if v, ok := d.GetOk("default_subnet_id"); ok {
   394  		req.DefaultSubnetId = aws.String(v.(string))
   395  	}
   396  	if v, ok := d.GetOk("default_availability_zone"); ok {
   397  		req.DefaultAvailabilityZone = aws.String(v.(string))
   398  	}
   399  	if v, ok := d.GetOk("hostname_theme"); ok {
   400  		req.HostnameTheme = aws.String(v.(string))
   401  	}
   402  	if v, ok := d.GetOk("color"); ok {
   403  		req.Attributes["Color"] = aws.String(v.(string))
   404  	}
   405  	req.ChefConfiguration = &opsworks.ChefConfiguration{
   406  		BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)),
   407  		ManageBerkshelf:  aws.Bool(d.Get("manage_berkshelf").(bool)),
   408  	}
   409  	req.ConfigurationManager = &opsworks.StackConfigurationManager{
   410  		Name:    aws.String(d.Get("configuration_manager_name").(string)),
   411  		Version: aws.String(d.Get("configuration_manager_version").(string)),
   412  	}
   413  
   414  	log.Printf("[DEBUG] Updating OpsWorks stack: %s", d.Id())
   415  
   416  	_, err = client.UpdateStack(req)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	return resourceAwsOpsworksStackRead(d, meta)
   422  }
   423  
   424  func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error {
   425  	client := meta.(*AWSClient).opsworksconn
   426  
   427  	req := &opsworks.DeleteStackInput{
   428  		StackId: aws.String(d.Id()),
   429  	}
   430  
   431  	log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id())
   432  
   433  	_, err := client.DeleteStack(req)
   434  	if err != nil {
   435  		return err
   436  	}
   437  
   438  	// For a stack in a VPC, OpsWorks has created some default security groups
   439  	// in the VPC, which it will now delete.
   440  	// Unfortunately, the security groups are deleted asynchronously and there
   441  	// is no robust way for us to determine when it is done. The VPC itself
   442  	// isn't deletable until the security groups are cleaned up, so this could
   443  	// make 'terraform destroy' fail if the VPC is also managed and we don't
   444  	// wait for the security groups to be deleted.
   445  	// There is no robust way to check for this, so we'll just wait a
   446  	// nominal amount of time.
   447  	if _, ok := d.GetOk("vpc_id"); ok {
   448  		log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted")
   449  		time.Sleep(30 * time.Second)
   450  	}
   451  
   452  	return nil
   453  }