diff --git a/.changelog/13018.txt b/.changelog/13018.txt new file mode 100644 index 00000000000..7ec742bd129 --- /dev/null +++ b/.changelog/13018.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ssm_service_setting +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e67aee4dcfa..04c065e59d8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2027,6 +2027,7 @@ func Provider() *schema.Provider { "aws_ssm_patch_baseline": ssm.ResourcePatchBaseline(), "aws_ssm_patch_group": ssm.ResourcePatchGroup(), "aws_ssm_resource_data_sync": ssm.ResourceResourceDataSync(), + "aws_ssm_service_setting": ssm.ResourceServiceSetting(), "aws_ssoadmin_account_assignment": ssoadmin.ResourceAccountAssignment(), "aws_ssoadmin_managed_policy_attachment": ssoadmin.ResourceManagedPolicyAttachment(), diff --git a/internal/service/ssm/find.go b/internal/service/ssm/find.go index 44d5b17fcda..55e3f36984b 100644 --- a/internal/service/ssm/find.go +++ b/internal/service/ssm/find.go @@ -86,3 +86,21 @@ func FindPatchGroup(conn *ssm.SSM, patchGroup, baselineId string) (*ssm.PatchGro return result, err } + +func FindServiceSettingByARN(conn *ssm.SSM, arn string) (*ssm.ServiceSetting, error) { + input := &ssm.GetServiceSettingInput{ + SettingId: aws.String(arn), + } + + output, err := conn.GetServiceSetting(input) + + if err != nil { + return nil, err + } + + if output == nil || output.ServiceSetting == nil { + return nil, fmt.Errorf("finding %s: empty result", arn) + } + + return output.ServiceSetting, nil +} diff --git a/internal/service/ssm/service_setting.go b/internal/service/ssm/service_setting.go new file mode 100644 index 00000000000..bfe72fbe80d --- /dev/null +++ b/internal/service/ssm/service_setting.go @@ -0,0 +1,110 @@ +package ssm + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResNameServiceSetting = "Service Setting" +) + +func ResourceServiceSetting() *schema.Resource { + return &schema.Resource{ + Create: resourceServiceSettingUpdate, + Read: resourceServiceSettingRead, + Update: resourceServiceSettingUpdate, + Delete: resourceServiceSettingReset, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "setting_id": { + Type: schema.TypeString, + Required: true, + }, + "setting_value": { + Type: schema.TypeString, + Required: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceServiceSettingUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SSMConn + + log.Printf("[DEBUG] SSM service setting create: %s", d.Get("setting_id").(string)) + + updateServiceSettingInput := &ssm.UpdateServiceSettingInput{ + SettingId: aws.String(d.Get("setting_id").(string)), + SettingValue: aws.String(d.Get("setting_value").(string)), + } + + if _, err := conn.UpdateServiceSetting(updateServiceSettingInput); err != nil { + return names.Error(names.SSM, names.ErrActionUpdating, ResNameServiceSetting, d.Get("setting_id").(string), err) + } + + d.SetId(d.Get("setting_id").(string)) + + if _, err := waitServiceSettingUpdated(conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return names.Error(names.SSM, names.ErrActionWaitingForUpdate, ResNameServiceSetting, d.Id(), err) + } + + return resourceServiceSettingRead(d, meta) +} + +func resourceServiceSettingRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SSMConn + + log.Printf("[DEBUG] Reading SSM Activation: %s", d.Id()) + + output, err := FindServiceSettingByARN(conn, d.Id()) + if err != nil { + return names.Error(names.SSM, names.ErrActionReading, ResNameServiceSetting, d.Id(), err) + } + + // AWS SSM service setting API requires the entire ARN as input, + // but setting_id in the output is only a part of ARN. + d.Set("setting_id", output.ARN) + d.Set("setting_value", output.SettingValue) + d.Set("arn", output.ARN) + d.Set("status", output.Status) + + return nil +} + +func resourceServiceSettingReset(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SSMConn + + log.Printf("[DEBUG] Deleting SSM Service Setting: %s", d.Id()) + + resetServiceSettingInput := &ssm.ResetServiceSettingInput{ + SettingId: aws.String(d.Get("setting_id").(string)), + } + + _, err := conn.ResetServiceSetting(resetServiceSettingInput) + if err != nil { + return names.Error(names.SSM, names.ErrActionDeleting, ResNameServiceSetting, d.Id(), err) + } + + if err := waitServiceSettingReset(conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return names.Error(names.SSM, names.ErrActionWaitingForDeletion, ResNameServiceSetting, d.Id(), err) + } + + return nil +} diff --git a/internal/service/ssm/service_setting_test.go b/internal/service/ssm/service_setting_test.go new file mode 100644 index 00000000000..a04f1761404 --- /dev/null +++ b/internal/service/ssm/service_setting_test.go @@ -0,0 +1,106 @@ +package ssm_test + +import ( + "fmt" + "testing" + + "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-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfssm "github.com/hashicorp/terraform-provider-aws/internal/service/ssm" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccSSMServiceSetting_basic(t *testing.T) { + var setting ssm.ServiceSetting + resourceName := "aws_ssm_service_setting.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ssm.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccServiceSettingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServiceSettingConfig_basic("false"), + Check: resource.ComposeTestCheckFunc( + testAccServiceSettingExists(resourceName, &setting), + resource.TestCheckResourceAttr(resourceName, "setting_value", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccServiceSettingConfig_basic("true"), + Check: resource.ComposeTestCheckFunc( + testAccServiceSettingExists(resourceName, &setting), + resource.TestCheckResourceAttr(resourceName, "setting_value", "true"), + ), + }, + }, + }) +} + +func testAccServiceSettingDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SSMConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ssm_service_setting" { + continue + } + + output, err := conn.GetServiceSetting(&ssm.GetServiceSettingInput{ + SettingId: aws.String(rs.Primary.Attributes["setting_id"]), + }) + _, ok := err.(awserr.Error) + if !ok { + return err + } + if output.ServiceSetting.Status != aws.String("default") { + return names.Error(names.SSM, names.ErrActionCheckingDestroyed, tfssm.ResNameServiceSetting, rs.Primary.Attributes["setting_id"], err) + } + } + + return nil +} + +func testAccServiceSettingExists(n string, res *ssm.ServiceSetting) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).SSMConn + + output, err := tfssm.FindServiceSettingByARN(conn, rs.Primary.Attributes["setting_id"]) + + if err != nil { + return names.Error(names.SSM, names.ErrActionReading, tfssm.ResNameServiceSetting, rs.Primary.Attributes["setting_id"], err) + } + + *res = *output + + return nil + } +} + +func testAccServiceSettingConfig_basic(settingValue string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +resource "aws_ssm_service_setting" "test" { + setting_id = "arn:${data.aws_partition.current.partition}:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:servicesetting/ssm/parameter-store/high-throughput-enabled" + setting_value = %[1]q +} +`, settingValue) +} diff --git a/internal/service/ssm/status.go b/internal/service/ssm/status.go index f32b506c890..e3f20568fb1 100644 --- a/internal/service/ssm/status.go +++ b/internal/service/ssm/status.go @@ -49,3 +49,15 @@ func statusDocument(conn *ssm.SSM, name string) resource.StateRefreshFunc { return output, aws.StringValue(output.Status), nil } } + +func statusServiceSetting(conn *ssm.SSM, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindServiceSettingByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/internal/service/ssm/wait.go b/internal/service/ssm/wait.go index 38d4804a8db..f5afec5db7c 100644 --- a/internal/service/ssm/wait.go +++ b/internal/service/ssm/wait.go @@ -70,3 +70,33 @@ func waitDocumentActive(conn *ssm.SSM, name string) (*ssm.DocumentDescription, e return nil, err } + +func waitServiceSettingUpdated(conn *ssm.SSM, arn string, timeout time.Duration) (*ssm.ServiceSetting, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PendingUpdate", ""}, + Target: []string{"Customized", "Default"}, + Refresh: statusServiceSetting(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ssm.ServiceSetting); ok { + return output, err + } + + return nil, err +} + +func waitServiceSettingReset(conn *ssm.SSM, arn string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"Customized", "PendingUpdate", ""}, + Target: []string{"Default"}, + Refresh: statusServiceSetting(conn, arn), + Timeout: timeout, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/website/docs/r/ssm_service_setting.html.markdown b/website/docs/r/ssm_service_setting.html.markdown new file mode 100644 index 00000000000..76251ca1c60 --- /dev/null +++ b/website/docs/r/ssm_service_setting.html.markdown @@ -0,0 +1,42 @@ +--- +subcategory: "SSM (Systems Manager)" +layout: "aws" +page_title: "AWS: aws_ssm_service_setting" +description: |- + Defines how a user interacts with or uses a service or a feature of a service. +--- + +# Resource: aws_ssm_service_setting + +This setting defines how a user interacts with or uses a service or a feature of a service. + +## Example Usage + +```terraform +resource "aws_ssm_service_setting" "test_setting" { + service_id = "arn:aws:ssm:us-east-1:123456789012:servicesetting/ssm/parameter-store/high-throughput-enabled" + service_value = "true" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_id` - (Required) ID of the service setting. +* `service_value` - (Required) Value of the service setting. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the service setting. +* `status` - Status of the service setting. Value can be `Default`, `Customized` or `PendingUpdate`. + +## Import + +AWS SSM Service Setting can be imported using the `setting_id`, e.g. + +```sh +$ terraform import aws_ssm_service_setting.example arn:aws:ssm:us-east-1:123456789012:servicesetting/ssm/parameter-store/high-throughput-enabled +```