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 @@