github.com/dougneal/terraform@v0.6.15-0.20170330092735-b6a3840768a4/builtin/providers/aws/resource_aws_opsworks_stack.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/hashicorp/errwrap"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/awserr"
    16  	"github.com/aws/aws-sdk-go/aws/session"
    17  	"github.com/aws/aws-sdk-go/service/opsworks"
    18  )
    19  
    20  func resourceAwsOpsworksStack() *schema.Resource {
    21  	return &schema.Resource{
    22  		Create: resourceAwsOpsworksStackCreate,
    23  		Read:   resourceAwsOpsworksStackRead,
    24  		Update: resourceAwsOpsworksStackUpdate,
    25  		Delete: resourceAwsOpsworksStackDelete,
    26  		Importer: &schema.ResourceImporter{
    27  			State: schema.ImportStatePassthrough,
    28  		},
    29  
    30  		Schema: map[string]*schema.Schema{
    31  			"agent_version": {
    32  				Type:     schema.TypeString,
    33  				Optional: true,
    34  				Computed: true,
    35  			},
    36  
    37  			"id": {
    38  				Type:     schema.TypeString,
    39  				Computed: true,
    40  			},
    41  
    42  			"name": {
    43  				Type:     schema.TypeString,
    44  				Required: true,
    45  			},
    46  
    47  			"region": {
    48  				Type:     schema.TypeString,
    49  				ForceNew: true,
    50  				Required: true,
    51  			},
    52  
    53  			"service_role_arn": {
    54  				Type:     schema.TypeString,
    55  				Required: true,
    56  			},
    57  
    58  			"default_instance_profile_arn": {
    59  				Type:     schema.TypeString,
    60  				Required: true,
    61  			},
    62  
    63  			"color": {
    64  				Type:     schema.TypeString,
    65  				Optional: true,
    66  			},
    67  
    68  			"configuration_manager_name": {
    69  				Type:     schema.TypeString,
    70  				Optional: true,
    71  				Default:  "Chef",
    72  			},
    73  
    74  			"configuration_manager_version": {
    75  				Type:     schema.TypeString,
    76  				Optional: true,
    77  				Default:  "11.10",
    78  			},
    79  
    80  			"manage_berkshelf": {
    81  				Type:     schema.TypeBool,
    82  				Optional: true,
    83  				Default:  false,
    84  			},
    85  
    86  			"berkshelf_version": {
    87  				Type:     schema.TypeString,
    88  				Optional: true,
    89  				Default:  "3.2.0",
    90  			},
    91  
    92  			"custom_cookbooks_source": {
    93  				Type:     schema.TypeList,
    94  				Optional: true,
    95  				Computed: true,
    96  				Elem: &schema.Resource{
    97  					Schema: map[string]*schema.Schema{
    98  						"type": {
    99  							Type:     schema.TypeString,
   100  							Required: true,
   101  						},
   102  
   103  						"url": {
   104  							Type:     schema.TypeString,
   105  							Required: true,
   106  						},
   107  
   108  						"username": {
   109  							Type:     schema.TypeString,
   110  							Optional: true,
   111  						},
   112  
   113  						"password": {
   114  							Type:      schema.TypeString,
   115  							Optional:  true,
   116  							Sensitive: true,
   117  						},
   118  
   119  						"revision": {
   120  							Type:     schema.TypeString,
   121  							Optional: true,
   122  						},
   123  
   124  						"ssh_key": {
   125  							Type:     schema.TypeString,
   126  							Optional: true,
   127  						},
   128  					},
   129  				},
   130  			},
   131  
   132  			"custom_json": {
   133  				Type:     schema.TypeString,
   134  				Optional: true,
   135  			},
   136  
   137  			"default_availability_zone": {
   138  				Type:     schema.TypeString,
   139  				Optional: true,
   140  				Computed: true,
   141  			},
   142  
   143  			"default_os": {
   144  				Type:     schema.TypeString,
   145  				Optional: true,
   146  				Default:  "Ubuntu 12.04 LTS",
   147  			},
   148  
   149  			"default_root_device_type": {
   150  				Type:     schema.TypeString,
   151  				Optional: true,
   152  				Default:  "instance-store",
   153  			},
   154  
   155  			"default_ssh_key_name": {
   156  				Type:     schema.TypeString,
   157  				Optional: true,
   158  			},
   159  
   160  			"default_subnet_id": {
   161  				Type:     schema.TypeString,
   162  				Optional: true,
   163  				Computed: true,
   164  			},
   165  
   166  			"hostname_theme": {
   167  				Type:     schema.TypeString,
   168  				Optional: true,
   169  				Default:  "Layer_Dependent",
   170  			},
   171  
   172  			"use_custom_cookbooks": {
   173  				Type:     schema.TypeBool,
   174  				Optional: true,
   175  				Default:  false,
   176  			},
   177  
   178  			"use_opsworks_security_groups": {
   179  				Type:     schema.TypeBool,
   180  				Optional: true,
   181  				Default:  true,
   182  			},
   183  
   184  			"vpc_id": {
   185  				Type:     schema.TypeString,
   186  				ForceNew: true,
   187  				Computed: true,
   188  				Optional: true,
   189  			},
   190  
   191  			"stack_endpoint": {
   192  				Type:     schema.TypeString,
   193  				Computed: true,
   194  			},
   195  		},
   196  	}
   197  }
   198  
   199  func resourceAwsOpsworksStackValidate(d *schema.ResourceData) error {
   200  	cookbooksSourceCount := d.Get("custom_cookbooks_source.#").(int)
   201  	if cookbooksSourceCount > 1 {
   202  		return fmt.Errorf("Only one custom_cookbooks_source is permitted")
   203  	}
   204  
   205  	vpcId := d.Get("vpc_id").(string)
   206  	if vpcId != "" {
   207  		if d.Get("default_subnet_id").(string) == "" {
   208  			return fmt.Errorf("default_subnet_id must be set if vpc_id is set")
   209  		}
   210  	} else {
   211  		if d.Get("default_availability_zone").(string) == "" {
   212  			return fmt.Errorf("either vpc_id or default_availability_zone must be set")
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func resourceAwsOpsworksStackCustomCookbooksSource(d *schema.ResourceData) *opsworks.Source {
   220  	count := d.Get("custom_cookbooks_source.#").(int)
   221  	if count == 0 {
   222  		return nil
   223  	}
   224  
   225  	return &opsworks.Source{
   226  		Type:     aws.String(d.Get("custom_cookbooks_source.0.type").(string)),
   227  		Url:      aws.String(d.Get("custom_cookbooks_source.0.url").(string)),
   228  		Username: aws.String(d.Get("custom_cookbooks_source.0.username").(string)),
   229  		Password: aws.String(d.Get("custom_cookbooks_source.0.password").(string)),
   230  		Revision: aws.String(d.Get("custom_cookbooks_source.0.revision").(string)),
   231  		SshKey:   aws.String(d.Get("custom_cookbooks_source.0.ssh_key").(string)),
   232  	}
   233  }
   234  
   235  func resourceAwsOpsworksSetStackCustomCookbooksSource(d *schema.ResourceData, v *opsworks.Source) {
   236  	nv := make([]interface{}, 0, 1)
   237  	if v != nil && v.Type != nil && *v.Type != "" {
   238  		m := make(map[string]interface{})
   239  		if v.Type != nil {
   240  			m["type"] = *v.Type
   241  		}
   242  		if v.Url != nil {
   243  			m["url"] = *v.Url
   244  		}
   245  		if v.Username != nil {
   246  			m["username"] = *v.Username
   247  		}
   248  		if v.Revision != nil {
   249  			m["revision"] = *v.Revision
   250  		}
   251  		// v.Password will, on read, contain the placeholder string
   252  		// "*****FILTERED*****", so we ignore it on read and let persist
   253  		// the value already in the state.
   254  		nv = append(nv, m)
   255  	}
   256  
   257  	err := d.Set("custom_cookbooks_source", nv)
   258  	if err != nil {
   259  		// should never happen
   260  		panic(err)
   261  	}
   262  }
   263  
   264  func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error {
   265  	client := meta.(*AWSClient).opsworksconn
   266  	var conErr error
   267  	if v := d.Get("stack_endpoint").(string); v != "" {
   268  		client, conErr = opsworksConnForRegion(v, meta)
   269  		if conErr != nil {
   270  			return conErr
   271  		}
   272  	}
   273  
   274  	req := &opsworks.DescribeStacksInput{
   275  		StackIds: []*string{
   276  			aws.String(d.Id()),
   277  		},
   278  	}
   279  
   280  	log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id())
   281  
   282  	// notFound represents the number of times we've called DescribeStacks looking
   283  	// for this Stack. If it's not found in the the default region we're in, we
   284  	// check us-east-1 in the event this stack was created with Terraform before
   285  	// version 0.9
   286  	// See https://github.com/hashicorp/terraform/issues/12842
   287  	var notFound int
   288  	var resp *opsworks.DescribeStacksOutput
   289  	var dErr error
   290  
   291  	for {
   292  		resp, dErr = client.DescribeStacks(req)
   293  		if dErr != nil {
   294  			if awserr, ok := dErr.(awserr.Error); ok {
   295  				if awserr.Code() == "ResourceNotFoundException" {
   296  					if notFound < 1 {
   297  						// If we haven't already, try us-east-1, legacy connection
   298  						notFound++
   299  						var connErr error
   300  						client, connErr = opsworksConnForRegion("us-east-1", meta)
   301  						if connErr != nil {
   302  							return connErr
   303  						}
   304  						// start again from the top of the FOR loop, but with a client
   305  						// configured to talk to us-east-1
   306  						continue
   307  					}
   308  
   309  					// We've tried both the original and us-east-1 endpoint, and the stack
   310  					// is still not found
   311  					log.Printf("[DEBUG] OpsWorks stack (%s) not found", d.Id())
   312  					d.SetId("")
   313  					return nil
   314  				}
   315  				// not ResoureNotFoundException, fall through to returning error
   316  			}
   317  			return dErr
   318  		}
   319  		// If the stack was found, set the stack_endpoint
   320  		if client.Config.Region != nil && *client.Config.Region != "" {
   321  			log.Printf("[DEBUG] Setting stack_endpoint for (%s) to (%s)", d.Id(), *client.Config.Region)
   322  			if err := d.Set("stack_endpoint", *client.Config.Region); err != nil {
   323  				log.Printf("[WARN] Error setting stack_endpoint: %s", err)
   324  			}
   325  		}
   326  		log.Printf("[DEBUG] Breaking stack endpoint search, found stack for (%s)", d.Id())
   327  		// Break the FOR loop
   328  		break
   329  	}
   330  
   331  	stack := resp.Stacks[0]
   332  	d.Set("agent_version", stack.AgentVersion)
   333  	d.Set("name", stack.Name)
   334  	d.Set("region", stack.Region)
   335  	d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn)
   336  	d.Set("service_role_arn", stack.ServiceRoleArn)
   337  	d.Set("default_availability_zone", stack.DefaultAvailabilityZone)
   338  	d.Set("default_os", stack.DefaultOs)
   339  	d.Set("default_root_device_type", stack.DefaultRootDeviceType)
   340  	d.Set("default_ssh_key_name", stack.DefaultSshKeyName)
   341  	d.Set("default_subnet_id", stack.DefaultSubnetId)
   342  	d.Set("hostname_theme", stack.HostnameTheme)
   343  	d.Set("use_custom_cookbooks", stack.UseCustomCookbooks)
   344  	if stack.CustomJson != nil {
   345  		d.Set("custom_json", stack.CustomJson)
   346  	}
   347  	d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups)
   348  	d.Set("vpc_id", stack.VpcId)
   349  	if color, ok := stack.Attributes["Color"]; ok {
   350  		d.Set("color", color)
   351  	}
   352  	if stack.ConfigurationManager != nil {
   353  		d.Set("configuration_manager_name", stack.ConfigurationManager.Name)
   354  		d.Set("configuration_manager_version", stack.ConfigurationManager.Version)
   355  	}
   356  	if stack.ChefConfiguration != nil {
   357  		d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion)
   358  		d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf)
   359  	}
   360  	resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource)
   361  
   362  	return nil
   363  }
   364  
   365  // opsworksConn will return a connection for the stack_endpoint in the
   366  // configuration. Stacks can only be accessed or managed within the endpoint
   367  // in which they are created, so we allow users to specify an original endpoint
   368  // for Stacks created before multiple endpoints were offered (Terraform v0.9.0).
   369  // See:
   370  //  - https://github.com/hashicorp/terraform/pull/12688
   371  //  - https://github.com/hashicorp/terraform/issues/12842
   372  func opsworksConnForRegion(region string, meta interface{}) (*opsworks.OpsWorks, error) {
   373  	originalConn := meta.(*AWSClient).opsworksconn
   374  
   375  	// Regions are the same, no need to reconfigure
   376  	if originalConn.Config.Region != nil && *originalConn.Config.Region == region {
   377  		return originalConn, nil
   378  	}
   379  
   380  	// Set up base session
   381  	sess, err := session.NewSession(&originalConn.Config)
   382  	if err != nil {
   383  		return nil, errwrap.Wrapf("Error creating AWS session: {{err}}", err)
   384  	}
   385  
   386  	sess.Handlers.Build.PushBackNamed(addTerraformVersionToUserAgent)
   387  
   388  	if extraDebug := os.Getenv("TERRAFORM_AWS_AUTHFAILURE_DEBUG"); extraDebug != "" {
   389  		sess.Handlers.UnmarshalError.PushFrontNamed(debugAuthFailure)
   390  	}
   391  
   392  	newSession := sess.Copy(&aws.Config{Region: aws.String(region)})
   393  	newOpsworksconn := opsworks.New(newSession)
   394  
   395  	log.Printf("[DEBUG] Returning new OpsWorks client")
   396  	return newOpsworksconn, nil
   397  }
   398  
   399  func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error {
   400  	client := meta.(*AWSClient).opsworksconn
   401  
   402  	err := resourceAwsOpsworksStackValidate(d)
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	req := &opsworks.CreateStackInput{
   408  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   409  		Name:                      aws.String(d.Get("name").(string)),
   410  		Region:                    aws.String(d.Get("region").(string)),
   411  		ServiceRoleArn:            aws.String(d.Get("service_role_arn").(string)),
   412  		DefaultOs:                 aws.String(d.Get("default_os").(string)),
   413  		UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)),
   414  	}
   415  	req.ConfigurationManager = &opsworks.StackConfigurationManager{
   416  		Name:    aws.String(d.Get("configuration_manager_name").(string)),
   417  		Version: aws.String(d.Get("configuration_manager_version").(string)),
   418  	}
   419  	inVpc := false
   420  	if vpcId, ok := d.GetOk("vpc_id"); ok {
   421  		req.VpcId = aws.String(vpcId.(string))
   422  		inVpc = true
   423  	}
   424  	if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok {
   425  		req.DefaultSubnetId = aws.String(defaultSubnetId.(string))
   426  	}
   427  	if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok {
   428  		req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string))
   429  	}
   430  	if defaultRootDeviceType, ok := d.GetOk("default_root_device_type"); ok {
   431  		req.DefaultRootDeviceType = aws.String(defaultRootDeviceType.(string))
   432  	}
   433  
   434  	log.Printf("[DEBUG] Creating OpsWorks stack: %s", req)
   435  
   436  	var resp *opsworks.CreateStackOutput
   437  	err = resource.Retry(20*time.Minute, func() *resource.RetryError {
   438  		var cerr error
   439  		resp, cerr = client.CreateStack(req)
   440  		if cerr != nil {
   441  			if opserr, ok := cerr.(awserr.Error); ok {
   442  				// If Terraform is also managing the service IAM role,
   443  				// it may have just been created and not yet be
   444  				// propagated.
   445  				// AWS doesn't provide a machine-readable code for this
   446  				// specific error, so we're forced to do fragile message
   447  				// matching.
   448  				// The full error we're looking for looks something like
   449  				// the following:
   450  				// Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes
   451  				propErr := "not yet propagated"
   452  				trustErr := "not the necessary trust relationship"
   453  				validateErr := "validate IAM role permission"
   454  				if opserr.Code() == "ValidationException" && (strings.Contains(opserr.Message(), trustErr) || strings.Contains(opserr.Message(), propErr) || strings.Contains(opserr.Message(), validateErr)) {
   455  					log.Printf("[INFO] Waiting for service IAM role to propagate")
   456  					return resource.RetryableError(cerr)
   457  				}
   458  			}
   459  			return resource.NonRetryableError(cerr)
   460  		}
   461  		return nil
   462  	})
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	stackId := *resp.StackId
   468  	d.SetId(stackId)
   469  	d.Set("id", stackId)
   470  
   471  	if inVpc && *req.UseOpsworksSecurityGroups {
   472  		// For VPC-based stacks, OpsWorks asynchronously creates some default
   473  		// security groups which must exist before layers can be created.
   474  		// Unfortunately it doesn't tell us what the ids of these are, so
   475  		// we can't actually check for them. Instead, we just wait a nominal
   476  		// amount of time for their creation to complete.
   477  		log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created")
   478  		time.Sleep(30 * time.Second)
   479  	}
   480  
   481  	return resourceAwsOpsworksStackUpdate(d, meta)
   482  }
   483  
   484  func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error {
   485  	client := meta.(*AWSClient).opsworksconn
   486  	var conErr error
   487  	if v := d.Get("stack_endpoint").(string); v != "" {
   488  		client, conErr = opsworksConnForRegion(v, meta)
   489  		if conErr != nil {
   490  			return conErr
   491  		}
   492  	}
   493  
   494  	err := resourceAwsOpsworksStackValidate(d)
   495  	if err != nil {
   496  		return err
   497  	}
   498  
   499  	req := &opsworks.UpdateStackInput{
   500  		CustomJson:                aws.String(d.Get("custom_json").(string)),
   501  		DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)),
   502  		DefaultRootDeviceType:     aws.String(d.Get("default_root_device_type").(string)),
   503  		DefaultSshKeyName:         aws.String(d.Get("default_ssh_key_name").(string)),
   504  		Name:                      aws.String(d.Get("name").(string)),
   505  		ServiceRoleArn:            aws.String(d.Get("service_role_arn").(string)),
   506  		StackId:                   aws.String(d.Id()),
   507  		UseCustomCookbooks:        aws.Bool(d.Get("use_custom_cookbooks").(bool)),
   508  		UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)),
   509  		Attributes:                make(map[string]*string),
   510  		CustomCookbooksSource:     resourceAwsOpsworksStackCustomCookbooksSource(d),
   511  	}
   512  	if v, ok := d.GetOk("agent_version"); ok {
   513  		req.AgentVersion = aws.String(v.(string))
   514  	}
   515  	if v, ok := d.GetOk("default_os"); ok {
   516  		req.DefaultOs = aws.String(v.(string))
   517  	}
   518  	if v, ok := d.GetOk("default_subnet_id"); ok {
   519  		req.DefaultSubnetId = aws.String(v.(string))
   520  	}
   521  	if v, ok := d.GetOk("default_availability_zone"); ok {
   522  		req.DefaultAvailabilityZone = aws.String(v.(string))
   523  	}
   524  	if v, ok := d.GetOk("hostname_theme"); ok {
   525  		req.HostnameTheme = aws.String(v.(string))
   526  	}
   527  	if v, ok := d.GetOk("color"); ok {
   528  		req.Attributes["Color"] = aws.String(v.(string))
   529  	}
   530  
   531  	req.ChefConfiguration = &opsworks.ChefConfiguration{
   532  		BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)),
   533  		ManageBerkshelf:  aws.Bool(d.Get("manage_berkshelf").(bool)),
   534  	}
   535  
   536  	req.ConfigurationManager = &opsworks.StackConfigurationManager{
   537  		Name:    aws.String(d.Get("configuration_manager_name").(string)),
   538  		Version: aws.String(d.Get("configuration_manager_version").(string)),
   539  	}
   540  
   541  	log.Printf("[DEBUG] Updating OpsWorks stack: %s", req)
   542  
   543  	_, err = client.UpdateStack(req)
   544  	if err != nil {
   545  		return err
   546  	}
   547  
   548  	return resourceAwsOpsworksStackRead(d, meta)
   549  }
   550  
   551  func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error {
   552  	client := meta.(*AWSClient).opsworksconn
   553  	var conErr error
   554  	if v := d.Get("stack_endpoint").(string); v != "" {
   555  		client, conErr = opsworksConnForRegion(v, meta)
   556  		if conErr != nil {
   557  			return conErr
   558  		}
   559  	}
   560  
   561  	req := &opsworks.DeleteStackInput{
   562  		StackId: aws.String(d.Id()),
   563  	}
   564  
   565  	log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id())
   566  
   567  	_, err := client.DeleteStack(req)
   568  	if err != nil {
   569  		return err
   570  	}
   571  
   572  	// For a stack in a VPC, OpsWorks has created some default security groups
   573  	// in the VPC, which it will now delete.
   574  	// Unfortunately, the security groups are deleted asynchronously and there
   575  	// is no robust way for us to determine when it is done. The VPC itself
   576  	// isn't deletable until the security groups are cleaned up, so this could
   577  	// make 'terraform destroy' fail if the VPC is also managed and we don't
   578  	// wait for the security groups to be deleted.
   579  	// There is no robust way to check for this, so we'll just wait a
   580  	// nominal amount of time.
   581  	_, inVpc := d.GetOk("vpc_id")
   582  	_, useOpsworksDefaultSg := d.GetOk("use_opsworks_security_group")
   583  
   584  	if inVpc && useOpsworksDefaultSg {
   585  		log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted")
   586  		time.Sleep(30 * time.Second)
   587  	}
   588  
   589  	return nil
   590  }