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