Skip to content

Commit ad07902

Browse files
authored
Merge pull request #21694 from joraff/f-ec2-ami-org-launch-permissions
Adds support for AMI sharing to Orgs and OUs
2 parents d0c49cd + d4c38b4 commit ad07902

8 files changed

+492
-220
lines changed

.changelog/20677.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_ami_launch_permission: Add `group` argument
3+
```

.changelog/21694.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_ami_launch_permission: Add `organization_arn` and `organizational_unit_arn` arguments
3+
```
+195-67
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,264 @@
11
package ec2
22

33
import (
4+
"context"
45
"fmt"
56
"log"
7+
"regexp"
68
"strings"
79

810
"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"
1012
"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"
1115
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
16+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1217
"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"
1320
)
1421

1522
func ResourceAMILaunchPermission() *schema.Resource {
1623
return &schema.Resource{
17-
Create: resourceAMILaunchPermissionCreate,
18-
Read: resourceAMILaunchPermissionRead,
19-
Delete: resourceAMILaunchPermissionDelete,
24+
CreateWithoutTimeout: resourceAMILaunchPermissionCreate,
25+
ReadWithoutTimeout: resourceAMILaunchPermissionRead,
26+
DeleteWithoutTimeout: resourceAMILaunchPermissionDelete,
27+
2028
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,
3330
},
3431

3532
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+
},
3646
"image_id": {
3747
Type: schema.TypeString,
3848
Required: true,
3949
ForceNew: true,
4050
},
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"},
4564
},
4665
},
4766
}
4867
}
4968

50-
func resourceAMILaunchPermissionCreate(d *schema.ResourceData, meta interface{}) error {
69+
func resourceAMILaunchPermissionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
5170
conn := meta.(*conns.AWSClient).EC2Conn
5271

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{
5879
Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
80+
ImageId: aws.String(imageID),
5981
LaunchPermission: &ec2.LaunchPermissionModifications{
60-
Add: []*ec2.LaunchPermission{
61-
{UserId: aws.String(account_id)},
62-
},
82+
Add: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN),
6383
},
64-
})
84+
}
85+
86+
log.Printf("[DEBUG] Creating AMI Launch Permission: %s", input)
87+
_, err := conn.ModifyImageAttributeWithContext(ctx, input)
88+
6589
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)
6791
}
6892

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)
7196
}
7297

73-
func resourceAMILaunchPermissionRead(d *schema.ResourceData, meta interface{}) error {
98+
func resourceAMILaunchPermissionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
7499
conn := meta.(*conns.AWSClient).EC2Conn
75100

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+
77103
if err != nil {
78-
return fmt.Errorf("error reading AMI launch permission (%s): %w", d.Id(), err)
104+
return diag.FromErr(err)
79105
}
80-
if !exists {
81-
if d.IsNewResource() {
82-
return fmt.Errorf("error reading EC2 AMI Launch Permission (%s): not found", d.Id())
83-
}
84106

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())
86111
d.SetId("")
87112
return nil
88113
}
89114

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+
90125
return nil
91126
}
92127

93-
func resourceAMILaunchPermissionDelete(d *schema.ResourceData, meta interface{}) error {
128+
func resourceAMILaunchPermissionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
94129
conn := meta.(*conns.AWSClient).EC2Conn
95130

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+
}
98136

99-
_, err := conn.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{
100-
ImageId: aws.String(image_id),
137+
input := &ec2.ModifyImageAttributeInput{
101138
Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
139+
ImageId: aws.String(imageID),
102140
LaunchPermission: &ec2.LaunchPermissionModifications{
103-
Remove: []*ec2.LaunchPermission{
104-
{UserId: aws.String(account_id)},
105-
},
141+
Remove: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN),
106142
},
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+
108152
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)
110154
}
111155

112156
return nil
113157
}
114158

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+
}
126189
}
127-
return false, err
128190
}
129191

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
133260
}
134261
}
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)
136264
}

0 commit comments

Comments
 (0)