Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for AMI sharing to Orgs and OUs #21694

Merged
merged 31 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1cb51f2
aws_ami_launch_permission: support group permissions
grahamc Aug 24, 2021
0b275c1
Adds support for AMI sharing to Orgs and OUs
joraff Nov 9, 2021
d9b4f95
Refactor import to use a comma and include arn_type
joraff Nov 9, 2021
08c38a8
Adds acceptance test for org sharing
joraff Nov 9, 2021
38203e5
Refactors existing tests to support arns
joraff Nov 9, 2021
99f110c
Renames test to be more descriptive
joraff Nov 10, 2021
00dd171
Adds org-enabled account precheck to test
joraff Nov 10, 2021
1040f01
Migrate to using commas for import string format
joraff Apr 6, 2022
08ed717
r/aws_ami_launch_permission: Alphabetize attributes.
ewbankkit Apr 13, 2022
062284d
r/aws_ami_launch_permission: Tidy up resource Create.
ewbankkit Apr 13, 2022
94873a2
r/aws_ami_launch_permission: Tidy up resource Delete.
ewbankkit Apr 13, 2022
dda5100
r/aws_ami_launch_permission: Tidy up resource Read.
ewbankkit Apr 13, 2022
4791ba3
r/aws_ami_launch_permission: Tidy up acceptance tests.
ewbankkit Apr 13, 2022
3cd6961
r/aws_ami_launch_permission: Correct resource ID parsing.
ewbankkit Apr 13, 2022
624dbb0
r/aws_ami_launch_permission: Switch to Terraform Plugin SDK V2 Withou…
ewbankkit Apr 13, 2022
02bb473
Correct 'TestAccEC2AMILaunchPermission_Disappears_ami'.
ewbankkit Apr 13, 2022
ca9a501
Revert "aws_ami_launch_permission: support group permissions"
ewbankkit Apr 13, 2022
51dbc20
Merge commit 'ca9a501f7efd8c12aefd82a08b9b7cb419a6a979' into tmp-aws_…
ewbankkit Apr 13, 2022
aa11d81
r/aws_ami_launch_permission: Add 'group' argument.
ewbankkit Apr 13, 2022
708ca1b
Revert "Migrate to using commas for import string format"
ewbankkit Apr 13, 2022
dc92fac
Revert "Adds org-enabled account precheck to test"
ewbankkit Apr 13, 2022
9a52e5a
Revert "Renames test to be more descriptive"
ewbankkit Apr 13, 2022
2135b09
Revert "Refactors existing tests to support arns"
ewbankkit Apr 13, 2022
fbc9e59
Revert "Adds acceptance test for org sharing"
ewbankkit Apr 13, 2022
dc7d958
Revert "Refactor import to use a comma and include arn_type"
ewbankkit Apr 13, 2022
3418069
Revert "Adds support for AMI sharing to Orgs and OUs"
ewbankkit Apr 13, 2022
6a2fadb
Merge commit 'aa11d813e0648e6d46604fdcafd5553f9488aa88' into HEAD
ewbankkit Apr 13, 2022
9ec3100
Add CHANGELOG entry.
ewbankkit Apr 13, 2022
6f9bc91
r/aws_ami_launch_permission: Add 'organization_arn' and 'organization…
ewbankkit Apr 13, 2022
15519b9
Add 'TestAccEC2AMILaunchPermission_organizationARN' and 'TestAccEC2AM…
ewbankkit Apr 13, 2022
d4c38b4
Fix typo.
ewbankkit Apr 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/20677.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_ami_launch_permission: Add `group` argument
```
3 changes: 3 additions & 0 deletions .changelog/21694.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_ami_launch_permission: Add `organization_arn` and `organizational_unit_arn` arguments
```
262 changes: 195 additions & 67 deletions internal/service/ec2/ami_launch_permission.go
Original file line number Diff line number Diff line change
@@ -1,136 +1,264 @@
package ec2

import (
"context"
"fmt"
"log"
"regexp"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
)

