diff --git a/builtin/providers/aws/diff_aws_policy_test.go b/builtin/providers/aws/diff_aws_policy_test.go new file mode 100644 index 000000000..ae06c26aa --- /dev/null +++ b/builtin/providers/aws/diff_aws_policy_test.go @@ -0,0 +1,38 @@ +package aws + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/jen20/awspolicyequivalence" +) + +func testAccCheckAwsPolicyMatch(resource, attr, expectedPolicy string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + given, ok := rs.Primary.Attributes[attr] + if !ok { + return fmt.Errorf("Attribute %q not found for %q", attr, resource) + } + + areEquivalent, err := awspolicy.PoliciesAreEquivalent(given, expectedPolicy) + if err != nil { + return fmt.Errorf("Comparing AWS Policies failed: %s", err) + } + + if !areEquivalent { + return fmt.Errorf("AWS policies differ.\nGiven: %s\nExpected: %s", given, expectedPolicy) + } + + return nil + } +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 74801523c..48a3061de 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -278,6 +278,7 @@ func Provider() terraform.ResourceProvider { "aws_elastic_beanstalk_configuration_template": resourceAwsElasticBeanstalkConfigurationTemplate(), "aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(), "aws_elasticsearch_domain": resourceAwsElasticSearchDomain(), + "aws_elasticsearch_domain_policy": resourceAwsElasticSearchDomainPolicy(), "aws_elastictranscoder_pipeline": resourceAwsElasticTranscoderPipeline(), "aws_elastictranscoder_preset": resourceAwsElasticTranscoderPreset(), "aws_elb": resourceAwsElb(), diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain.go b/builtin/providers/aws/resource_aws_elasticsearch_domain.go index 683e75579..bf79cf093 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain.go @@ -25,6 +25,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource { "access_policies": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ValidateFunc: validateJsonString, DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, }, diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain_policy.go b/builtin/providers/aws/resource_aws_elasticsearch_domain_policy.go new file mode 100644 index 000000000..dfb22c64d --- /dev/null +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain_policy.go @@ -0,0 +1,127 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsElasticSearchDomainPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsElasticSearchDomainPolicyUpsert, + Read: resourceAwsElasticSearchDomainPolicyRead, + Update: resourceAwsElasticSearchDomainPolicyUpsert, + Delete: resourceAwsElasticSearchDomainPolicyDelete, + + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Required: true, + }, + "access_policies": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, + }, + }, + } +} + +func resourceAwsElasticSearchDomainPolicyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + name := d.Get("domain_name").(string) + out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(name), + }) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFound" { + log.Printf("[WARN] ElasticSearch Domain %q not found, removing", name) + d.SetId("") + return nil + } + return err + } + + log.Printf("[DEBUG] Received ElasticSearch domain: %s", out) + + ds := out.DomainStatus + d.Set("access_policies", ds.AccessPolicies) + + return nil +} + +func resourceAwsElasticSearchDomainPolicyUpsert(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + domainName := d.Get("domain_name").(string) + _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ + DomainName: aws.String(domainName), + AccessPolicies: aws.String(d.Get("access_policies").(string)), + }) + if err != nil { + return err + } + + d.SetId("esd-policy-" + domainName) + + err = resource.Retry(50*time.Minute, func() *resource.RetryError { + out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + }) + if err != nil { + return resource.NonRetryableError(err) + } + + if *out.DomainStatus.Processing == false { + return nil + } + + return resource.RetryableError( + fmt.Errorf("%q: Timeout while waiting for changes to be processed", d.Id())) + }) + if err != nil { + return err + } + + return resourceAwsElasticSearchDomainPolicyRead(d, meta) +} + +func resourceAwsElasticSearchDomainPolicyDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).esconn + + _, err := conn.UpdateElasticsearchDomainConfig(&elasticsearch.UpdateElasticsearchDomainConfigInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + AccessPolicies: aws.String(""), + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Waiting for ElasticSearch domain policy %q to be deleted", d.Get("domain_name").(string)) + err = resource.Retry(60*time.Minute, func() *resource.RetryError { + out, err := conn.DescribeElasticsearchDomain(&elasticsearch.DescribeElasticsearchDomainInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + }) + if err != nil { + return resource.NonRetryableError(err) + } + + if *out.DomainStatus.Processing == false { + return nil + } + + return resource.RetryableError( + fmt.Errorf("%q: Timeout while waiting for policy to be deleted", d.Id())) + }) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain_policy_test.go b/builtin/providers/aws/resource_aws_elasticsearch_domain_policy_test.go new file mode 100644 index 000000000..76f650f6c --- /dev/null +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain_policy_test.go @@ -0,0 +1,97 @@ +package aws + +import ( + "fmt" + "testing" + + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSElasticSearchDomainPolicy_basic(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() + policy := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "es:*", + "Principal": "*", + "Effect": "Allow", + "Condition": { + "IpAddress": {"aws:SourceIp": "127.0.0.1/32"} + }, + "Resource": "${aws_elasticsearch_domain.example.arn}" + } + ] +}` + expectedPolicyTpl := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "es:*", + "Principal": "*", + "Effect": "Allow", + "Condition": { + "IpAddress": {"aws:SourceIp": "127.0.0.1/32"} + }, + "Resource": "%s" + } + ] +}` + name := fmt.Sprintf("tf-test-%d", ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccESDomainPolicyConfig(ri, policy), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + resource.TestCheckResourceAttr("aws_elasticsearch_domain.example", "elasticsearch_version", "2.3"), + func(s *terraform.State) error { + awsClient := testAccProvider.Meta().(*AWSClient) + expectedArn, err := buildESDomainArn(name, awsClient.partition, awsClient.accountid, awsClient.region) + if err != nil { + return err + } + expectedPolicy := fmt.Sprintf(expectedPolicyTpl, expectedArn) + + return testAccCheckAwsPolicyMatch("aws_elasticsearch_domain_policy.main", "access_policies", expectedPolicy)(s) + }, + ), + }, + }, + }) +} + +func buildESDomainArn(name, partition, accId, region string) (string, error) { + if partition == "" { + return "", fmt.Errorf("Unable to construct ES Domain ARN because of missing AWS partition") + } + if accId == "" { + return "", fmt.Errorf("Unable to construct ES Domain ARN because of missing AWS Account ID") + } + // arn:aws:es:us-west-2:187416307283:domain/example-name + return fmt.Sprintf("arn:%s:es:%s:%s:domain/%s", partition, region, accId, name), nil +} + +func testAccESDomainPolicyConfig(randInt int, policy string) string { + return fmt.Sprintf(` +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-%d" + elasticsearch_version = "2.3" +} + +resource "aws_elasticsearch_domain_policy" "main" { + domain_name = "${aws_elasticsearch_domain.example.domain_name}" + access_policies = <aws_elasticsearch_domain + > + aws_elasticsearch_domain_policy + +