github.com/gabrielperezs/terraform@v0.7.0-rc2.0.20160715084931-f7da2612946f/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 }