From e4899de13e33ead55b6fffbf80dd452bafbbeae9 Mon Sep 17 00:00:00 2001 From: Paul Morton Date: Tue, 6 Jun 2017 01:55:25 -0700 Subject: [PATCH] provider/aws: New SSM Parameter resource (#15035) * New SSM Parameter resource Can be used for creating parameters in AWS' SSM Parameter Store that can then be used by other applications that have access to AWS and necessary IAM permissions. * Add docs for new SSM Parameter resource * Code Review and Bug Hunt and KMS Key - Addressed all issues in #14043 - Added ForceNew directive to type - Added the ability to specify a KMS key for encryption and decryption * Add SSM Parameter Data Source * Fix bad merge * Fix SSM Parameter Integration Tests * docs/aws: Fix typo in SSM sidebar link --- .../aws/data_source_aws_ssm_parameter.go | 63 ++++++ .../aws/data_source_aws_ssm_parameter_test.go | 42 ++++ builtin/providers/aws/provider.go | 2 + .../aws/resource_aws_ssm_parameter.go | 128 +++++++++++ .../aws/resource_aws_ssm_parameter_test.go | 210 ++++++++++++++++++ builtin/providers/aws/validators.go | 14 ++ builtin/providers/aws/validators_test.go | 26 +++ .../aws/d/ssm_parameter.html.markdown | 37 +++ .../aws/r/ssm_parameter.html.markdown | 65 ++++++ website/source/layouts/aws.erb | 6 + 10 files changed, 593 insertions(+) create mode 100644 builtin/providers/aws/data_source_aws_ssm_parameter.go create mode 100644 builtin/providers/aws/data_source_aws_ssm_parameter_test.go create mode 100644 builtin/providers/aws/resource_aws_ssm_parameter.go create mode 100644 builtin/providers/aws/resource_aws_ssm_parameter_test.go create mode 100644 website/source/docs/providers/aws/d/ssm_parameter.html.markdown create mode 100644 website/source/docs/providers/aws/r/ssm_parameter.html.markdown diff --git a/builtin/providers/aws/data_source_aws_ssm_parameter.go b/builtin/providers/aws/data_source_aws_ssm_parameter.go new file mode 100644 index 000000000..388366686 --- /dev/null +++ b/builtin/providers/aws/data_source_aws_ssm_parameter.go @@ -0,0 +1,63 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceAwsSsmParameter() *schema.Resource { + return &schema.Resource{ + Read: dataAwsSsmParameterRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func dataAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id()) + + paramInput := &ssm.GetParametersInput{ + Names: []*string{ + aws.String(d.Get("name").(string)), + }, + WithDecryption: aws.Bool(true), + } + + resp, err := ssmconn.GetParameters(paramInput) + + if err != nil { + return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err) + } + + if len(resp.InvalidParameters) > 0 { + return fmt.Errorf("[ERROR] SSM Parameter %s is invalid", d.Get("name").(string)) + } + + param := resp.Parameters[0] + d.SetId(*param.Name) + d.Set("name", param.Name) + d.Set("type", param.Type) + d.Set("value", param.Value) + + return nil +} diff --git a/builtin/providers/aws/data_source_aws_ssm_parameter_test.go b/builtin/providers/aws/data_source_aws_ssm_parameter_test.go new file mode 100644 index 000000000..3b9c0d0f4 --- /dev/null +++ b/builtin/providers/aws/data_source_aws_ssm_parameter_test.go @@ -0,0 +1,42 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAwsSsmParameterDataSource_basic(t *testing.T) { + name := "test.parameter" + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsSsmParameterDataSourceConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "name", name), + resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "type", "String"), + resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "value", "TestValue"), + ), + }, + }, + }) +} + +func testAccCheckAwsSsmParameterDataSourceConfig(name string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = "%s" + type = "String" + value = "TestValue" +} + +data "aws_ssm_parameter" "test" { + name = "${aws_ssm_parameter.test.name}" +} +`, name) +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 4e2ebf764..0a9619297 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -203,6 +203,7 @@ func Provider() terraform.ResourceProvider { "aws_route53_zone": dataSourceAwsRoute53Zone(), "aws_s3_bucket_object": dataSourceAwsS3BucketObject(), "aws_sns_topic": dataSourceAwsSnsTopic(), + "aws_ssm_parameter": dataSourceAwsSsmParameter(), "aws_subnet": dataSourceAwsSubnet(), "aws_subnet_ids": dataSourceAwsSubnetIDs(), "aws_security_group": dataSourceAwsSecurityGroup(), @@ -433,6 +434,7 @@ func Provider() terraform.ResourceProvider { "aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(), "aws_ssm_patch_baseline": resourceAwsSsmPatchBaseline(), "aws_ssm_patch_group": resourceAwsSsmPatchGroup(), + "aws_ssm_parameter": resourceAwsSsmParameter(), "aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(), "aws_spot_instance_request": resourceAwsSpotInstanceRequest(), "aws_spot_fleet_request": resourceAwsSpotFleetRequest(), diff --git a/builtin/providers/aws/resource_aws_ssm_parameter.go b/builtin/providers/aws/resource_aws_ssm_parameter.go new file mode 100644 index 000000000..16b44bebd --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_parameter.go @@ -0,0 +1,128 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsSsmParameter() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSsmParameterCreate, + Read: resourceAwsSsmParameterRead, + Update: resourceAwsSsmParameterUpdate, + Delete: resourceAwsSsmParameterDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateSsmParameterType, + }, + "value": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + "key_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsSsmParameterCreate(d *schema.ResourceData, meta interface{}) error { + return putAwsSSMParameter(d, meta) +} + +func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id()) + + paramInput := &ssm.GetParametersInput{ + Names: []*string{ + aws.String(d.Get("name").(string)), + }, + WithDecryption: aws.Bool(true), + } + + resp, err := ssmconn.GetParameters(paramInput) + + if err != nil { + return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err) + } + + if len(resp.InvalidParameters) > 0 { + return fmt.Errorf("[ERROR] SSM Parameter %s is invalid", d.Id()) + } + + param := resp.Parameters[0] + d.Set("name", param.Name) + d.Set("type", param.Type) + d.Set("value", param.Value) + + return nil +} + +func resourceAwsSsmParameterUpdate(d *schema.ResourceData, meta interface{}) error { + return putAwsSSMParameter(d, meta) +} + +func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[INFO] Deleting SSM Parameter: %s", d.Id()) + + paramInput := &ssm.DeleteParameterInput{ + Name: aws.String(d.Get("name").(string)), + } + + _, err := ssmconn.DeleteParameter(paramInput) + if err != nil { + return err + } + + d.SetId("") + + return nil +} + +func putAwsSSMParameter(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + log.Printf("[INFO] Creating SSM Parameter: %s", d.Get("name").(string)) + + paramInput := &ssm.PutParameterInput{ + Name: aws.String(d.Get("name").(string)), + Type: aws.String(d.Get("type").(string)), + Value: aws.String(d.Get("value").(string)), + Overwrite: aws.Bool(!d.IsNewResource()), + } + if keyID, ok := d.GetOk("key_id"); ok { + log.Printf("[DEBUG] Setting key_id for SSM Parameter %s: %s", d.Get("name").(string), keyID.(string)) + paramInput.SetKeyId(keyID.(string)) + } + + log.Printf("[DEBUG] Waiting for SSM Parameter %q to be updated", d.Get("name").(string)) + _, err := ssmconn.PutParameter(paramInput) + + if err != nil { + return errwrap.Wrapf("[ERROR] Error creating SSM parameter: {{err}}", err) + } + + d.SetId(d.Get("name").(string)) + + return resourceAwsSsmParameterRead(d, meta) +} diff --git a/builtin/providers/aws/resource_aws_ssm_parameter_test.go b/builtin/providers/aws/resource_aws_ssm_parameter_test.go new file mode 100644 index 000000000..b8c46b229 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ssm_parameter_test.go @@ -0,0 +1,210 @@ +package aws + +import ( + "fmt" + "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 TestAccAWSSSMParameter_basic(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterBasicConfig(name, "bar"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.foo", "bar"), + testAccCheckAWSSSMParameterType("aws_ssm_parameter.foo", "String"), + ), + }, + }, + }) +} + +func TestAccAWSSSMParameter_update(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterBasicConfig(name, "bar"), + }, + { + Config: testAccAWSSSMParameterBasicConfig(name, "baz"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.foo", "baz"), + testAccCheckAWSSSMParameterType("aws_ssm_parameter.foo", "String"), + ), + }, + }, + }) +} + +func TestAccAWSSSMParameter_secure(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterSecureConfig(name, "secret"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.secret_foo", "secret"), + testAccCheckAWSSSMParameterType("aws_ssm_parameter.secret_foo", "SecureString"), + ), + }, + }, + }) +} + +func TestAccAWSSSMParameter_secure_with_key(t *testing.T) { + name := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterSecureConfigWithKey(name, "secret"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.secret_foo", "secret"), + testAccCheckAWSSSMParameterType("aws_ssm_parameter.secret_foo", "SecureString"), + ), + }, + }, + }) +} + +func testAccCheckAWSSSMGetParameter(s *terraform.State, n string) ([]*ssm.Parameter, error) { + rs, ok := s.RootModule().Resources[n] + if !ok { + return []*ssm.Parameter{}, fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return []*ssm.Parameter{}, fmt.Errorf("No SSM Parameter ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + paramInput := &ssm.GetParametersInput{ + Names: []*string{ + aws.String(rs.Primary.Attributes["name"]), + }, + WithDecryption: aws.Bool(true), + } + + resp, _ := conn.GetParameters(paramInput) + + if len(resp.Parameters) == 0 { + return resp.Parameters, fmt.Errorf("Expected AWS SSM Parameter to be created, but wasn't found") + } + return resp.Parameters, nil +} + +func testAccCheckAWSSSMParameterHasValue(n string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + parameters, err := testAccCheckAWSSSMGetParameter(s, n) + if err != nil { + return err + } + + parameterValue := parameters[0].Value + + if *parameterValue != v { + return fmt.Errorf("Expected AWS SSM Parameter to have value %s but had %s", v, *parameterValue) + } + + return nil + } +} + +func testAccCheckAWSSSMParameterType(n string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + parameters, err := testAccCheckAWSSSMGetParameter(s, n) + if err != nil { + return err + } + + parameterValue := parameters[0].Type + + if *parameterValue != v { + return fmt.Errorf("Expected AWS SSM Parameter to have type %s but had %s", v, *parameterValue) + } + + return nil + } +} + +func testAccCheckAWSSSMParameterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ssmconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ssm_parameter" { + continue + } + + paramInput := &ssm.GetParametersInput{ + Names: []*string{ + aws.String(rs.Primary.Attributes["name"]), + }, + } + + resp, _ := conn.GetParameters(paramInput) + + if len(resp.Parameters) > 0 { + return fmt.Errorf("Expected AWS SSM Parameter to be gone, but was still found") + } + + return nil + } + + return fmt.Errorf("Default error in SSM Parameter Test") +} + +func testAccAWSSSMParameterBasicConfig(rName string, value string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "foo" { + name = "test_parameter-%s" + type = "String" + value = "%s" +} +`, rName, value) +} + +func testAccAWSSSMParameterSecureConfig(rName string, value string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "secret_foo" { + name = "test_secure_parameter-%s" + type = "SecureString" + value = "%s" +} +`, rName, value) +} + +func testAccAWSSSMParameterSecureConfigWithKey(rName string, value string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "secret_foo" { + name = "test_secure_parameter-%s" + type = "SecureString" + value = "%s" + key_id = "${aws_kms_key.test_key.id}" +} + +resource "aws_kms_key" "test_key" { + description = "KMS key 1" + deletion_window_in_days = 7 +} +`, rName, value) +} diff --git a/builtin/providers/aws/validators.go b/builtin/providers/aws/validators.go index 9109753ac..075958725 100644 --- a/builtin/providers/aws/validators.go +++ b/builtin/providers/aws/validators.go @@ -1336,3 +1336,17 @@ func validateIamRoleDescription(v interface{}, k string) (ws []string, errors [] } return } + +func validateSsmParameterType(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + types := map[string]bool{ + "String": true, + "StringList": true, + "SecureString": true, + } + + if !types[value] { + errors = append(errors, fmt.Errorf("Parameter type %s is invalid. Valid types are String, StringList or SecureString", value)) + } + return +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index 3386a3e39..c577fdbfd 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -2291,3 +2291,29 @@ func TestValidateIamRoleDescription(t *testing.T) { } } } + +func TestValidateSsmParameterType(t *testing.T) { + validTypes := []string{ + "String", + "StringList", + "SecureString", + } + for _, v := range validTypes { + _, errors := validateSsmParameterType(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid SSM parameter type: %q", v, errors) + } + } + + invalidTypes := []string{ + "foo", + "string", + "Securestring", + } + for _, v := range invalidTypes { + _, errors := validateSsmParameterType(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid SSM parameter type", v) + } + } +} diff --git a/website/source/docs/providers/aws/d/ssm_parameter.html.markdown b/website/source/docs/providers/aws/d/ssm_parameter.html.markdown new file mode 100644 index 000000000..cd1fc9114 --- /dev/null +++ b/website/source/docs/providers/aws/d/ssm_parameter.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "aws" +page_title: "AWS: aws_ssm_parameter" +sidebar_current: "docs-aws-datasource-ssm-parameter" +description: |- + Provides a SSM Parameter datasource +--- + +# aws\_ssm\_parameter + +Provides an SSM Parameter data source. + +## Example Usage + +To store a basic string parameter: + +```hcl +data "aws_ssm_parameter" "foo" { + name = "foo" +} +``` + +~> **Note:** The unencrypted value of a SecureString will be stored in the raw state as plain-text. +[Read more about sensitive data in state](/docs/state/sensitive-data.html). + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the parameter. + + +The following attributes are exported: + +* `name` - (Required) The name of the parameter. +* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. +* `value` - (Required) The value of the parameter. diff --git a/website/source/docs/providers/aws/r/ssm_parameter.html.markdown b/website/source/docs/providers/aws/r/ssm_parameter.html.markdown new file mode 100644 index 000000000..37fe89b3d --- /dev/null +++ b/website/source/docs/providers/aws/r/ssm_parameter.html.markdown @@ -0,0 +1,65 @@ +--- +layout: "aws" +page_title: "AWS: aws_ssm_parameter" +sidebar_current: "docs-aws-resource-ssm-parameter" +description: |- + Provides a SSM Parameter resource +--- + +# aws\_ssm\_parameter + +Provides an SSM Parameter resource. + +## Example Usage + +To store a basic string parameter: + +```hcl +resource "aws_ssm_parameter" "foo" { + name = "foo" + type = "String" + value = "bar" +} +``` + +To store an encrypted string using the default SSM KMS key: + +```hcl +resource "aws_db_instance" "default" { + allocated_storage = 10 + storage_type = "gp2" + engine = "mysql" + engine_version = "5.7.16" + instance_class = "db.t2.micro" + name = "mydb" + username = "foo" + password = "${var.database_master_password}" + db_subnet_group_name = "my_database_subnet_group" + parameter_group_name = "default.mysql5.7" +} + +resource "aws_ssm_parameter" "secret" { + name = "${var.environment}/database/password/master" + type = "SecureString" + value = "${var.database_master_password}" +} +``` + +~> **Note:** The unencrypted value of a SecureString will be stored in the raw state as plain-text. +[Read more about sensitive data in state](/docs/state/sensitive-data.html). + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the parameter. +* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. +* `value` - (Required) The value of the parameter. +* `key_id` - (Optional) The KMS key id or arn for encrypting a SecureString. +## Attributes Reference + +The following attributes are exported: + +* `name` - (Required) The name of the parameter. +* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. +* `value` - (Required) The value of the parameter. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 637a0cf73..3f03761a6 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -149,6 +149,9 @@ > aws_sns_topic + > + aws_ssm_parameter + > aws_subnet @@ -1323,6 +1326,9 @@ > aws_ssm_patch_group + > + aws_ssm_parameter +