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