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