Skip to content

Commit

Permalink
Merge pull request #12370 from thehunt33r/Add-db_cluster_role_associa…
Browse files Browse the repository at this point in the history
…tion_resource

r/aws_rds_cluster_role_association - New resource
  • Loading branch information
ewbankkit authored Jul 6, 2021
2 parents b50f66a + 7bd40b6 commit 54d77ae
Show file tree
Hide file tree
Showing 12 changed files with 549 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .changelog/12370.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:new-resource
aws_rds_cluster_role_association
```

```release-note:enhancement
aws_rds_cluster: Set `iam_roles` as Computed to prevent drift when the `aws_rds_cluster_role_association` resource is used
```
7 changes: 7 additions & 0 deletions aws/internal/service/rds/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package rds

const (
DBClusterRoleStatusActive = "ACTIVE"
DBClusterRoleStatusDeleted = "DELETED"
DBClusterRoleStatusPending = "PENDING"
)
57 changes: 57 additions & 0 deletions aws/internal/service/rds/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package finder
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds"
)

Expand Down Expand Up @@ -63,3 +65,58 @@ func DBProxyEndpoint(conn *rds.RDS, id string) (*rds.DBProxyEndpoint, error) {

return dbProxyEndpoint, err
}

func DBClusterRoleByDBClusterIDAndRoleARN(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) {
dbCluster, err := DBClusterByID(conn, dbClusterID)

if err != nil {
return nil, err
}

for _, associatedRole := range dbCluster.AssociatedRoles {
if aws.StringValue(associatedRole.RoleArn) == roleARN {
if status := aws.StringValue(associatedRole.Status); status == tfrds.DBClusterRoleStatusDeleted {
return nil, &resource.NotFoundError{
Message: status,
}
}

return associatedRole, nil
}
}

return nil, &resource.NotFoundError{}
}

func DBClusterByID(conn *rds.RDS, id string) (*rds.DBCluster, error) {
input := &rds.DescribeDBClustersInput{
DBClusterIdentifier: aws.String(id),
}

output, err := conn.DescribeDBClusters(input)

if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterNotFoundFault) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if output == nil || len(output.DBClusters) == 0 || output.DBClusters[0] == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

dbCluster := output.DBClusters[0]

// Eventual consistency check.
if aws.StringValue(dbCluster.DBClusterIdentifier) != id {
return nil, &resource.NotFoundError{
LastRequest: input,
}
}

return dbCluster, nil
}
19 changes: 19 additions & 0 deletions aws/internal/service/rds/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@ func ResourceAwsDbProxyEndpointParseID(id string) (string, string, error) {
}
return idParts[0], idParts[1], nil
}

const clusterRoleAssociationResourceIDSeparator = ","

func ClusterRoleAssociationCreateResourceID(dbClusterID, roleARN string) string {
parts := []string{dbClusterID, roleARN}
id := strings.Join(parts, clusterRoleAssociationResourceIDSeparator)

return id
}

func ClusterRoleAssociationParseResourceID(id string) (string, string, error) {
parts := strings.Split(id, clusterRoleAssociationResourceIDSeparator)

if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
return parts[0], parts[1], nil
}

return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected DBCLUSTERID%[2]sROLEARN", id, clusterRoleAssociationResourceIDSeparator)
}
17 changes: 17 additions & 0 deletions aws/internal/service/rds/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

const (
Expand Down Expand Up @@ -58,3 +59,19 @@ func DBProxyEndpointStatus(conn *rds.RDS, id string) resource.StateRefreshFunc {
return output, aws.StringValue(output.Status), nil
}
}

func DBClusterRoleStatus(conn *rds.RDS, dbClusterID, roleARN string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.Status), nil
}
}
38 changes: 38 additions & 0 deletions aws/internal/service/rds/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (

"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds"
)

const (
// Maximum amount of time to wait for an EventSubscription to return Deleted
EventSubscriptionDeletedTimeout = 10 * time.Minute
RdsClusterInitiateUpgradeTimeout = 5 * time.Minute

DBClusterRoleAssociationCreatedTimeout = 5 * time.Minute
DBClusterRoleAssociationDeletedTimeout = 5 * time.Minute
)

// EventSubscriptionDeleted waits for a EventSubscription to return Deleted
Expand Down Expand Up @@ -69,3 +73,37 @@ func DBProxyEndpointDeleted(conn *rds.RDS, id string, timeout time.Duration) (*r

return nil, err
}

func DBClusterRoleAssociationCreated(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{tfrds.DBClusterRoleStatusPending},
Target: []string{tfrds.DBClusterRoleStatusActive},
Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN),
Timeout: DBClusterRoleAssociationCreatedTimeout,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*rds.DBClusterRole); ok {
return output, err
}

return nil, err
}

func DBClusterRoleAssociationDeleted(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{tfrds.DBClusterRoleStatusActive, tfrds.DBClusterRoleStatusPending},
Target: []string{},
Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN),
Timeout: DBClusterRoleAssociationDeletedTimeout,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*rds.DBClusterRole); ok {
return output, err
}

return nil, err
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ func Provider() *schema.Provider {
"aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(),
"aws_rds_cluster_instance": resourceAwsRDSClusterInstance(),
"aws_rds_cluster_parameter_group": resourceAwsRDSClusterParameterGroup(),
"aws_rds_cluster_role_association": resourceAwsRDSClusterRoleAssociation(),
"aws_rds_global_cluster": resourceAwsRDSGlobalCluster(),
"aws_redshift_cluster": resourceAwsRedshiftCluster(),
"aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(),
Expand Down
1 change: 1 addition & 0 deletions aws/resource_aws_rds_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ func resourceAwsRDSCluster() *schema.Resource {
"iam_roles": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
Expand Down
138 changes: 138 additions & 0 deletions aws/resource_aws_rds_cluster_role_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package aws

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsRDSClusterRoleAssociation() *schema.Resource {
return &schema.Resource{
Create: resourceAwsRDSClusterRoleAssociationCreate,
Read: resourceAwsRDSClusterRoleAssociationRead,
Delete: resourceAwsRDSClusterRoleAssociationDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"db_cluster_identifier": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"feature_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"role_arn": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateArn,
},
},
}
}

func resourceAwsRDSClusterRoleAssociationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn

dbClusterID := d.Get("db_cluster_identifier").(string)
roleARN := d.Get("role_arn").(string)
input := &rds.AddRoleToDBClusterInput{
DBClusterIdentifier: aws.String(dbClusterID),
FeatureName: aws.String(d.Get("feature_name").(string)),
RoleArn: aws.String(roleARN),
}

log.Printf("[DEBUG] Creating RDS DB Cluster IAM Role Association: %s", input)
_, err := conn.AddRoleToDBCluster(input)

if err != nil {
return fmt.Errorf("error creating RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err)
}

d.SetId(tfrds.ClusterRoleAssociationCreateResourceID(dbClusterID, roleARN))

_, err = waiter.DBClusterRoleAssociationCreated(conn, dbClusterID, roleARN)

if err != nil {
return fmt.Errorf("error waiting for RDS DB Cluster (%s) IAM Role (%s) Association to create: %w", dbClusterID, roleARN, err)
}

return resourceAwsRDSClusterRoleAssociationRead(d, meta)
}

func resourceAwsRDSClusterRoleAssociationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn

dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(d.Id())

if err != nil {
return fmt.Errorf("error parsing RDS DB Cluster IAM Role Association ID: %s", err)
}

output, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] RDS DB Cluster (%s) IAM Role (%s) Association not found, removing from state", dbClusterID, roleARN)
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err)
}

d.Set("db_cluster_identifier", dbClusterID)
d.Set("feature_name", output.FeatureName)
d.Set("role_arn", output.RoleArn)

return nil
}

func resourceAwsRDSClusterRoleAssociationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).rdsconn

dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(d.Id())

if err != nil {
return fmt.Errorf("error parsing RDS DB Cluster IAM Role Association ID: %s", err)
}

input := &rds.RemoveRoleFromDBClusterInput{
DBClusterIdentifier: aws.String(dbClusterID),
FeatureName: aws.String(d.Get("feature_name").(string)),
RoleArn: aws.String(roleARN),
}

log.Printf("[DEBUG] Deleting RDS DB Cluster IAM Role Association: %s", d.Id())
_, err = conn.RemoveRoleFromDBCluster(input)

if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterNotFoundFault) || tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterRoleNotFoundFault) {
return nil
}

if err != nil {
return fmt.Errorf("error deleting RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err)
}

_, err = waiter.DBClusterRoleAssociationDeleted(conn, dbClusterID, roleARN)

if err != nil {
return fmt.Errorf("error waiting for RDS DB Cluster (%s) IAM Role (%s) Association to delete: %w", dbClusterID, roleARN, err)
}

return nil
}
Loading

0 comments on commit 54d77ae

Please sign in to comment.