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

New Resource: aws_lightsail_bucket_access_key. New Shared Resource Separator and associated functions #28699

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/28699.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_lightsail_bucket_access_key
```
2 changes: 2 additions & 0 deletions internal/create/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const (
ErrActionWaitingForCreation = "waiting for creation"
ErrActionWaitingForDeletion = "waiting for delete"
ErrActionWaitingForUpdate = "waiting for update"
ErrActionExpandingResourceId = "expanding resource id"
ErrActionFlatteningResourceId = "flattening resource id"
)

// ProblemStandardMessage is a standardized message for reporting errors and warnings
Expand Down
64 changes: 64 additions & 0 deletions internal/flex/flex.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package flex

import (
"fmt"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
// A common separator to be used for creating resource Ids from a combination of attributes
ResourceIdSeparator = ","
)

// Takes the result of flatmap.Expand for an array of strings
// and returns a []*string
func ExpandStringList(configured []interface{}) []*string {
Expand Down Expand Up @@ -143,3 +151,59 @@ func PointersMapToStringList(pointers map[string]*string) map[string]interface{}
}
return list
}

// Takes a string of resource attributes separated by the ResourceIdSeparator constant and an expected number of Id Parts
// Returns a list of the resource attributes strings used to construct the unique Id or an error message if the resource id does not parse properly
func ExpandResourceId(id string, partCount int) ([]string, error) {
idParts := strings.Split(id, ResourceIdSeparator)

if len(idParts) <= 1 {
return nil, fmt.Errorf("unexpected format for ID (%v), expected more than one part", idParts)
}

if len(idParts) != partCount {
return nil, fmt.Errorf("unexpected format for ID (%s), expected (%d) parts separated by (%s)", id, partCount, ResourceIdSeparator)
}

var emptyPart bool
emptyParts := make([]int, 0, partCount)
for index, part := range idParts {
if part == "" {
emptyPart = true
emptyParts = append(emptyParts, index)
}
}

if emptyPart {
return nil, fmt.Errorf("unexpected format for ID (%[1]s), the following id parts indexes are blank (%v)", id, emptyParts)
}

return idParts, nil
}

// Takes a list of the resource attributes as strings used to construct the unique Id and an expected number of Id Parts
// Returns a string of resource attributes separated by the ResourceIdSeparator constant or an error message if the id parts do not parse properly
func FlattenResourceId(idParts []string, partCount int) (string, error) {
if len(idParts) <= 1 {
return "", fmt.Errorf("unexpected format for ID parts (%v), expected more than one part", idParts)
}

if len(idParts) != partCount {
return "", fmt.Errorf("unexpected format for ID parts (%v), expected (%d) parts", idParts, partCount)
}

var emptyPart bool
emptyParts := make([]int, 0, len(idParts))
for index, part := range idParts {
if part == "" {
emptyPart = true
emptyParts = append(emptyParts, index)
}
}

if emptyPart {
return "", fmt.Errorf("unexpected format for ID parts (%v), the following id parts indexes are blank (%v)", idParts, emptyParts)
}

return strings.Join(idParts, ResourceIdSeparator), nil
}
85 changes: 85 additions & 0 deletions internal/flex/flex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flex

import (
"reflect"
"strings"
"testing"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -39,3 +40,87 @@ func TestExpandStringListEmptyItems(t *testing.T) {
expected)
}
}

func TestExpandResourceId(t *testing.T) {
resourceId := "foo,bar,baz"
expandedId, _ := ExpandResourceId(resourceId, 3)
expected := []string{
"foo",
"bar",
"baz",
}

if !reflect.DeepEqual(expandedId, expected) {
t.Fatalf(
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
expandedId,
expected)
}
}

func TestExpandResourceIdEmptyPart(t *testing.T) {
resourceId := "foo,,baz"
_, err := ExpandResourceId(resourceId, 3)

if !strings.Contains(err.Error(), "format for ID (foo,,baz), the following id parts indexes are blank ([1])") {
t.Fatalf("Expected an error when parsing ResourceId with an empty part")
}
}

func TestExpandResourceIdIncorrectPartCount(t *testing.T) {
resourceId := "foo,bar,baz"
_, err := ExpandResourceId(resourceId, 2)

if !strings.Contains(err.Error(), "unexpected format for ID (foo,bar,baz), expected (2) parts separated by (,)") {
t.Fatalf("Expected an error when parsing ResourceId with incorrect part count")
}
}

func TestExpandResourceIdSinglePart(t *testing.T) {
resourceId := "foo"
_, err := ExpandResourceId(resourceId, 2)

if !strings.Contains(err.Error(), "unexpected format for ID ([foo]), expected more than one part") {
t.Fatalf("Expected an error when parsing ResourceId with single part count")
}
}

func TestFlattenResourceId(t *testing.T) {
idParts := []string{"foo", "bar", "baz"}
flattenedId, _ := FlattenResourceId(idParts, 3)
expected := "foo,bar,baz"

if !reflect.DeepEqual(flattenedId, expected) {
t.Fatalf(
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
flattenedId,
expected)
}
}

func TestFlattenResourceIdEmptyPart(t *testing.T) {
idParts := []string{"foo", "", "baz"}
_, err := FlattenResourceId(idParts, 3)

if !strings.Contains(err.Error(), "unexpected format for ID parts ([foo baz]), the following id parts indexes are blank ([1])") {
t.Fatalf("Expected an error when parsing ResourceId with an empty part")
}
}

func TestFlattenResourceIdIncorrectPartCount(t *testing.T) {
idParts := []string{"foo", "bar", "baz"}
_, err := FlattenResourceId(idParts, 2)

if !strings.Contains(err.Error(), "unexpected format for ID parts ([foo bar baz]), expected (2) parts") {
t.Fatalf("Expected an error when parsing ResourceId with incorrect part count")
}
}

func TestFlattenResourceIdSinglePart(t *testing.T) {
idParts := []string{"foo"}
_, err := FlattenResourceId(idParts, 2)

if !strings.Contains(err.Error(), "unexpected format for ID parts ([foo]), expected more than one part") {
t.Fatalf("Expected an error when parsing ResourceId with single part count")
}
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,7 @@ func New(ctx context.Context) (*schema.Provider, error) {
"aws_licensemanager_license_configuration": licensemanager.ResourceLicenseConfiguration(),

"aws_lightsail_bucket": lightsail.ResourceBucket(),
"aws_lightsail_bucket_access_key": lightsail.ResourceBucketAccessKey(),
"aws_lightsail_certificate": lightsail.ResourceCertificate(),
"aws_lightsail_container_service": lightsail.ResourceContainerService(),
"aws_lightsail_container_service_deployment_version": lightsail.ResourceContainerServiceDeploymentVersion(),
Expand Down
152 changes: 152 additions & 0 deletions internal/service/lightsail/bucket_access_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package lightsail

import (
"context"
"errors"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lightsail"
"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/create"
"github.com/hashicorp/terraform-provider-aws/internal/flex"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

const (
BucketAccessKeyIdPartsCount = 2
)

func ResourceBucketAccessKey() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceBucketAccessKeyCreate,
ReadWithoutTimeout: resourceBucketAccessKeyRead,
DeleteWithoutTimeout: resourceBucketAccessKeyDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"access_key_id": {
Type: schema.TypeString,
Computed: true,
},
"bucket_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,52}[a-z0-9]$`), "Invalid Bucket name. Must match regex: ^[a-z0-9][a-z0-9-]{1,52}[a-z0-9]$"),
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"secret_access_key": {
Type: schema.TypeString,
Computed: true,
},
"status": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

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

