-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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_ssm_resource_data_sync #1895
Changes from 2 commits
e3912c2
360b110
cb588ab
926668e
879b0f8
2e1236e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package aws | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"log" | ||
"strings" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/awserr" | ||
"github.com/aws/aws-sdk-go/service/ssm" | ||
"github.com/hashicorp/terraform/helper/hashcode" | ||
"github.com/hashicorp/terraform/helper/schema" | ||
) | ||
|
||
func resourceAwsSsmResourceDataSync() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceAwsSsmResourceDataSyncCreate, | ||
Read: resourceAwsSsmResourceDataSyncRead, | ||
Delete: resourceAwsSsmResourceDataSyncDelete, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"name": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"destination": { | ||
Type: schema.TypeSet, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is a singleton I think we can just make it |
||
Required: true, | ||
ForceNew: true, | ||
MaxItems: 1, | ||
Elem: &schema.Resource{ | ||
Schema: map[string]*schema.Schema{ | ||
"key": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above - |
||
Type: schema.TypeString, | ||
Optional: true, | ||
}, | ||
"bucket": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above - |
||
Type: schema.TypeString, | ||
Required: true, | ||
}, | ||
"prefix": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
}, | ||
"region": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
Set: func(v interface{}) int { | ||
var buf bytes.Buffer | ||
m := v.(map[string]interface{}) | ||
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["bucket"].(string)))) | ||
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["region"].(string)))) | ||
if val, ok := m["key"]; ok { | ||
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(val.(string)))) | ||
} | ||
if val, ok := m["prefix"]; ok { | ||
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(val.(string)))) | ||
} | ||
return hashcode.String(buf.String()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please explain why do we need to lowercase all the values here? It seems unnecessary to me. 🤔 |
||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceAwsSsmResourceDataSyncCreate(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).ssmconn | ||
|
||
input := &ssm.CreateResourceDataSyncInput{ | ||
S3Destination: &ssm.ResourceDataSyncS3Destination{ | ||
SyncFormat: aws.String(ssm.ResourceDataSyncS3FormatJsonSerDe), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps a nitpick, but how do you feel about exposing this argument via schema too? It would help us to be prepared for time when AWS introduces new sync format(s). |
||
}, | ||
SyncName: aws.String(d.Get("name").(string)), | ||
} | ||
destination := d.Get("destination").(*schema.Set).List() | ||
for _, v := range destination { | ||
dest := v.(map[string]interface{}) | ||
input.S3Destination.SetBucketName(dest["bucket"].(string)) | ||
input.S3Destination.SetRegion(dest["region"].(string)) | ||
if val, ok := dest["key"].(string); ok && val != "" { | ||
input.S3Destination.SetAWSKMSKeyARN(val) | ||
} | ||
if val, ok := dest["prefix"].(string); ok && val != "" { | ||
input.S3Destination.SetPrefix(val) | ||
} | ||
} | ||
|
||
_, err := conn.CreateResourceDataSync(input) | ||
if err != nil { | ||
if aerr, ok := err.(awserr.Error); ok { | ||
switch aerr.Code() { | ||
case ssm.ErrCodeResourceDataSyncAlreadyExistsException: | ||
if err := resourceAwsSsmResourceDataSyncDeleteAndCreate(meta, input); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please explain why we should ever recreate the resource? The common convention (and user's expectations) is to just error out in case there's a conflict. |
||
return err | ||
} | ||
default: | ||
return err | ||
} | ||
} else { | ||
return err | ||
} | ||
} | ||
d.SetId(d.Get("name").(string)) | ||
return resourceAwsSsmResourceDataSyncRead(d, meta) | ||
} | ||
|
||
func resourceAwsSsmResourceDataSyncRead(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).ssmconn | ||
|
||
nextToken := "" | ||
found := false | ||
for { | ||
input := &ssm.ListResourceDataSyncInput{} | ||
if nextToken != "" { | ||
input.NextToken = aws.String(nextToken) | ||
} | ||
resp, err := conn.ListResourceDataSync(input) | ||
if err != nil { | ||
return err | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm slightly worried about this conditional here, because we may add more errors to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure👍 I'll apply that idea! |
||
for _, v := range resp.ResourceDataSyncItems { | ||
if *v.SyncName == d.Get("name").(string) { | ||
found = true | ||
} | ||
} | ||
if found || *resp.NextToken == "" { | ||
break | ||
} | ||
nextToken = *resp.NextToken | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind moving this for loop into a separate function, e.g. |
||
if !found { | ||
log.Printf("[INFO] No Resource Data Sync found for SyncName: %s", d.Get("name").(string)) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a few things missing here - we need to re-set all the fields here based on what came from the API, e.g. d.Set("s3_destination", flattenS3Destination(ds.S3Destination)) where the flattener will turn the SDK object into and secondly if there's no sync found we should remove it from state by calling |
||
return nil | ||
} | ||
|
||
func resourceAwsSsmResourceDataSyncDelete(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*AWSClient).ssmconn | ||
|
||
input := &ssm.DeleteResourceDataSyncInput{ | ||
SyncName: aws.String(d.Get("name").(string)), | ||
} | ||
|
||
_, err := conn.DeleteResourceDataSync(input) | ||
if err != nil { | ||
if aerr, ok := err.(awserr.Error); ok { | ||
switch aerr.Code() { | ||
case ssm.ErrCodeResourceDataSyncNotFoundException: | ||
return nil | ||
default: | ||
return err | ||
} | ||
} | ||
return err | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of the above can be IMO simplified to something like this: if isAWSErr(err, ssm.ErrCodeResourceDataSyncNotFoundException, "") {
return nil
}
return err |
||
return nil | ||
} | ||
|
||
func resourceAwsSsmResourceDataSyncDeleteAndCreate(meta interface{}, input *ssm.CreateResourceDataSyncInput) error { | ||
conn := meta.(*AWSClient).ssmconn | ||
|
||
delinput := &ssm.DeleteResourceDataSyncInput{ | ||
SyncName: input.SyncName, | ||
} | ||
|
||
_, err := conn.DeleteResourceDataSync(delinput) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = conn.CreateResourceDataSync(input) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package aws | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"testing" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/service/ssm" | ||
"github.com/hashicorp/terraform/helper/acctest" | ||
"github.com/hashicorp/terraform/helper/resource" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
func TestAccAWSSsmResourceDataSync(t *testing.T) { | ||
rInt := acctest.RandInt() | ||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
CheckDestroy: testAccCheckAWSSsmResourceDataSyncDestroy, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccSsmResourceDataSyncConfig(rInt), | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckAWSSsmResourceDataSyncExists("aws_ssm_resource_data_sync.foo"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckAWSSsmResourceDataSyncDestroy(s *terraform.State) error { | ||
conn := testAccProvider.Meta().(*AWSClient).ssmconn | ||
|
||
for _, rs := range s.RootModule().Resources { | ||
if rs.Type != "aws_ssm_resource_data_sync" { | ||
continue | ||
} | ||
nextToken := "" | ||
found := false | ||
for { | ||
input := &ssm.ListResourceDataSyncInput{} | ||
if nextToken != "" { | ||
input.NextToken = aws.String(nextToken) | ||
} | ||
resp, err := conn.ListResourceDataSync(input) | ||
if err != nil { | ||
return err | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ I share the same concerns here as above |
||
for _, v := range resp.ResourceDataSyncItems { | ||
if *v.SyncName == rs.Primary.Attributes["name"] { | ||
found = true | ||
} | ||
} | ||
if resp.NextToken != nil { | ||
nextToken = *resp.NextToken | ||
} | ||
if found || nextToken == "" { | ||
break | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once you have decoupled the logic for finding the right data sync, it can also be reused here 😉 |
||
if found { | ||
return fmt.Errorf("[DELETE ERROR] Resource Data Sync found for SyncName: %s", rs.Primary.Attributes["name"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick, but there's no reason to prefix the message with |
||
} | ||
} | ||
return nil | ||
} | ||
|
||
func testAccCheckAWSSsmResourceDataSyncExists(name string) resource.TestCheckFunc { | ||
return func(s *terraform.State) error { | ||
log.Println(s.RootModule().Resources) | ||
_, ok := s.RootModule().Resources[name] | ||
if !ok { | ||
return fmt.Errorf("Not found: %s", name) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
func testAccSsmResourceDataSyncConfig(randInt int) string { | ||
return fmt.Sprintf(` | ||
resource "aws_s3_bucket" "hoge" { | ||
bucket = "tf-test-bucket-%d" | ||
region = "us-east-1" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason why this bucket should be in |
||
force_destroy = true | ||
} | ||
|
||
resource "aws_s3_bucket_policy" "hoge" { | ||
bucket = "${aws_s3_bucket.hoge.bucket}" | ||
policy = <<EOF | ||
{ | ||
"Version": "2012-10-17", | ||
"Statement": [ | ||
{ | ||
"Sid": "SSMBucketPermissionsCheck", | ||
"Effect": "Allow", | ||
"Principal": { | ||
"Service": "ssm.amazonaws.com" | ||
}, | ||
"Action": "s3:GetBucketAcl", | ||
"Resource": "arn:aws:s3:::tf-test-bucket-%d" | ||
}, | ||
{ | ||
"Sid": " SSMBucketDelivery", | ||
"Effect": "Allow", | ||
"Principal": { | ||
"Service": "ssm.amazonaws.com" | ||
}, | ||
"Action": "s3:PutObject", | ||
"Resource": ["arn:aws:s3:::tf-test-bucket-%d/*"], | ||
"Condition": { | ||
"StringEquals": { | ||
"s3:x-amz-acl": "bucket-owner-full-control" | ||
} | ||
} | ||
} | ||
] | ||
} | ||
EOF | ||
} | ||
|
||
resource "aws_ssm_resource_data_sync" "foo" { | ||
name = "foo" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind randomizing the name here? |
||
destination = { | ||
bucket = "${aws_s3_bucket.hoge.bucket}" | ||
region = "${aws_s3_bucket.hoge.region}" | ||
} | ||
} | ||
`, randInt, randInt, randInt) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason we should drift away from the API here? i.e. how do you feel about renaming this to
s3_destination
?