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