func ResourceAMILaunchPermission() *schema.Resource {
return &schema.Resource{
Create: resourceAMILaunchPermissionCreate,
Read: resourceAMILaunchPermissionRead,
Delete: resourceAMILaunchPermissionDelete,
CreateWithoutTimeout: resourceAMILaunchPermissionCreate,
ReadWithoutTimeout: resourceAMILaunchPermissionRead,
DeleteWithoutTimeout: resourceAMILaunchPermissionDelete,

Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
idParts := strings.Split(d.Id(), "/")
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
return nil, fmt.Errorf("Unexpected format of ID (%q), expected ACCOUNT-ID/IMAGE-ID", d.Id())
}
accountId := idParts[0]
imageId := idParts[1]
d.Set("account_id", accountId)
d.Set("image_id", imageId)
d.SetId(fmt.Sprintf("%s-%s", imageId, accountId))
return []*schema.ResourceData{d}, nil
},
StateContext: resourceAMILaunchPermissionImport,
},

Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"},
},
"group": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(ec2.PermissionGroup_Values(), false),
ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"},
},
"image_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"account_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
"organization_arn": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"},
},
"organizational_unit_arn": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
ExactlyOneOf: []string{"account_id", "group", "organization_arn", "organizational_unit_arn"},
},
},
}
}

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

image_id := d.Get("image_id").(string)
account_id := d.Get("account_id").(string)

_, err := conn.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{
ImageId: aws.String(image_id),
imageID := d.Get("image_id").(string)
accountID := d.Get("account_id").(string)
group := d.Get("group").(string)
organizationARN := d.Get("organization_arn").(string)
organizationalUnitARN := d.Get("organizational_unit_arn").(string)
id := AMILaunchPermissionCreateResourceID(imageID, accountID, group, organizationARN, organizationalUnitARN)
input := &ec2.ModifyImageAttributeInput{
Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
ImageId: aws.String(imageID),
LaunchPermission: &ec2.LaunchPermissionModifications{
Add: []*ec2.LaunchPermission{
{UserId: aws.String(account_id)},
},
Add: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN),
},
})
}

log.Printf("[DEBUG] Creating AMI Launch Permission: %s", input)
_, err := conn.ModifyImageAttributeWithContext(ctx, input)

if err != nil {
return fmt.Errorf("error creating AMI launch permission: %w", err)
return diag.Errorf("creating AMI Launch Permission (%s): %s", id, err)
}

d.SetId(fmt.Sprintf("%s-%s", image_id, account_id))
return nil
d.SetId(id)

return resourceAMILaunchPermissionRead(ctx, d, meta)
}

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

exists, err := HasLaunchPermission(conn, d.Get("image_id").(string), d.Get("account_id").(string))
imageID, accountID, group, organizationARN, organizationalUnitARN, err := AMILaunchPermissionParseResourceID(d.Id())

if err != nil {
return fmt.Errorf("error reading AMI launch permission (%s): %w", d.Id(), err)
return diag.FromErr(err)
}
if !exists {
if d.IsNewResource() {
return fmt.Errorf("error reading EC2 AMI Launch Permission (%s): not found", d.Id())
}

log.Printf("[WARN] AMI launch permission (%s) not found, removing from state", d.Id())
_, err = FindImageLaunchPermission(ctx, conn, imageID, accountID, group, organizationARN, organizationalUnitARN)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] AMI Launch Permission %s not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return diag.Errorf("reading AMI Launch Permission (%s): %s", d.Id(), err)
}

d.Set("account_id", accountID)
d.Set("group", group)
d.Set("image_id", imageID)
d.Set("organization_arn", organizationARN)
d.Set("organizational_unit_arn", organizationalUnitARN)

return nil
}

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

image_id := d.Get("image_id").(string)
account_id := d.Get("account_id").(string)
imageID, accountID, group, organizationARN, organizationalUnitARN, err := AMILaunchPermissionParseResourceID(d.Id())

if err != nil {
return diag.FromErr(err)
}

_, err := conn.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{
ImageId: aws.String(image_id),
input := &ec2.ModifyImageAttributeInput{
Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
ImageId: aws.String(imageID),
LaunchPermission: &ec2.LaunchPermissionModifications{
Remove: []*ec2.LaunchPermission{
{UserId: aws.String(account_id)},
},
Remove: expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN),
},
})
}

log.Printf("[INFO] Deleting AMI Launch Permission: %s", d.Id())
_, err = conn.ModifyImageAttributeWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, ErrCodeInvalidAMIIDNotFound, ErrCodeInvalidAMIIDUnavailable) {
return nil
}

if err != nil {
return fmt.Errorf("error deleting AMI launch permission (%s): %w", d.Id(), err)
return diag.Errorf("deleting AMI Launch Permission (%s): %s", d.Id(), err)
}

return nil
}

