diff --git a/aws/config.go b/aws/config.go index 03a1b69d2b6..029bd7259fd 100644 --- a/aws/config.go +++ b/aws/config.go @@ -84,6 +84,7 @@ import ( "github.com/aws/aws-sdk-go/service/organizations" "github.com/aws/aws-sdk-go/service/pinpoint" "github.com/aws/aws-sdk-go/service/pricing" + "github.com/aws/aws-sdk-go/service/ram" "github.com/aws/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/route53" @@ -149,6 +150,7 @@ type Config struct { KinesisAnalyticsEndpoint string KmsEndpoint string LambdaEndpoint string + RamEndpoint string RdsEndpoint string R53Endpoint string S3Endpoint string @@ -216,6 +218,7 @@ type AWSClient struct { accountid string supportedplatforms []string region string + ramconn *ram.RAM rdsconn *rds.RDS iamconn *iam.IAM kinesisconn *kinesis.Kinesis @@ -436,6 +439,7 @@ func (c *Config) Client() (interface{}, error) { awsKinesisSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.KinesisEndpoint)}) awsKinesisAnalyticsSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.KinesisAnalyticsEndpoint)}) awsKmsSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.KmsEndpoint)}) + awsRamSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.RamEndpoint)}) awsRdsSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.RdsEndpoint)}) awsS3Sess := sess.Copy(&aws.Config{Endpoint: aws.String(c.S3Endpoint)}) awsS3ControlSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.S3ControlEndpoint)}) @@ -570,6 +574,7 @@ func (c *Config) Client() (interface{}, error) { client.opsworksconn = opsworks.New(sess) client.organizationsconn = organizations.New(sess) client.r53conn = route53.New(r53Sess) + client.ramconn = ram.New(awsRamSess) client.rdsconn = rds.New(awsRdsSess) client.redshiftconn = redshift.New(sess) client.simpledbconn = simpledb.New(sess) diff --git a/aws/provider.go b/aws/provider.go index 074aa98995a..2cae69c1028 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -583,6 +583,7 @@ func Provider() terraform.ResourceProvider { "aws_organizations_policy_attachment": resourceAwsOrganizationsPolicyAttachment(), "aws_placement_group": resourceAwsPlacementGroup(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), + "aws_ram_resource_share": resourceAwsRamResourceShare(), "aws_rds_cluster": resourceAwsRDSCluster(), "aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), diff --git a/aws/resource_aws_ram_resource_share.go b/aws/resource_aws_ram_resource_share.go new file mode 100644 index 00000000000..9820e7c67ac --- /dev/null +++ b/aws/resource_aws_ram_resource_share.go @@ -0,0 +1,241 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRamResourceShare() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRamResourceShareCreate, + Read: resourceAwsRamResourceShareRead, + Update: resourceAwsRamResourceShareUpdate, + Delete: resourceAwsRamResourceShareDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "allow_external_principals": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsRamResourceShareCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + request := &ram.CreateResourceShareInput{ + Name: aws.String(d.Get("name").(string)), + AllowExternalPrincipals: aws.Bool(d.Get("allow_external_principals").(bool)), + } + + if v, ok := d.GetOk("tags"); ok { + tags := tagsFromMapRAM(v.(map[string]interface{})) + request.Tags = tags + } + + log.Println("[DEBUG] Create RAM resource share request:", request) + createResp, err := conn.CreateResourceShare(request) + if err != nil { + return fmt.Errorf("Error creating RAM resource share: %s", err) + } + + d.SetId(aws.StringValue(createResp.ResourceShare.ResourceShareArn)) + + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareStatusPending}, + Target: []string{ram.ResourceShareStatusActive}, + Refresh: resourceAwsRamResourceShareStateRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for RAM resource share (%s) to become ready: %s", d.Id(), err) + } + + return resourceAwsRamResourceShareRead(d, meta) +} + +func resourceAwsRamResourceShareRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + request := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(d.Id())}, + ResourceOwner: aws.String(ram.ResourceOwnerSelf), + } + + output, err := conn.GetResourceShares(request) + if err != nil { + if isAWSErr(err, ram.ErrCodeUnknownResourceException, "") { + log.Printf("[WARN] No RAM resource share by ARN (%s) found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading RAM resource share %s: %s", d.Id(), err) + } + + if len(output.ResourceShares) == 0 { + log.Printf("[WARN] No RAM resource share by ARN (%s) found, removing from state", d.Id()) + d.SetId("") + return nil + } + + resourceShare := output.ResourceShares[0] + + if aws.StringValue(resourceShare.Status) != ram.ResourceShareStatusActive { + log.Printf("[WARN] RAM resource share (%s) delet(ing|ed), removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("name", resourceShare.Name) + d.Set("allow_external_principals", resourceShare.AllowExternalPrincipals) + + if err := d.Set("tags", tagsToMapRAM(resourceShare.Tags)); err != nil { + return fmt.Errorf("Error setting tags: %s", err) + } + + return nil +} + +func resourceAwsRamResourceShareUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + d.Partial(true) + + if d.HasChange("name") || d.HasChange("allow_external_principals") { + request := &ram.UpdateResourceShareInput{ + ResourceShareArn: aws.String(d.Id()), + Name: aws.String(d.Get("name").(string)), + AllowExternalPrincipals: aws.Bool(d.Get("allow_external_principals").(bool)), + } + + log.Println("[DEBUG] Update RAM resource share request:", request) + _, err := conn.UpdateResourceShare(request) + if err != nil { + if isAWSErr(err, ram.ErrCodeUnknownResourceException, "") { + log.Printf("[WARN] No RAM resource share by ARN (%s) found", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error updating RAM resource share %s: %s", d.Id(), err) + } + + d.SetPartial("name") + d.SetPartial("allow_external_principals") + } + + if d.HasChange("tags") { + // Reset all tags to empty set + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + c, r := diffTagsRAM(tagsFromMapRAM(o), tagsFromMapRAM(n)) + + if len(r) > 0 { + _, err := conn.UntagResource(&ram.UntagResourceInput{ + ResourceShareArn: aws.String(d.Id()), + TagKeys: tagKeysRam(r), + }) + if err != nil { + return fmt.Errorf("Error deleting RAM resource share tags: %s", err) + } + } + + if len(c) > 0 { + input := &ram.TagResourceInput{ + ResourceShareArn: aws.String(d.Id()), + Tags: c, + } + _, err := conn.TagResource(input) + if err != nil { + return fmt.Errorf("Error updating RAM resource share tags: %s", err) + } + } + + d.SetPartial("tags") + } + + d.Partial(false) + + return resourceAwsRamResourceShareRead(d, meta) +} + +func resourceAwsRamResourceShareDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ramconn + + deleteResourceShareInput := &ram.DeleteResourceShareInput{ + ResourceShareArn: aws.String(d.Id()), + } + + log.Println("[DEBUG] Delete RAM resource share request:", deleteResourceShareInput) + _, err := conn.DeleteResourceShare(deleteResourceShareInput) + if err != nil { + if isAWSErr(err, ram.ErrCodeUnknownResourceException, "") { + return nil + } + return fmt.Errorf("Error deleting RAM resource share %s: %s", d.Id(), err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareStatusDeleting}, + Target: []string{ram.ResourceShareStatusDeleted}, + Refresh: resourceAwsRamResourceShareStateRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for RAM resource share (%s) to become ready: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsRamResourceShareStateRefreshFunc(conn *ram.RAM, resourceShareArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + request := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(resourceShareArn)}, + ResourceOwner: aws.String(ram.ResourceOwnerSelf), + } + + output, err := conn.GetResourceShares(request) + + if err != nil { + return nil, ram.ResourceShareStatusFailed, err + } + + if len(output.ResourceShares) == 0 { + return nil, ram.ResourceShareStatusDeleted, nil + } + + resourceShare := output.ResourceShares[0] + + return resourceShare, aws.StringValue(resourceShare.Status), nil + } +} diff --git a/aws/resource_aws_ram_resource_share_test.go b/aws/resource_aws_ram_resource_share_test.go new file mode 100644 index 00000000000..14722b2262b --- /dev/null +++ b/aws/resource_aws_ram_resource_share_test.go @@ -0,0 +1,122 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" +) + +func TestAccAwsRamResourceShare_basic(t *testing.T) { + var resourceShare ram.ResourceShare + resourceName := "aws_ram_resource_share.example" + shareName := fmt.Sprintf("tf-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsRamResourceShareConfig_basic(shareName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsRamResourceShareExists(resourceName, &resourceShare), + resource.TestCheckResourceAttr(resourceName, "name", shareName), + resource.TestCheckResourceAttr(resourceName, "allow_external_principals", "true"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Environment", "Production"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsRamResourceShareExists(resourceName string, resourceShare *ram.ResourceShare) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ramconn + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + request := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(rs.Primary.ID)}, + ResourceOwner: aws.String(ram.ResourceOwnerSelf), + } + + output, err := conn.GetResourceShares(request) + if err != nil { + return err + } + + if len(output.ResourceShares) == 0 { + return fmt.Errorf("No RAM resource share found") + } + + resourceShare = output.ResourceShares[0] + + if aws.StringValue(resourceShare.Status) != ram.ResourceShareStatusActive { + return fmt.Errorf("RAM resource share (%s) delet(ing|ed)", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAwsRamResourceShareDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ramconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ram_resource_share" { + continue + } + + request := &ram.GetResourceSharesInput{ + ResourceShareArns: []*string{aws.String(rs.Primary.ID)}, + ResourceOwner: aws.String(ram.ResourceOwnerSelf), + } + + output, err := conn.GetResourceShares(request) + if err != nil { + return err + } + + if len(output.ResourceShares) > 0 { + resourceShare := output.ResourceShares[0] + if aws.StringValue(resourceShare.Status) != ram.ResourceShareStatusDeleted { + return fmt.Errorf("RAM resource share (%s) still exists", rs.Primary.ID) + } + return fmt.Errorf("No RAM resource share found") + } + } + + return nil +} + +func testAccAwsRamResourceShareConfig_basic(shareName string) string { + return fmt.Sprintf(` +resource "aws_ram_resource_share" "example" { + name = "%s" + allow_external_principals = true + + tags { + Environment = "Production" + } +} +`, shareName) +} diff --git a/aws/tagsRAM.go b/aws/tagsRAM.go new file mode 100644 index 00000000000..5cdf464e76b --- /dev/null +++ b/aws/tagsRAM.go @@ -0,0 +1,85 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" +) + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsRAM(oldTags, newTags []*ram.Tag) ([]*ram.Tag, []*ram.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + // Build the list of what to remove + var remove []*ram.Tag + for _, t := range oldTags { + old, ok := create[aws.StringValue(t.Key)] + if !ok || old != aws.StringValue(t.Value) { + // Delete it! + remove = append(remove, t) + } else if ok { + delete(create, aws.StringValue(t.Key)) + } + } + + return tagsFromMapRAM(create), remove +} + +// tagsFromMapRAM returns the tags for the given map of data for RAM. +func tagsFromMapRAM(m map[string]interface{}) []*ram.Tag { + result := make([]*ram.Tag, 0, len(m)) + for k, v := range m { + t := &ram.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + if !tagIgnoredRAM(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMapRAM turns the list of RAM tags into a map. +func tagsToMapRAM(ts []*ram.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredRAM(t) { + result[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredRAM(t *ram.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key) + if r, _ := regexp.MatchString(v, *t.Key); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value) + return true + } + } + return false +} + +// tagKeysRam returns the keys for the list of RAM tags +func tagKeysRam(ts []*ram.Tag) []*string { + result := make([]*string, 0, len(ts)) + for _, t := range ts { + result = append(result, t.Key) + } + return result +} diff --git a/aws/tagsRAM_test.go b/aws/tagsRAM_test.go new file mode 100644 index 00000000000..a18f91ee3a6 --- /dev/null +++ b/aws/tagsRAM_test.go @@ -0,0 +1,111 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" +) + +// go test -v -run="TestDiffRAMTags" +func TestDiffRAMTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Add + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{}, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Overlap + { + Old: map[string]interface{}{ + "foo": "bar", + "hello": "world", + }, + New: map[string]interface{}{ + "foo": "baz", + "hello": "world", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Remove + { + Old: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + New: map[string]interface{}{ + "foo": "bar", + }, + Create: map[string]string{}, + Remove: map[string]string{ + "bar": "baz", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsRAM(tagsFromMapRAM(tc.Old), tagsFromMapRAM(tc.New)) + cm := tagsToMapRAM(c) + rm := tagsToMapRAM(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// go test -v -run="TestIgnoringTagsRAM" +func TestIgnoringTagsRAM(t *testing.T) { + var ignoredTags []*ram.Tag + ignoredTags = append(ignoredTags, &ram.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &ram.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredRAM(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} diff --git a/website/aws.erb b/website/aws.erb index d16a7f690d7..de7e4e448fb 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1876,6 +1876,15 @@ + > + RAM Resources + + + > RDS Resources