diff --git a/cloud/aws/deploy/bucket.go b/cloud/aws/deploy/bucket.go index 6dbabf62c..1bfa65a28 100644 --- a/cloud/aws/deploy/bucket.go +++ b/cloud/aws/deploy/bucket.go @@ -18,7 +18,10 @@ package deploy import ( "fmt" + "regexp" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/nitrictech/nitric/cloud/common/deploy/resources" common "github.com/nitrictech/nitric/cloud/common/deploy/tags" deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1" @@ -103,13 +106,95 @@ func createNotification(ctx *pulumi.Context, name string, args *S3NotificationAr return notification, nil } +// extractBucketName - extracts the bucket name from an S3 ARN. +func extractBucketName(arn string) (string, error) { + s3ArnRegex := regexp.MustCompile(`(?i)^arn:aws:s3:::([^/]+)`) + + matches := s3ArnRegex.FindStringSubmatch(arn) + if len(matches) < 2 { + return "", fmt.Errorf("invalid S3 bucket ARN: %s", arn) + } + + bucketName := matches[1] + + if bucketName == "" { + return "", fmt.Errorf("invalid S3 bucket ARN: bucket name could not be extracted from %s", arn) + } + + return bucketName, nil +} + +// importBucket - tags an existing bucket in AWS and adds it to the stack. +func importBucket(ctx *pulumi.Context, name string, importIdentifier string, opts []pulumi.ResourceOption, tags map[string]string, tagClient *resourcegroupstaggingapi.ResourceGroupsTaggingAPI) (*s3.Bucket, error) { + // Allow bucket names or ARNs as import identifiers + bucketName, err := extractBucketName(importIdentifier) + if err != nil { + bucketName = importIdentifier + } + + bucketLookup, err := s3.LookupBucket(ctx, &s3.LookupBucketArgs{ + Bucket: bucketName, + }) + if err != nil { + return nil, fmt.Errorf("unable to lookup imported S3 bucket %s: %w", bucketName, err) + } + + _, err = tagClient.TagResources(&resourcegroupstaggingapi.TagResourcesInput{ + ResourceARNList: aws.StringSlice([]string{bucketLookup.Arn}), + Tags: aws.StringMap(tags), + }) + if err != nil { + return nil, fmt.Errorf("unable to tag imported S3 bucket %s: %w", bucketName, err) + } + + // nitric didn't create this resource, so it shouldn't delete it either. + allOpts := append(opts, pulumi.RetainOnDelete(true)) + + bucket, err := s3.GetBucket( + ctx, + name, + pulumi.ID(bucketLookup.Id), + nil, + allOpts..., + ) + if err != nil { + return nil, fmt.Errorf("unable to import S3 bucket %s: %w", bucketName, err) + } + + return bucket, nil +} + +// createBucket - creates a new S3 bucket in AWS and tags it. +func createBucket(ctx *pulumi.Context, name string, opts []pulumi.ResourceOption, tags map[string]string) (*s3.Bucket, error) { + bucket, err := s3.NewBucket(ctx, name, &s3.BucketArgs{ + Tags: pulumi.ToStringMap(tags), + }, opts...) + if err != nil { + return nil, err + } + + return bucket, nil +} + // Bucket - Implements deployments of Nitric Buckets using AWS S3 func (a *NitricAwsPulumiProvider) Bucket(ctx *pulumi.Context, parent pulumi.Resource, name string, config *deploymentspb.Bucket) error { opts := []pulumi.ResourceOption{pulumi.Parent(parent)} + tags := common.Tags(a.StackId, name, resources.Bucket) + + var err error + var bucket *s3.Bucket + + importArn := "" + if a.AwsConfig.Import.Buckets != nil { + importArn = a.AwsConfig.Import.Buckets[name] + } + + if importArn != "" { + bucket, err = importBucket(ctx, name, importArn, opts, tags, a.ResourceTaggingClient) + } else { + bucket, err = createBucket(ctx, name, opts, tags) + } - bucket, err := s3.NewBucket(ctx, name, &s3.BucketArgs{ - Tags: pulumi.ToStringMap(common.Tags(a.StackId, name, resources.Bucket)), - }, opts...) if err != nil { return err } diff --git a/cloud/aws/deploy/config.go b/cloud/aws/deploy/config.go index 7d15f27cc..43c5d93ec 100644 --- a/cloud/aws/deploy/config.go +++ b/cloud/aws/deploy/config.go @@ -26,9 +26,11 @@ type ApiConfig struct { Domains []string } +// AwsImports - Import configuration for AWS, maps nitric names of resources to the ARNs of existing AWS resources. type AwsImports struct { // A map of nitric names to ARNs Secrets map[string]string + Buckets map[string]string } type AwsConfig struct { diff --git a/cloud/aws/deploy/deploy.go b/cloud/aws/deploy/deploy.go index 857b5f474..7e7bd5607 100644 --- a/cloud/aws/deploy/deploy.go +++ b/cloud/aws/deploy/deploy.go @@ -144,6 +144,7 @@ func (a *NitricAwsPulumiProvider) Pre(ctx *pulumi.Context, resources []*pulumix. a.StackId = <-stackIdChan sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Region: aws.String(a.Region)}, SharedConfigState: session.SharedConfigEnable, })) diff --git a/cloud/aws/deploy/secret.go b/cloud/aws/deploy/secret.go index 563a7a8f9..a3acf27b4 100644 --- a/cloud/aws/deploy/secret.go +++ b/cloud/aws/deploy/secret.go @@ -26,8 +26,8 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// tagSecret - tags an existing secret in AWS and adds it to the stack. -func tagSecret(ctx *pulumi.Context, name string, importArn string, tags map[string]string, client *resourcegroupstaggingapi.ResourceGroupsTaggingAPI) (*secretsmanager.Secret, error) { +// importSecret - tags an existing secret in AWS and adds it to the stack. +func importSecret(ctx *pulumi.Context, name string, importArn string, tags map[string]string, tagClient *resourcegroupstaggingapi.ResourceGroupsTaggingAPI) (*secretsmanager.Secret, error) { secretLookup, err := secretsmanager.LookupSecret(ctx, &secretsmanager.LookupSecretArgs{ Arn: aws.String(importArn), }) @@ -35,7 +35,7 @@ func tagSecret(ctx *pulumi.Context, name string, importArn string, tags map[stri return nil, err } - _, err = client.TagResources(&resourcegroupstaggingapi.TagResourcesInput{ + _, err = tagClient.TagResources(&resourcegroupstaggingapi.TagResourcesInput{ ResourceARNList: aws.StringSlice([]string{secretLookup.Arn}), Tags: aws.StringMap(tags), }) @@ -82,7 +82,7 @@ func (a *NitricAwsPulumiProvider) Secret(ctx *pulumi.Context, parent pulumi.Reso } if importArn != "" { - secret, err = tagSecret(ctx, name, importArn, awsTags, a.ResourceTaggingClient) + secret, err = importSecret(ctx, name, importArn, awsTags, a.ResourceTaggingClient) } else { secret, err = createSecret(ctx, name, awsTags) }