From 48d61870b0543b78a029c0fc26af7acb50b871e9 Mon Sep 17 00:00:00 2001 From: Makoto Shiga Date: Wed, 1 Oct 2025 20:29:17 +0900 Subject: [PATCH 1/3] support on-call rotations for ssmcontacts --- internal/service/ssmcontacts/contact.go | 42 ++++- internal/service/ssmcontacts/contact_test.go | 154 ++++++++++++++++++ internal/service/ssmcontacts/helper.go | 3 + .../service/ssmcontacts/ssmcontacts_test.go | 1 + .../docs/d/ssmcontacts_contact.html.markdown | 3 +- .../docs/r/ssmcontacts_contact.html.markdown | 42 ++++- 6 files changed, 237 insertions(+), 8 deletions(-) diff --git a/internal/service/ssmcontacts/contact.go b/internal/service/ssmcontacts/contact.go index 7885b3b917d1..c86072bd3f5b 100644 --- a/internal/service/ssmcontacts/contact.go +++ b/internal/service/ssmcontacts/contact.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" @@ -49,6 +50,13 @@ func ResourceContact() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "rotation_ids": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, names.AttrType: { Type: schema.TypeString, Required: true, @@ -68,12 +76,23 @@ func resourceContactCreate(ctx context.Context, d *schema.ResourceData, meta any var diags diag.Diagnostics client := meta.(*conns.AWSClient).SSMContactsClient(ctx) + contactType := types.ContactType(d.Get(names.AttrType).(string)) + input := &ssmcontacts.CreateContactInput{ Alias: aws.String(d.Get(names.AttrAlias).(string)), DisplayName: aws.String(d.Get(names.AttrDisplayName).(string)), - Plan: &types.Plan{Stages: []types.Stage{}}, Tags: getTagsIn(ctx), - Type: types.ContactType(d.Get(names.AttrType).(string)), + Type: contactType, + } + + if contactType == types.ContactTypeOncallSchedule { + plan := &types.Plan{} + if v, ok := d.GetOk("rotation_ids"); ok { + plan.RotationIds = flex.ExpandStringValueList(v.([]any)) + } + input.Plan = plan + } else { + input.Plan = &types.Plan{Stages: []types.Stage{}} } output, err := client.CreateContact(ctx, input) @@ -117,10 +136,23 @@ func resourceContactUpdate(ctx context.Context, d *schema.ResourceData, meta any var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SSMContactsClient(ctx) - if d.HasChanges(names.AttrDisplayName) { + if d.HasChanges(names.AttrDisplayName, "rotation_ids") { + contactType := types.ContactType(d.Get(names.AttrType).(string)) + in := &ssmcontacts.UpdateContactInput{ - ContactId: aws.String(d.Id()), - DisplayName: aws.String(d.Get(names.AttrDisplayName).(string)), + ContactId: aws.String(d.Id()), + } + + if d.HasChange(names.AttrDisplayName) { + in.DisplayName = aws.String(d.Get(names.AttrDisplayName).(string)) + } + + if d.HasChange("rotation_ids") && contactType == types.ContactTypeOncallSchedule { + plan := &types.Plan{} + if v, ok := d.GetOk("rotation_ids"); ok { + plan.RotationIds = flex.ExpandStringValueList(v.([]any)) + } + in.Plan = plan } _, err := conn.UpdateContact(ctx, in) diff --git a/internal/service/ssmcontacts/contact_test.go b/internal/service/ssmcontacts/contact_test.go index 9a5f0b92aa57..c6b0df5456d3 100644 --- a/internal/service/ssmcontacts/contact_test.go +++ b/internal/service/ssmcontacts/contact_test.go @@ -241,6 +241,58 @@ func testAccContact_updateDisplayName(t *testing.T) { }) } +func testAccContact_oncallSchedule(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ssmcontacts_contact.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccContactPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMContactsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckContactDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccContactConfig_oncallSchedule(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckContactExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, names.AttrAlias, rName), + resource.TestCheckResourceAttr(resourceName, names.AttrType, "ONCALL_SCHEDULE"), + resource.TestCheckResourceAttr(resourceName, "rotation_ids.#", "1"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "ssm-contacts", regexache.MustCompile(`contact/.+$`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccContactConfig_oncallScheduleUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckContactExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, names.AttrAlias, rName), + resource.TestCheckResourceAttr(resourceName, names.AttrType, "ONCALL_SCHEDULE"), + resource.TestCheckResourceAttr(resourceName, "rotation_ids.#", "2"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "ssm-contacts", regexache.MustCompile(`contact/.+$`)), + ), + }, + { + Config: testAccContactConfig_none(), + Check: testAccCheckContactDestroy(ctx), + }, + }, + }) +} + + func testAccCheckContactDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).SSMContactsClient(ctx) @@ -383,3 +435,105 @@ resource "aws_ssmcontacts_contact" "contact_one" { } `, alias, displayName)) } + +func testAccContactConfig_oncallSchedule(alias string) string { + return acctest.ConfigCompose( + testAccContactConfig_base(), + fmt.Sprintf(` +resource "aws_ssmcontacts_contact" "test_contact" { + alias = "%[1]s-contact" + type = "PERSONAL" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_rotation" "test" { + contact_ids = [aws_ssmcontacts_contact.test_contact.arn] + name = %[1]q + recurrence { + number_of_on_calls = 1 + recurrence_multiplier = 1 + daily_settings { + hour_of_day = 9 + minute_of_hour = 0 + } + } + time_zone_id = "America/Los_Angeles" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_contact" "test" { + alias = %[1]q + display_name = %[1]q + type = "ONCALL_SCHEDULE" + rotation_ids = [aws_ssmcontacts_rotation.test.arn] + + depends_on = [aws_ssmincidents_replication_set.test] +} +`, alias)) +} + +func testAccContactConfig_oncallScheduleUpdated(alias string) string { + return acctest.ConfigCompose( + testAccContactConfig_base(), + fmt.Sprintf(` +resource "aws_ssmcontacts_contact" "test_contact" { + alias = "%[1]s-contact" + type = "PERSONAL" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_contact" "test_contact_2" { + alias = "%[1]s-contact-2" + type = "PERSONAL" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_rotation" "test" { + contact_ids = [aws_ssmcontacts_contact.test_contact.arn] + name = %[1]q + recurrence { + number_of_on_calls = 1 + recurrence_multiplier = 1 + daily_settings { + hour_of_day = 9 + minute_of_hour = 0 + } + } + time_zone_id = "America/Los_Angeles" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_rotation" "test_2" { + contact_ids = [aws_ssmcontacts_contact.test_contact_2.arn] + name = "%[1]s-2" + recurrence { + number_of_on_calls = 1 + recurrence_multiplier = 1 + daily_settings { + hour_of_day = 14 + minute_of_hour = 30 + } + } + time_zone_id = "America/New_York" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_contact" "test" { + alias = %[1]q + display_name = %[1]q + type = "ONCALL_SCHEDULE" + rotation_ids = [ + aws_ssmcontacts_rotation.test.arn, + aws_ssmcontacts_rotation.test_2.arn + ] + + depends_on = [aws_ssmincidents_replication_set.test] +} +`, alias)) +} diff --git a/internal/service/ssmcontacts/helper.go b/internal/service/ssmcontacts/helper.go index 90d489eb7908..c43fcba4635d 100644 --- a/internal/service/ssmcontacts/helper.go +++ b/internal/service/ssmcontacts/helper.go @@ -16,6 +16,9 @@ func setContactResourceData(d *schema.ResourceData, getContactOutput *ssmcontact d.Set(names.AttrAlias, getContactOutput.Alias) d.Set(names.AttrType, getContactOutput.Type) d.Set(names.AttrDisplayName, getContactOutput.DisplayName) + if getContactOutput.Plan != nil { + d.Set("rotation_ids", getContactOutput.Plan.RotationIds) + } return nil } diff --git a/internal/service/ssmcontacts/ssmcontacts_test.go b/internal/service/ssmcontacts/ssmcontacts_test.go index d102cd5326f1..7d6949d0a3fb 100644 --- a/internal/service/ssmcontacts/ssmcontacts_test.go +++ b/internal/service/ssmcontacts/ssmcontacts_test.go @@ -22,6 +22,7 @@ func TestAccSSMContacts_serial(t *testing.T) { "updateDisplayName": testAccContact_updateDisplayName, "tags": testAccSSMContactsContact_tagsSerial, "updateType": testAccContact_updateType, + "oncallSchedule": testAccContact_oncallSchedule, }, "ContactDataSource": { acctest.CtBasic: testAccContactDataSource_basic, diff --git a/website/docs/d/ssmcontacts_contact.html.markdown b/website/docs/d/ssmcontacts_contact.html.markdown index 043d944c95d3..533907c0cbf2 100644 --- a/website/docs/d/ssmcontacts_contact.html.markdown +++ b/website/docs/d/ssmcontacts_contact.html.markdown @@ -32,6 +32,7 @@ This data source supports the following arguments: This data source exports the following attributes in addition to the arguments above: * `alias` - A unique and identifiable alias of the contact or escalation plan. -* `type` - The type of contact engaged. A single contact is type `PERSONAL` and an escalation plan is type `ESCALATION`. +* `type` - The type of contact engaged. A single contact is type `PERSONAL`, an escalation plan is type `ESCALATION`, and an on-call schedule is type `ONCALL_SCHEDULE`. * `display_name` - Full friendly name of the contact or escalation plan. +* `rotation_ids` - List of rotation IDs associated with the contact. * `tags` - Map of tags to assign to the resource. diff --git a/website/docs/r/ssmcontacts_contact.html.markdown b/website/docs/r/ssmcontacts_contact.html.markdown index e6c5c59c4ae4..c668e8570f00 100644 --- a/website/docs/r/ssmcontacts_contact.html.markdown +++ b/website/docs/r/ssmcontacts_contact.html.markdown @@ -41,18 +41,56 @@ resource "aws_ssmcontacts_contact" "example" { } ``` +### On-call Schedule Usage + +```terraform +resource "aws_ssmcontacts_contact" "oncall_contact" { + alias = "oncall-contact" + type = "PERSONAL" + + depends_on = [aws_ssmincidents_replication_set.example] +} + +resource "aws_ssmcontacts_rotation" "example" { + contact_ids = [aws_ssmcontacts_contact.oncall_contact.arn] + name = "example-rotation" + + recurrence { + number_of_on_calls = 1 + recurrence_multiplier = 1 + daily_settings { + hour_of_day = 9 + minute_of_hour = 0 + } + } + + time_zone_id = "America/Los_Angeles" + + depends_on = [aws_ssmincidents_replication_set.example] +} + +resource "aws_ssmcontacts_contact" "example" { + alias = "oncall-schedule" + display_name = "Example On-call Schedule" + type = "ONCALL_SCHEDULE" + rotation_ids = [aws_ssmcontacts_rotation.example.arn] + + depends_on = [aws_ssmincidents_replication_set.example] +} +``` + ## Argument Reference The following arguments are required: - `alias` - (Required) A unique and identifiable alias for the contact or escalation plan. Must be between 1 and 255 characters, and may contain alphanumerics, underscores (`_`), and hyphens (`-`). -- `type` - (Required) The type of contact engaged. A single contact is type PERSONAL and an escalation - plan is type ESCALATION. +- `type` - (Required) The type of contact engaged. A single contact is type `PERSONAL`, an escalation plan is type `ESCALATION`, and an on-call schedule is type `ONCALL_SCHEDULE`. The following arguments are optional: - `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). - `display_name` - (Optional) Full friendly name of the contact or escalation plan. If set, must be between 1 and 255 characters, and may contain alphanumerics, underscores (`_`), hyphens (`-`), periods (`.`), and spaces. +- `rotation_ids` - (Optional) List of rotation IDs associated with the contact. Required when `type` is `ONCALL_SCHEDULE`. - `tags` - (Optional) Key-value tags for the monitor. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attribute Reference From 37e87407868432af57641507042aabb687d0a43c Mon Sep 17 00:00:00 2001 From: makoto shiga Date: Thu, 2 Oct 2025 15:53:36 +0900 Subject: [PATCH 2/3] add changelog --- .changelog/44523.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/44523.txt diff --git a/.changelog/44523.txt b/.changelog/44523.txt new file mode 100644 index 000000000000..82075b81627a --- /dev/null +++ b/.changelog/44523.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ssmcontacts_contact: Add `rotation_ids` argument to support `ONCALL_SCHEDULE` contact type +``` From bb716df498707cde8df299f0407b8579ed7a042c Mon Sep 17 00:00:00 2001 From: makoto shiga Date: Thu, 2 Oct 2025 17:27:54 +0900 Subject: [PATCH 3/3] support data block --- .changelog/44523.txt | 4 + .../ssmcontacts/contact_data_source.go | 7 ++ .../ssmcontacts/contact_data_source_test.go | 75 +++++++++++++++++++ internal/service/ssmcontacts/contact_test.go | 1 - .../service/ssmcontacts/ssmcontacts_test.go | 5 +- 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/.changelog/44523.txt b/.changelog/44523.txt index 82075b81627a..ddf38aeb09a3 100644 --- a/.changelog/44523.txt +++ b/.changelog/44523.txt @@ -1,3 +1,7 @@ ```release-note:enhancement resource/aws_ssmcontacts_contact: Add `rotation_ids` argument to support `ONCALL_SCHEDULE` contact type ``` + +```release-note:enhancement +data-source/aws_ssmcontacts_contact: Add `rotation_ids` attribute to support `ONCALL_SCHEDULE` contact type +``` diff --git a/internal/service/ssmcontacts/contact_data_source.go b/internal/service/ssmcontacts/contact_data_source.go index 095a275a250f..472c7edb18da 100644 --- a/internal/service/ssmcontacts/contact_data_source.go +++ b/internal/service/ssmcontacts/contact_data_source.go @@ -35,6 +35,13 @@ func DataSourceContact() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "rotation_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, names.AttrType: { Type: schema.TypeString, Computed: true, diff --git a/internal/service/ssmcontacts/contact_data_source_test.go b/internal/service/ssmcontacts/contact_data_source_test.go index 8f91c0863ea3..06bb2f4f8e4c 100644 --- a/internal/service/ssmcontacts/contact_data_source_test.go +++ b/internal/service/ssmcontacts/contact_data_source_test.go @@ -47,6 +47,39 @@ func testAccContactDataSource_basic(t *testing.T) { }) } +func testAccContactDataSource_oncallSchedule(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ssmcontacts_contact.test" + dataSourceName := "data.aws_ssmcontacts_contact.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccContactPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMContactsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccContactDataSourceConfig_oncallSchedule(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, names.AttrARN, dataSourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(resourceName, names.AttrAlias, dataSourceName, names.AttrAlias), + resource.TestCheckResourceAttrPair(resourceName, names.AttrType, dataSourceName, names.AttrType), + resource.TestCheckResourceAttrPair(resourceName, names.AttrDisplayName, dataSourceName, names.AttrDisplayName), + resource.TestCheckResourceAttrPair(resourceName, "rotation_ids.#", dataSourceName, "rotation_ids.#"), + resource.TestCheckResourceAttrPair(resourceName, "rotation_ids.0", dataSourceName, "rotation_ids.0"), + ), + }, + }, + }) +} + func testAccContactDataSourceConfig_base() string { return fmt.Sprintf(` resource "aws_ssmincidents_replication_set" "test" { @@ -79,3 +112,45 @@ data "aws_ssmcontacts_contact" "contact_one" { } `, alias)) } + +func testAccContactDataSourceConfig_oncallSchedule(alias string) string { + return acctest.ConfigCompose( + testAccContactDataSourceConfig_base(), + fmt.Sprintf(` +resource "aws_ssmcontacts_contact" "test_contact" { + alias = "%[1]s-contact" + type = "PERSONAL" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_rotation" "test" { + contact_ids = [aws_ssmcontacts_contact.test_contact.arn] + name = %[1]q + recurrence { + number_of_on_calls = 1 + recurrence_multiplier = 1 + daily_settings { + hour_of_day = 9 + minute_of_hour = 0 + } + } + time_zone_id = "America/Los_Angeles" + + depends_on = [aws_ssmincidents_replication_set.test] +} + +resource "aws_ssmcontacts_contact" "test" { + alias = %[1]q + display_name = %[1]q + type = "ONCALL_SCHEDULE" + rotation_ids = [aws_ssmcontacts_rotation.test.arn] + + depends_on = [aws_ssmincidents_replication_set.test] +} + +data "aws_ssmcontacts_contact" "test" { + arn = aws_ssmcontacts_contact.test.arn +} +`, alias)) +} diff --git a/internal/service/ssmcontacts/contact_test.go b/internal/service/ssmcontacts/contact_test.go index c6b0df5456d3..5c4703d8b6f9 100644 --- a/internal/service/ssmcontacts/contact_test.go +++ b/internal/service/ssmcontacts/contact_test.go @@ -292,7 +292,6 @@ func testAccContact_oncallSchedule(t *testing.T) { }) } - func testAccCheckContactDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).SSMContactsClient(ctx) diff --git a/internal/service/ssmcontacts/ssmcontacts_test.go b/internal/service/ssmcontacts/ssmcontacts_test.go index 7d6949d0a3fb..6ce5ad0a7bc4 100644 --- a/internal/service/ssmcontacts/ssmcontacts_test.go +++ b/internal/service/ssmcontacts/ssmcontacts_test.go @@ -25,8 +25,9 @@ func TestAccSSMContacts_serial(t *testing.T) { "oncallSchedule": testAccContact_oncallSchedule, }, "ContactDataSource": { - acctest.CtBasic: testAccContactDataSource_basic, - "tags": testAccSSMContactsContactDataSource_tagsSerial, + acctest.CtBasic: testAccContactDataSource_basic, + "tags": testAccSSMContactsContactDataSource_tagsSerial, + "oncallSchedule": testAccContactDataSource_oncallSchedule, }, "ContactChannelResource": { acctest.CtBasic: testAccContactChannel_basic,