|
1 | 1 | package ec2
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "context" |
4 | 5 | "fmt"
|
5 | 6 | "log"
|
| 7 | + "regexp" |
6 | 8 | "strings"
|
7 | 9 |
|
8 | 10 | "github.com/aws/aws-sdk-go/aws"
|
9 |
| - "github.com/aws/aws-sdk-go/aws/awserr" |
| 11 | + "github.com/aws/aws-sdk-go/aws/arn" |
10 | 12 | "github.com/aws/aws-sdk-go/service/ec2"
|
| 13 | + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" |
| 14 | + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" |
11 | 15 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
| 16 | + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" |
12 | 17 | "github.com/hashicorp/terraform-provider-aws/internal/conns"
|
| 18 | + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" |
| 19 | + "github.com/hashicorp/terraform-provider-aws/internal/verify" |
13 | 20 | )
|
14 | 21 |
|
15 | 22 | func ResourceAMILaunchPermission() *schema.Resource {
|
16 | 23 | return &schema.Resource{
|
17 |
| - Create: resourceAMILaunchPermissionCreate, |
18 |
| - Read: resourceAMILaunchPermissionRead, |
19 |
| - Delete: resourceAMILaunchPermissionDelete, |
| 24 | + CreateWithoutTimeout: resourceAMILaunchPermissionCreate, |
| 25 | + ReadWithoutTimeout: resourceAMILaunchPermissionRead, |
| 26 | + DeleteWithoutTimeout: resourceAMILaunchPermissionDelete, |
| 27 | + |
20 | 28 | Importer: &schema.ResourceImporter{
|
21 |
| - State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { |
22 |
| - idParts := strings.Split(d.Id(), "/") |
23 |
| - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { |
24 |
| - return nil, fmt.Errorf("Unexpected format of ID (%q), expected ACCOUNT-ID/IMAGE-ID", d.Id()) |
25 |
| - } |
26 |
| - accountId := idParts[0] |
27 |
| - imageId := idParts[1] |
28 |
| - d.Set("account_id", accountId) |
29 |
| - d.Set("image_id", imageId) |
30 |
| - d.SetId(fmt.Sprintf("%s-%s", imageId, accountId)) |
31 |
| - return []*schema.ResourceData{d}, nil |
32 |
| - }, |
| 29 | + StateContext: resourceAMILaunchPermissionImport, |
33 | 30 | },
|
34 | 31 |
|
35 | 32 | Schema: map[string]*schema.Schema{
|
| 33 | + "account_id": { |
| 34 | + Type: schema.TypeString, |
| 35 | + Optional: true, |
| 36 | + ForceNew: true, |
| 37 | + ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"}, |
| 38 | + }, |
| 39 | + "group": { |
| 40 | + Type: schema.TypeString, |
| 41 | + Optional: true, |
| 42 | + ForceNew: true, |
| 43 | + ValidateFunc: validation.StringInSlice(ec2.PermissionGroup_Values(), false), |
| 44 | + ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"}, |
| 45 | + }, |
36 | 46 | "image_id": {
|
37 | 47 | Type: schema.TypeString,
|
38 | 48 | Required: true,
|
39 | 49 | ForceNew: true,
|
40 | 50 | },
|
41 |
| - "account_id": { |
42 |
| - Type: schema.TypeString, |
43 |
| - Required: true, |
44 |
| - ForceNew: true, |
| 51 | + "organization_arn": { |
| 52 | + Type: schema.TypeString, |
| 53 | + Optional: true, |
| 54 | + ForceNew: true, |
| 55 | + ValidateFunc: verify.ValidARN, |
| 56 | + ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"}, |
| 57 | + }, |
| 58 | + "organizational_unit_arn": { |
| 59 | + Type: schema.TypeString, |
| 60 | + Optional: true, |
| 61 | + ForceNew: true, |
| 62 | + ValidateFunc: verify.ValidARN, |
| 63 | + ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"}, |
45 | 64 | },
|
46 | 65 | },
|
47 | 66 | }
|
48 | 67 | }
|
49 | 68 |
|
50 |
| -func resourceAMILaunchPermissionCreate(d *schema.ResourceData, meta interface{}) error { |
| 69 | +func resourceAMILaunchPermissionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { |
51 | 70 | conn := meta.(*conns.AWSClient).EC2Conn
|
52 | 71 |
|
53 |
| - image_id := d.Get("image_id").(string) |
54 |
| - account_id := d.Get("account_id").(string) |
55 |
| - |
56 |
| - _, err := conn.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ |
57 |
| - ImageId: aws.String(image_id), |
| 72 | + imageID := d.Get("image_id").(string) |
| 73 | + accountID := d.Get("account_id").(string) |
| 74 | + group := d.Get("group").(string) |
| 75 | + organizationARN := d.Get("organization_arn").(string) |
| 76 | + organizationalUnitARN := d.Get("organizational_unit_arn").(string) |
| 77 | + id := AMILaunchPermissionCreateResourceID(imageID, accountID, group, organizationARN, organizationalUnitARN) |
| 78 | + input := &ec2.ModifyImageAttributeInput{ |
58 | 79 | Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
|
| 80 | + ImageId: aws.String(imageID), |
59 | 81 | LaunchPermission: &ec2.LaunchPermissionModifications{
|
60 |
| - Add: []*ec2.LaunchPermission{ |
61 |
| - {UserId: aws.String(account_id)}, |
62 |
| - }, |
| 82 | + Add: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN), |
63 | 83 | },
|
64 |
| - }) |
| 84 | + } |
| 85 | + |
| 86 | + log.Printf("[DEBUG] Creating AMI Launch Permission: %s", input) |
| 87 | + _, err := conn.ModifyImageAttributeWithContext(ctx, input) |
| 88 | + |
65 | 89 | if err != nil {
|
66 |
| - return fmt.Errorf("error creating AMI launch permission: %w", err) |
| 90 | + return diag.Errorf("creating AMI Launch Permission (%s): %s", id, err) |
67 | 91 | }
|
68 | 92 |
|
69 |
| - d.SetId(fmt.Sprintf("%s-%s", image_id, account_id)) |
70 |
| - return nil |
| 93 | + d.SetId(id) |
| 94 | + |
| 95 | + return resourceAMILaunchPermissionRead(ctx, d, meta) |
71 | 96 | }
|
72 | 97 |
|
73 |
| -func resourceAMILaunchPermissionRead(d *schema.ResourceData, meta interface{}) error { |
| 98 | +func resourceAMILaunchPermissionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { |
74 | 99 | conn := meta.(*conns.AWSClient).EC2Conn
|
75 | 100 |
|
76 |
| - exists, err := HasLaunchPermission(conn, d.Get("image_id").(string), d.Get("account_id").(string)) |
| 101 | + imageID, accountID, group, organizationARN, organizationalUnitARN, err := AMILaunchPermissionParseResourceID(d.Id()) |
| 102 | + |
77 | 103 | if err != nil {
|
78 |
| - return fmt.Errorf("error reading AMI launch permission (%s): %w", d.Id(), err) |
| 104 | + return diag.FromErr(err) |
79 | 105 | }
|
80 |
| - if !exists { |
81 |
| - if d.IsNewResource() { |
82 |
| - return fmt.Errorf("error reading EC2 AMI Launch Permission (%s): not found", d.Id()) |
83 |
| - } |
84 | 106 |
|
85 |
| - log.Printf("[WARN] AMI launch permission (%s) not found, removing from state", d.Id()) |
| 107 | + _, err = FindImageLaunchPermission(ctx, conn, imageID, accountID, group, organizationARN, organizationalUnitARN) |
| 108 | + |
| 109 | + if !d.IsNewResource() && tfresource.NotFound(err) { |
| 110 | + log.Printf("[WARN] AMI Launch Permission %s not found, removing from state", d.Id()) |
86 | 111 | d.SetId("")
|
87 | 112 | return nil
|
88 | 113 | }
|
89 | 114 |
|
| 115 | + if err != nil { |
| 116 | + return diag.Errorf("reading AMI Launch Permission (%s): %s", d.Id(), err) |
| 117 | + } |
| 118 | + |
| 119 | + d.Set("account_id", accountID) |
| 120 | + d.Set("group", group) |
| 121 | + d.Set("image_id", imageID) |
| 122 | + d.Set("organization_arn", organizationARN) |
| 123 | + d.Set("organizational_unit_arn", organizationalUnitARN) |
| 124 | + |
90 | 125 | return nil
|
91 | 126 | }
|
92 | 127 |
|
93 |
| -func resourceAMILaunchPermissionDelete(d *schema.ResourceData, meta interface{}) error { |
| 128 | +func resourceAMILaunchPermissionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { |
94 | 129 | conn := meta.(*conns.AWSClient).EC2Conn
|
95 | 130 |
|
96 |
| - image_id := d.Get("image_id").(string) |
97 |
| - account_id := d.Get("account_id").(string) |
| 131 | + imageID, accountID, group, organizationARN, organizationalUnitARN, err := AMILaunchPermissionParseResourceID(d.Id()) |
| 132 | + |
| 133 | + if err != nil { |
| 134 | + return diag.FromErr(err) |
| 135 | + } |
98 | 136 |
|
99 |
| - _, err := conn.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ |
100 |
| - ImageId: aws.String(image_id), |
| 137 | + input := &ec2.ModifyImageAttributeInput{ |
101 | 138 | Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
|
| 139 | + ImageId: aws.String(imageID), |
102 | 140 | LaunchPermission: &ec2.LaunchPermissionModifications{
|
103 |
| - Remove: []*ec2.LaunchPermission{ |
104 |
| - {UserId: aws.String(account_id)}, |
105 |
| - }, |
| 141 | + Remove: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN), |
106 | 142 | },
|
107 |
| - }) |
| 143 | + } |
| 144 | + |
| 145 | + log.Printf("[INFO] Deleting AMI Launch Permission: %s", d.Id()) |
| 146 | + _, err = conn.ModifyImageAttributeWithContext(ctx, input) |
| 147 | + |
| 148 | + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidAMIIDNotFound, ErrCodeInvalidAMIIDUnavailable) { |
| 149 | + return nil |
| 150 | + } |
| 151 | + |
108 | 152 | if err != nil {
|
109 |
| - return fmt.Errorf("error deleting AMI launch permission (%s): %w", d.Id(), err) |
| 153 | + return diag.Errorf("deleting AMI Launch Permission (%s): %s", d.Id(), err) |
110 | 154 | }
|
111 | 155 |
|
112 | 156 | return nil
|
113 | 157 | }
|
114 | 158 |
|
115 |
| -func HasLaunchPermission(conn *ec2.EC2, image_id string, account_id string) (bool, error) { |
116 |
| - attrs, err := conn.DescribeImageAttribute(&ec2.DescribeImageAttributeInput{ |
117 |
| - ImageId: aws.String(image_id), |
118 |
| - Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission), |
119 |
| - }) |
120 |
| - if err != nil { |
121 |
| - // When an AMI disappears out from under a launch permission resource, we will |
122 |
| - // see either InvalidAMIID.NotFound or InvalidAMIID.Unavailable. |
123 |
| - if ec2err, ok := err.(awserr.Error); ok && strings.HasPrefix(ec2err.Code(), "InvalidAMIID") { |
124 |
| - log.Printf("[DEBUG] %s no longer exists, so we'll drop launch permission for %s from the state", image_id, account_id) |
125 |
| - return false, nil |
| 159 | +func resourceAMILaunchPermissionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { |
| 160 | + const importIDSeparator = "/" |
| 161 | + parts := strings.Split(d.Id(), importIDSeparator) |
| 162 | + |
| 163 | + // Heuristic to identify the permission type. |
| 164 | + var ok bool |
| 165 | + if n := len(parts); n >= 2 { |
| 166 | + if permissionID, imageID := strings.Join(parts[:n-1], importIDSeparator), parts[n-1]; permissionID != "" && imageID != "" { |
| 167 | + if regexp.MustCompile(`^\d{12}$`).MatchString(permissionID) { |
| 168 | + // AWS account ID. |
| 169 | + d.SetId(AMILaunchPermissionCreateResourceID(imageID, permissionID, "", "", "")) |
| 170 | + ok = true |
| 171 | + } else if arn.IsARN(permissionID) { |
| 172 | + if v, _ := arn.Parse(permissionID); v.Service == "organizations" { |
| 173 | + // See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsorganizations.html#awsorganizations-resources-for-iam-policies. |
| 174 | + if strings.HasPrefix(v.Resource, "organization/") { |
| 175 | + // Organization ARN. |
| 176 | + d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", "", permissionID, "")) |
| 177 | + ok = true |
| 178 | + } else if strings.HasPrefix(v.Resource, "ou/") { |
| 179 | + // Organizational unit ARN. |
| 180 | + d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", "", "", permissionID)) |
| 181 | + ok = true |
| 182 | + } |
| 183 | + } |
| 184 | + } else { |
| 185 | + // Group name. |
| 186 | + d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", permissionID, "", "")) |
| 187 | + ok = true |
| 188 | + } |
126 | 189 | }
|
127 |
| - return false, err |
128 | 190 | }
|
129 | 191 |
|
130 |
| - for _, lp := range attrs.LaunchPermissions { |
131 |
| - if aws.StringValue(lp.UserId) == account_id { |
132 |
| - return true, nil |
| 192 | + if !ok { |
| 193 | + return nil, fmt.Errorf("unexpected format for ID (%[1]s), expected [ACCOUNT-ID|GROUP-NAME|ORGANIZATION-ARN|ORGANIZATIONAL-UNIT-ARN]%[2]sIMAGE-ID", d.Id(), importIDSeparator) |
| 194 | + } |
| 195 | + |
| 196 | + return []*schema.ResourceData{d}, nil |
| 197 | +} |
| 198 | + |
| 199 | +func expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN string) []*ec2.LaunchPermission { |
| 200 | + apiObject := &ec2.LaunchPermission{} |
| 201 | + |
| 202 | + if accountID != "" { |
| 203 | + apiObject.UserId = aws.String(accountID) |
| 204 | + } |
| 205 | + |
| 206 | + if group != "" { |
| 207 | + apiObject.Group = aws.String(group) |
| 208 | + } |
| 209 | + |
| 210 | + if organizationARN != "" { |
| 211 | + apiObject.OrganizationArn = aws.String(organizationARN) |
| 212 | + } |
| 213 | + |
| 214 | + if organizationalUnitARN != "" { |
| 215 | + apiObject.OrganizationalUnitArn = aws.String(organizationalUnitARN) |
| 216 | + } |
| 217 | + |
| 218 | + return []*ec2.LaunchPermission{apiObject} |
| 219 | +} |
| 220 | + |
| 221 | +const ( |
| 222 | + amiLaunchPermissionIDSeparator = "-" |
| 223 | + amiLaunchPermissionIDGroupIndicator = "group" |
| 224 | + amiLaunchPermissionIDOrganizationIndicator = "org" |
| 225 | + amiLaunchPermissionIDOrganizationalUnitIndicator = "ou" |
| 226 | +) |
| 227 | + |
| 228 | +func AMILaunchPermissionCreateResourceID(imageID, accountID, group, organizationARN, organizationalUnitARN string) string { |
| 229 | + parts := []string{imageID} |
| 230 | + |
| 231 | + if accountID != "" { |
| 232 | + parts = append(parts, accountID) |
| 233 | + } else if group != "" { |
| 234 | + parts = append(parts, amiLaunchPermissionIDGroupIndicator, group) |
| 235 | + } else if organizationARN != "" { |
| 236 | + parts = append(parts, amiLaunchPermissionIDOrganizationIndicator, organizationARN) |
| 237 | + } else if organizationalUnitARN != "" { |
| 238 | + parts = append(parts, amiLaunchPermissionIDOrganizationalUnitIndicator, organizationalUnitARN) |
| 239 | + } |
| 240 | + |
| 241 | + id := strings.Join(parts, amiLaunchPermissionIDSeparator) |
| 242 | + |
| 243 | + return id |
| 244 | +} |
| 245 | + |
| 246 | +func AMILaunchPermissionParseResourceID(id string) (string, string, string, string, string, error) { |
| 247 | + parts := strings.Split(id, amiLaunchPermissionIDSeparator) |
| 248 | + |
| 249 | + switch { |
| 250 | + case len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "": |
| 251 | + return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), parts[2], "", "", "", nil |
| 252 | + case len(parts) > 3 && parts[0] != "" && parts[1] != "" && parts[3] != "": |
| 253 | + switch parts[2] { |
| 254 | + case amiLaunchPermissionIDGroupIndicator: |
| 255 | + return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), "", "", nil |
| 256 | + case amiLaunchPermissionIDOrganizationIndicator: |
| 257 | + return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), "", nil |
| 258 | + case amiLaunchPermissionIDOrganizationalUnitIndicator: |
| 259 | + return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", "", "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), nil |
133 | 260 | }
|
134 | 261 | }
|
135 |
| - return false, nil |
| 262 | + |
| 263 | + return "", "", "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected IMAGE-ID%[2]sACCOUNT-ID or IMAGE-ID%[2]s%[3]s%[2]sGROUP-NAME or IMAGE-ID%[2]s%[4]s%[2]sORGANIZATION-ARN or IMAGE-ID%[2]s%[5]s%[2]sORGANIZATIONAL-UNIT-ARN", id, amiLaunchPermissionIDSeparator, amiLaunchPermissionIDGroupIndicator, amiLaunchPermissionIDOrganizationIndicator, amiLaunchPermissionIDOrganizationalUnitIndicator) |
136 | 264 | }
|
0 commit comments