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