in := lightsail.CreateBucketAccessKeyInput{
BucketName: aws.String(d.Get("bucket_name").(string)),
}

out, err := conn.CreateBucketAccessKeyWithContext(ctx, &in)

if err != nil {
return create.DiagError(names.Lightsail, lightsail.OperationTypeCreateBucketAccessKey, ResBucketAccessKey, d.Get("bucket_name").(string), err)
}

if len(out.Operations) == 0 {
return create.DiagError(names.Lightsail, lightsail.OperationTypeCreateBucketAccessKey, ResBucketAccessKey, d.Get("bucket_name").(string), errors.New("No operations found for request"))
}

op := out.Operations[0]

err = waitOperation(conn, op.Id)
if err != nil {
return create.DiagError(names.Lightsail, lightsail.OperationTypeCreateBucketAccessKey, ResBucketAccessKey, d.Get("bucket_name").(string), errors.New("Error waiting for request operation"))
}

idParts := []string{d.Get("bucket_name").(string), *out.AccessKey.AccessKeyId}
id, err := flex.FlattenResourceId(idParts, BucketAccessKeyIdPartsCount)

if err != nil {
return create.DiagError(names.Lightsail, create.ErrActionFlatteningResourceId, ResBucketAccessKey, d.Get("bucket_name").(string), err)
}

d.SetId(id)
d.Set("secret_access_key", out.AccessKey.SecretAccessKey)

return resourceBucketAccessKeyRead(ctx, d, meta)
}

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

out, err := FindBucketAccessKeyById(ctx, conn, d.Id())

if !d.IsNewResource() && tfresource.NotFound(err) {
create.LogNotFoundRemoveState(names.Lightsail, create.ErrActionReading, ResBucketAccessKey, d.Id())
d.SetId("")
return nil
}

if err != nil {
return create.DiagError(names.Lightsail, create.ErrActionReading, ResBucketAccessKey, d.Id(), err)
}

d.Set("access_key_id", out.AccessKeyId)
d.Set("bucket_name", d.Get("bucket_name").(string))
d.Set("created_at", out.CreatedAt.Format(time.RFC3339))
d.Set("status", out.Status)

return nil
}

func resourceBucketAccessKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).LightsailConn()
parts, err := flex.ExpandResourceId(d.Id(), BucketAccessKeyIdPartsCount)

if err != nil {
return create.DiagError(names.Lightsail, create.ErrActionExpandingResourceId, ResBucketAccessKey, d.Id(), err)
}

out, err := conn.DeleteBucketAccessKeyWithContext(ctx, &lightsail.DeleteBucketAccessKeyInput{
BucketName: aws.String(parts[0]),
AccessKeyId: aws.String(parts[1]),
})

if err != nil && tfawserr.ErrCodeEquals(err, lightsail.ErrCodeNotFoundException) {
return nil
}

if err != nil {
return create.DiagError(names.Lightsail, create.ErrActionDeleting, ResBucketAccessKey, d.Id(), err)
}

op := out.Operations[0]

err = waitOperation(conn, op.Id)

if err != nil {
return create.DiagError(names.Lightsail, lightsail.OperationTypeDeleteCertificate, ResBucketAccessKey, d.Id(), err)
}

return nil
}
Loading