func HasLaunchPermission(conn *ec2.EC2, image_id string, account_id string) (bool, error) {
attrs, err := conn.DescribeImageAttribute(&ec2.DescribeImageAttributeInput{
ImageId: aws.String(image_id),
Attribute: aws.String(ec2.ImageAttributeNameLaunchPermission),
})
if err != nil {
// When an AMI disappears out from under a launch permission resource, we will
// see either InvalidAMIID.NotFound or InvalidAMIID.Unavailable.
if ec2err, ok := err.(awserr.Error); ok && strings.HasPrefix(ec2err.Code(), "InvalidAMIID") {
log.Printf("[DEBUG] %s no longer exists, so we'll drop launch permission for %s from the state", image_id, account_id)
return false, nil
func resourceAMILaunchPermissionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
const importIDSeparator = "/"
parts := strings.Split(d.Id(), importIDSeparator)

// Heuristic to identify the permission type.
var ok bool
if n := len(parts); n >= 2 {
if permissionID, imageID := strings.Join(parts[:n-1], importIDSeparator), parts[n-1]; permissionID != "" && imageID != "" {
if regexp.MustCompile(`^\d{12}$`).MatchString(permissionID) {
// AWS account ID.
d.SetId(AMILaunchPermissionCreateResourceID(imageID, permissionID, "", "", ""))
ok = true
} else if arn.IsARN(permissionID) {
if v, _ := arn.Parse(permissionID); v.Service == "organizations" {
// See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsorganizations.html#awsorganizations-resources-for-iam-policies.
if strings.HasPrefix(v.Resource, "organization/") {
// Organization ARN.
d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", "", permissionID, ""))
ok = true
} else if strings.HasPrefix(v.Resource, "ou/") {
// Organizational unit ARN.
d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", "", "", permissionID))
ok = true
}
}
} else {
// Group name.
d.SetId(AMILaunchPermissionCreateResourceID(imageID, "", permissionID, "", ""))
ok = true
}
}
return false, err
}

for _, lp := range attrs.LaunchPermissions {
if aws.StringValue(lp.UserId) == account_id {
return true, nil
if !ok {
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)
}

return []*schema.ResourceData{d}, nil
}

func expandLaunchPermissions(accountID, group, organizationARN, organizationalUnitARN string) []*ec2.LaunchPermission {
apiObject := &ec2.LaunchPermission{}

if accountID != "" {
apiObject.UserId = aws.String(accountID)
}

if group != "" {
apiObject.Group = aws.String(group)
}

if organizationARN != "" {
apiObject.OrganizationArn = aws.String(organizationARN)
}

if organizationalUnitARN != "" {
apiObject.OrganizationArn = aws.String(organizationalUnitARN)
}

return []*ec2.LaunchPermission{apiObject}
}

const (
amiLaunchPermissionIDSeparator = "-"
amiLaunchPermissionIDGroupIndicator = "group"
amiLaunchPermissionIDOrganizationIndicator = "org"
amiLaunchPermissionIDOrganizationalUnitIndicator = "ou"
)

func AMILaunchPermissionCreateResourceID(imageID, accountID, group, organizationARN, organizationalUnitARN string) string {
parts := []string{imageID}

if accountID != "" {
parts = append(parts, accountID)
} else if group != "" {
parts = append(parts, amiLaunchPermissionIDGroupIndicator, group)
} else if organizationARN != "" {
parts = append(parts, amiLaunchPermissionIDOrganizationIndicator, organizationARN)
} else if organizationalUnitARN != "" {
parts = append(parts, amiLaunchPermissionIDOrganizationalUnitIndicator, organizationalUnitARN)
}

id := strings.Join(parts, amiLaunchPermissionIDSeparator)

return id
}

func AMILaunchPermissionParseResourceID(id string) (string, string, string, string, string, error) {
parts := strings.Split(id, amiLaunchPermissionIDSeparator)

switch {
case len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "":
return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), parts[2], "", "", "", nil
case len(parts) > 3 && parts[0] != "" && parts[1] != "" && parts[3] != "":
switch parts[2] {
case amiLaunchPermissionIDGroupIndicator:
return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), "", "", nil
case amiLaunchPermissionIDOrganizationIndicator:
return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), "", nil
case amiLaunchPermissionIDOrganizationalUnitIndicator:
return strings.Join([]string{parts[0], parts[1]}, amiLaunchPermissionIDSeparator), "", "", "", strings.Join(parts[3:], amiLaunchPermissionIDSeparator), nil
}
}
return false, nil

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)
}
Loading