diff --git a/.changelog/30665.txt b/.changelog/30665.txt new file mode 100644 index 00000000000..439cec44031 --- /dev/null +++ b/.changelog/30665.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_ssmincidents_response_plan +``` + +```release-note:new-data-source +aws_ssmincidents_response_plan +``` \ No newline at end of file diff --git a/internal/service/ssmincidents/find.go b/internal/service/ssmincidents/find.go index 3545cbd0d59..7f00715a16d 100644 --- a/internal/service/ssmincidents/find.go +++ b/internal/service/ssmincidents/find.go @@ -11,6 +11,31 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) +func FindResponsePlanByID(context context.Context, client *ssmincidents.Client, arn string) (*ssmincidents.GetResponsePlanOutput, error) { + input := &ssmincidents.GetResponsePlanInput{ + Arn: aws.String(arn), + } + output, err := client.GetResponsePlan(context, input) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + func FindReplicationSetByID(context context.Context, client *ssmincidents.Client, arn string) (*types.ReplicationSet, error) { input := &ssmincidents.GetReplicationSetInput{ Arn: aws.String(arn), diff --git a/internal/service/ssmincidents/flex.go b/internal/service/ssmincidents/flex.go index c6eb751c4e9..904d12033f9 100644 --- a/internal/service/ssmincidents/flex.go +++ b/internal/service/ssmincidents/flex.go @@ -3,6 +3,8 @@ package ssmincidents import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssmincidents/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/flex" ) func expandRegions(regions []interface{}) map[string]types.RegionMapInputValue { @@ -48,3 +50,365 @@ func flattenRegions(regions map[string]types.RegionInfo) []map[string]interface{ return tfRegionData } + +func expandIncidentTemplate(config []interface{}) *types.IncidentTemplate { + // we require exactly one item so we grab first in list + templateConfig := config[0].(map[string]interface{}) + + template := &types.IncidentTemplate{} + + if v, ok := templateConfig["title"].(string); ok && v != "" { + template.Title = aws.String(v) + } + + if v, ok := templateConfig["impact"].(int); ok && v != 0 { + template.Impact = aws.Int32(int32(v)) + } + + // dedupe string can be updated to have no value (denoted as "") + if v, ok := templateConfig["dedupe_string"].(string); ok { + template.DedupeString = aws.String(v) + } + + if v, ok := templateConfig["incident_tags"].(map[string]interface{}); ok && len(v) > 0 { + template.IncidentTags = flex.ExpandStringValueMap(v) + } + + // summary can be updated to have no value (denoted as "") + if v, ok := templateConfig["summary"].(string); ok { + template.Summary = aws.String(v) + } + + if v, ok := templateConfig["notification_target"].(*schema.Set); ok && v.Len() > 0 { + template.NotificationTargets = expandNotificationTargets(v.List()) + } + + return template +} + +func flattenIncidentTemplate(template *types.IncidentTemplate) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + tfTemplate := make(map[string]interface{}) + + tfTemplate["impact"] = aws.ToInt32(template.Impact) + tfTemplate["title"] = aws.ToString(template.Title) + + if v := template.DedupeString; v != nil { + tfTemplate["dedupe_string"] = aws.ToString(v) + } + + if v := template.IncidentTags; v != nil { + tfTemplate["incident_tags"] = template.IncidentTags + } + + if v := template.Summary; v != nil { + tfTemplate["summary"] = aws.ToString(template.Summary) + } + + if v := template.NotificationTargets; v != nil { + tfTemplate["notification_target"] = flattenNotificationTargets(template.NotificationTargets) + } + + result = append(result, tfTemplate) + return result +} + +func expandNotificationTargets(targets []interface{}) []types.NotificationTargetItem { + if len(targets) == 0 { + return nil + } + + notificationTargets := make([]types.NotificationTargetItem, len(targets)) + + for i, target := range targets { + targetData := target.(map[string]interface{}) + + targetItem := &types.NotificationTargetItemMemberSnsTopicArn{ + Value: targetData["sns_topic_arn"].(string), + } + + notificationTargets[i] = targetItem + } + + return notificationTargets +} + +func flattenNotificationTargets(targets []types.NotificationTargetItem) []map[string]interface{} { + if len(targets) == 0 { + return nil + } + + notificationTargets := make([]map[string]interface{}, len(targets)) + + for i, target := range targets { + targetItem := make(map[string]interface{}) + + targetItem["sns_topic_arn"] = target.(*types.NotificationTargetItemMemberSnsTopicArn).Value + + notificationTargets[i] = targetItem + } + + return notificationTargets +} + +func expandChatChannel(chatChannels *schema.Set) types.ChatChannel { + chatChannelList := flex.ExpandStringValueSet(chatChannels) + + if len(chatChannelList) == 0 { + return &types.ChatChannelMemberEmpty{ + Value: types.EmptyChatChannel{}, + } + } + + return &types.ChatChannelMemberChatbotSns{ + Value: chatChannelList, + } +} + +func flattenChatChannel(chatChannel types.ChatChannel) *schema.Set { + if _, ok := chatChannel.(*types.ChatChannelMemberEmpty); ok { + return &schema.Set{} + } + + if chatBotSns, ok := chatChannel.(*types.ChatChannelMemberChatbotSns); ok { + return flex.FlattenStringValueSet(chatBotSns.Value) + } + return nil +} + +func expandAction(actions []interface{}) []types.Action { + if len(actions) == 0 { + return nil + } + + result := make([]types.Action, 0) + + actionConfig := actions[0].(map[string]interface{}) + if v, ok := actionConfig["ssm_automation"].([]interface{}); ok { + result = append(result, expandSSMAutomations(v)...) + } + + return result +} + +func flattenAction(actions []types.Action) []interface{} { + if len(actions) == 0 { + return nil + } + + result := make([]interface{}, 0) + + action := make(map[string]interface{}) + action["ssm_automation"] = flattenSSMAutomations(actions) + result = append(result, action) + + return result +} + +func expandSSMAutomations(automations []interface{}) []types.Action { + var result []types.Action + for _, automation := range automations { + ssmAutomation := types.SsmAutomation{} + automationData := automation.(map[string]interface{}) + + if v, ok := automationData["document_name"].(string); ok { + ssmAutomation.DocumentName = aws.String(v) + } + + if v, ok := automationData["role_arn"].(string); ok { + ssmAutomation.RoleArn = aws.String(v) + } + + if v, ok := automationData["document_version"].(string); ok { + ssmAutomation.DocumentVersion = aws.String(v) + } + + if v, ok := automationData["target_account"].(string); ok { + ssmAutomation.TargetAccount = types.SsmTargetAccount(v) + } + + if v, ok := automationData["parameter"].(*schema.Set); ok { + ssmAutomation.Parameters = expandParameters(v) + } + + if v, ok := automationData["dynamic_parameters"].(map[string]interface{}); ok { + ssmAutomation.DynamicParameters = expandDynamicParameters(v) + } + + result = append( + result, + &types.ActionMemberSsmAutomation{Value: ssmAutomation}, + ) + } + return result +} + +func flattenSSMAutomations(actions []types.Action) []interface{} { + var result []interface{} + + for _, action := range actions { + if ssmAutomationAction, ok := action.(*types.ActionMemberSsmAutomation); ok { + ssmAutomation := ssmAutomationAction.Value + + a := map[string]interface{}{} + + if v := ssmAutomation.DocumentName; v != nil { + a["document_name"] = aws.ToString(v) + } + + if v := ssmAutomation.RoleArn; v != nil { + a["role_arn"] = aws.ToString(v) + } + + if v := ssmAutomation.DocumentVersion; v != nil { + a["document_version"] = aws.ToString(v) + } + + if v := ssmAutomation.TargetAccount; v != "" { + a["target_account"] = ssmAutomation.TargetAccount + } + + if v := ssmAutomation.Parameters; v != nil { + a["parameter"] = flattenParameters(v) + } + + if v := ssmAutomation.DynamicParameters; v != nil { + a["dynamic_parameters"] = flattenDynamicParameters(v) + } + + result = append(result, a) + } + } + return result +} + +func expandParameters(parameters *schema.Set) map[string][]string { + parameterMap := make(map[string][]string) + for _, parameter := range parameters.List() { + parameterData := parameter.(map[string]interface{}) + name := parameterData["name"].(string) + values := flex.ExpandStringValueSet(parameterData["values"].(*schema.Set)) + parameterMap[name] = values + } + return parameterMap +} + +func flattenParameters(parameterMap map[string][]string) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + for name, values := range parameterMap { + data := make(map[string]interface{}) + data["name"] = name + data["values"] = flex.FlattenStringValueList(values) + result = append(result, data) + } + return result +} + +func expandDynamicParameters(parameterMap map[string]interface{}) map[string]types.DynamicSsmParameterValue { + result := make(map[string]types.DynamicSsmParameterValue) + for key, value := range parameterMap { + parameterValue := &types.DynamicSsmParameterValueMemberVariable{ + Value: types.VariableType(value.(string)), + } + result[key] = parameterValue + } + return result +} + +func flattenDynamicParameters(parameterMap map[string]types.DynamicSsmParameterValue) map[string]interface{} { + result := make(map[string]interface{}) + for key, value := range parameterMap { + parameterValue := value.(*types.DynamicSsmParameterValueMemberVariable) + result[key] = parameterValue.Value + } + + return result +} + +func expandIntegration(integrations []interface{}) []types.Integration { + if len(integrations) == 0 { + return nil + } + + // we require exactly one integration item + integrationData := integrations[0].(map[string]interface{}) + result := make([]types.Integration, 0) + + if v, ok := integrationData["pagerduty"].([]interface{}); ok { + result = append(result, expandPagerDutyIntegration(v)...) + } + + return result +} + +func flattenIntegration(integrations []types.Integration) []interface{} { + if len(integrations) == 0 { + return nil + } + + result := make([]interface{}, 0) + + integration := make(map[string]interface{}) + integration["pagerduty"] = flattenPagerDutyIntegration(integrations) + result = append(result, integration) + + return result +} + +func expandPagerDutyIntegration(integrations []interface{}) []types.Integration { + result := make([]types.Integration, 0) + + for _, integration := range integrations { + if integration == nil { + continue + } + integrationData := integration.(map[string]interface{}) + + pagerDutyIntegration := types.PagerDutyConfiguration{} + + if v, ok := integrationData["name"].(string); ok && v != "" { + pagerDutyIntegration.Name = aws.String(v) + } + + if v, ok := integrationData["service_id"].(string); ok && v != "" { + pagerDutyIntegration.PagerDutyIncidentConfiguration = + &types.PagerDutyIncidentConfiguration{ + ServiceId: aws.String(v), + } + } + + if v, ok := integrationData["secret_id"].(string); ok && v != "" { + pagerDutyIntegration.SecretId = aws.String(v) + } + + result = append(result, &types.IntegrationMemberPagerDutyConfiguration{Value: pagerDutyIntegration}) + } + + return result +} + +func flattenPagerDutyIntegration(integrations []types.Integration) []interface{} { + result := make([]interface{}, 0) + + for _, integration := range integrations { + if v, ok := integration.(*types.IntegrationMemberPagerDutyConfiguration); ok { + pagerDutyConfiguration := v.Value + pagerDutyData := map[string]interface{}{} + + if v := pagerDutyConfiguration.Name; v != nil { + pagerDutyData["name"] = v + } + + if v := pagerDutyConfiguration.PagerDutyIncidentConfiguration.ServiceId; v != nil { + pagerDutyData["service_id"] = v + } + + if v := pagerDutyConfiguration.SecretId; v != nil { + pagerDutyData["secret_id"] = v + } + + result = append(result, pagerDutyData) + } + } + return result +} diff --git a/internal/service/ssmincidents/helper.go b/internal/service/ssmincidents/helper.go index 138641d9a18..d94ebb1f676 100644 --- a/internal/service/ssmincidents/helper.go +++ b/internal/service/ssmincidents/helper.go @@ -1,12 +1,12 @@ package ssmincidents -// contains misc functions used by multiple files - import ( "context" "fmt" "github.com/aws/aws-sdk-go-v2/service/ssmincidents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/flex" ) func getReplicationSetARN(context context.Context, client *ssmincidents.Client) (string, error) { @@ -23,3 +23,34 @@ func getReplicationSetARN(context context.Context, client *ssmincidents.Client) // currently only one replication set is supported return replicationSets.ReplicationSetArns[0], nil } + +func setResponsePlanResourceData( + d *schema.ResourceData, + getResponsePlanOutput *ssmincidents.GetResponsePlanOutput, +) (*schema.ResourceData, error) { + if err := d.Set("action", flattenAction(getResponsePlanOutput.Actions)); err != nil { + return d, err + } + if err := d.Set("arn", getResponsePlanOutput.Arn); err != nil { + return d, err + } + if err := d.Set("chat_channel", flattenChatChannel(getResponsePlanOutput.ChatChannel)); err != nil { + return d, err + } + if err := d.Set("display_name", getResponsePlanOutput.DisplayName); err != nil { + return d, err + } + if err := d.Set("engagements", flex.FlattenStringValueSet(getResponsePlanOutput.Engagements)); err != nil { + return d, err + } + if err := d.Set("incident_template", flattenIncidentTemplate(getResponsePlanOutput.IncidentTemplate)); err != nil { + return d, err + } + if err := d.Set("integration", flattenIntegration(getResponsePlanOutput.Integrations)); err != nil { + return d, err + } + if err := d.Set("name", getResponsePlanOutput.Name); err != nil { + return d, err + } + return d, nil +} diff --git a/internal/service/ssmincidents/response_plan.go b/internal/service/ssmincidents/response_plan.go new file mode 100644 index 00000000000..cf7bf01414d --- /dev/null +++ b/internal/service/ssmincidents/response_plan.go @@ -0,0 +1,327 @@ +package ssmincidents + +import ( + "context" + "errors" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssmincidents" + "github.com/aws/aws-sdk-go-v2/service/ssmincidents/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResNameResponsePlan = "Response Plan" +) + +// @SDKResource("aws_ssmincidents_response_plan", name="Response Plan") +// @Tags(identifierAttribute="id") +func ResourceResponsePlan() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceResponsePlanCreate, + ReadWithoutTimeout: resourceResponsePlanRead, + UpdateWithoutTimeout: resourceResponsePlanUpdate, + DeleteWithoutTimeout: resourceResponsePlanDelete, + + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ssm_automation": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "document_name": { + Type: schema.TypeString, + Required: true, + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + }, + "document_version": { + Type: schema.TypeString, + Optional: true, + }, + "target_account": { + Type: schema.TypeString, + Optional: true, + }, + "parameter": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "dynamic_parameters": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "chat_channel": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + }, + "engagements": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "incident_template": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + }, + "impact": { + Type: schema.TypeInt, + Required: true, + }, + "dedupe_string": { + Type: schema.TypeString, + Optional: true, + }, + "incident_tags": tftags.TagsSchema(), + "notification_target": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sns_topic_arn": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "summary": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "integration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pagerduty": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "service_id": { + Type: schema.TypeString, + Required: true, + }, + "secret_id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceResponsePlanCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*conns.AWSClient).SSMIncidentsClient() + + input := &ssmincidents.CreateResponsePlanInput{ + Actions: expandAction(d.Get("action").([]interface{})), + ChatChannel: expandChatChannel(d.Get("chat_channel").(*schema.Set)), + DisplayName: aws.String(d.Get("display_name").(string)), + Engagements: flex.ExpandStringValueSet(d.Get("engagements").(*schema.Set)), + IncidentTemplate: expandIncidentTemplate(d.Get("incident_template").([]interface{})), + Integrations: expandIntegration(d.Get("integration").([]interface{})), + Name: aws.String(d.Get("name").(string)), + Tags: GetTagsIn(ctx), + } + + output, err := client.CreateResponsePlan(ctx, input) + + if err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionCreating, ResNameResponsePlan, d.Get("name").(string), err) + } + + if output == nil { + return create.DiagError(names.SSMIncidents, create.ErrActionCreating, ResNameResponsePlan, d.Get("name").(string), errors.New("empty output")) + } + + d.SetId(aws.ToString(output.Arn)) + + return resourceResponsePlanRead(ctx, d, meta) +} + +func resourceResponsePlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*conns.AWSClient).SSMIncidentsClient() + + responsePlan, err := FindResponsePlanByID(ctx, client, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] SSMIncidents ResponsePlan (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionReading, ResNameResponsePlan, d.Id(), err) + } + + if d, err := setResponsePlanResourceData(d, responsePlan); err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionSetting, ResNameResponsePlan, d.Id(), err) + } + + return nil +} + +func resourceResponsePlanUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*conns.AWSClient).SSMIncidentsClient() + + if d.HasChangesExcept("tags", "tags_all") { + input := &ssmincidents.UpdateResponsePlanInput{ + Arn: aws.String(d.Id()), + } + + if d.HasChanges("action") { + input.Actions = expandAction(d.Get("action").([]interface{})) + } + + if d.HasChanges("chat_channel") { + input.ChatChannel = expandChatChannel(d.Get("chat_channel").(*schema.Set)) + } + + if d.HasChanges("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChanges("engagements") { + input.Engagements = flex.ExpandStringValueSet(d.Get("engagements").(*schema.Set)) + } + + if d.HasChanges("incident_template") { + incidentTemplate := d.Get("incident_template") + template := expandIncidentTemplate(incidentTemplate.([]interface{})) + updateResponsePlanInputWithIncidentTemplate(input, template) + } + + if d.HasChanges("integration") { + input.Integrations = expandIntegration(d.Get("integration").([]interface{})) + } + + _, err := client.UpdateResponsePlan(ctx, input) + + if err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionUpdating, ResNameResponsePlan, d.Id(), err) + } + } + + // tags can have a change without tags_all having a change when value of tag is "" + if d.HasChanges("tags_all", "tags") { + if err := updateResourceTags(ctx, client, d); err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionUpdating, ResNameResponsePlan, d.Id(), err) + } + } + + return resourceResponsePlanRead(ctx, d, meta) +} + +func resourceResponsePlanDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*conns.AWSClient).SSMIncidentsClient() + + log.Printf("[INFO] Deleting SSMIncidents ResponsePlan %s", d.Id()) + + input := &ssmincidents.DeleteResponsePlanInput{ + Arn: aws.String(d.Id()), + } + + _, err := client.DeleteResponsePlan(ctx, input) + + if err != nil { + var notFoundError *types.ResourceNotFoundException + + if errors.As(err, ¬FoundError) { + return nil + } + + return create.DiagError(names.SSMIncidents, create.ErrActionDeleting, ResNameResponsePlan, d.Id(), err) + } + + return nil +} + +// input validation already done in flattenIncidentTemplate function +func updateResponsePlanInputWithIncidentTemplate(input *ssmincidents.UpdateResponsePlanInput, template *types.IncidentTemplate) { + input.IncidentTemplateImpact = template.Impact + input.IncidentTemplateTitle = template.Title + input.IncidentTemplateTags = template.IncidentTags + input.IncidentTemplateNotificationTargets = template.NotificationTargets + input.IncidentTemplateDedupeString = template.DedupeString + input.IncidentTemplateSummary = template.Summary +} diff --git a/internal/service/ssmincidents/response_plan_data_source.go b/internal/service/ssmincidents/response_plan_data_source.go new file mode 100644 index 00000000000..2b02a529993 --- /dev/null +++ b/internal/service/ssmincidents/response_plan_data_source.go @@ -0,0 +1,200 @@ +package ssmincidents + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKDataSource("aws_ssmincidents_response_plan") +func DataSourceResponsePlan() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceResponsePlanRead, + + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ssm_automation": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "document_name": { + Type: schema.TypeString, + Computed: true, + }, + "role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "document_version": { + Type: schema.TypeString, + Computed: true, + }, + "target_account": { + Type: schema.TypeString, + Computed: true, + }, + "parameter": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "values": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "dynamic_parameters": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "arn": { + Type: schema.TypeString, + Required: true, + }, + "chat_channel": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "display_name": { + Type: schema.TypeString, + Computed: true, + }, + "engagements": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "incident_template": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Computed: true, + }, + "impact": { + Type: schema.TypeInt, + Computed: true, + }, + "dedupe_string": { + Type: schema.TypeString, + Computed: true, + }, + "incident_tags": tftags.TagsSchemaComputed(), + "notification_target": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sns_topic_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "summary": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "integration": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pagerduty": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "service_id": { + Type: schema.TypeString, + Computed: true, + }, + "secret_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchemaComputed(), + }, + } +} + +const ( + DSNameResponsePlan = "Response Plan Data Source" +) + +func dataSourceResponsePlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*conns.AWSClient).SSMIncidentsClient() + + d.SetId(d.Get("arn").(string)) + + responsePlan, err := FindResponsePlanByID(ctx, client, d.Id()) + + if err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionReading, DSNameResponsePlan, d.Id(), err) + } + + if d, err := setResponsePlanResourceData(d, responsePlan); err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionReading, DSNameResponsePlan, d.Id(), err) + } + + tags, err := ListTags(ctx, client, d.Id()) + if err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionReading, DSNameResponsePlan, d.Id(), err) + } + + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + //lintignore:AWSR002 + if err := d.Set("tags", tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return create.DiagError(names.SSMIncidents, create.ErrActionSetting, DSNameResponsePlan, d.Id(), err) + } + + return nil +} diff --git a/internal/service/ssmincidents/response_plan_data_source_test.go b/internal/service/ssmincidents/response_plan_data_source_test.go new file mode 100644 index 00000000000..81d16e38b80 --- /dev/null +++ b/internal/service/ssmincidents/response_plan_data_source_test.go @@ -0,0 +1,330 @@ +package ssmincidents_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testResponsePlanDataSource_basic(t *testing.T) { + ctx := context.Background() + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rTitle := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_ssmincidents_response_plan.test" + resourceName := "aws_ssmincidents_response_plan.test" + + snsTopic1 := "aws_sns_topic.topic1" + snsTopic2 := "aws_sns_topic.topic2" + + displayName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + chatChannelTopic := "aws_sns_topic.channel_topic" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SSMIncidentsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMIncidentsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResponsePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccResponsePlanDataSourceConfig_basic( + rName, + rTitle, + snsTopic1, + snsTopic2, + displayName, + chatChannelTopic, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckResponsePlanExists(dataSourceName), + + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.title", dataSourceName, "incident_template.0.title"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.impact", dataSourceName, "incident_template.0.impact"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.dedupe_string", dataSourceName, "incident_template.0.dedupe_string"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.summary", dataSourceName, "incident_template.0.summary"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.incident_tags.%", dataSourceName, "incident_template.0.incident_tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.incident_tags.a", dataSourceName, "incident_template.0.incident_tags.a"), + resource.TestCheckResourceAttrPair(resourceName, "incident_template.0.incident_tags.b", dataSourceName, "incident_template.0.incident_tags.b"), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "incident_template.0.notification_target.*.sns_topic_arn", snsTopic1, "arn"), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "incident_template.0.notification_target.*.sns_topic_arn", snsTopic2, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "display_name", dataSourceName, "display_name"), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "chat_channel.0", chatChannelTopic, "arn"), + resource.TestCheckTypeSetElemAttrPair( + resourceName, + "engagements.0", + dataSourceName, + "engagements.0", + ), + resource.TestCheckTypeSetElemAttrPair( + dataSourceName, + "action.0.ssm_automation.0.document_name", + "aws_ssm_document.document", + "name", + ), + resource.TestCheckTypeSetElemAttrPair( + dataSourceName, + "action.0.ssm_automation.0.role_arn", + "aws_iam_role.role", + "arn", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.#", + dataSourceName, + "action.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.#", + dataSourceName, + "action.0.ssm_automation.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.document_version", + dataSourceName, + "action.0.ssm_automation.0.document_version", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.target_account", + dataSourceName, + "action.0.ssm_automation.0.target_account", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.parameter.#", + dataSourceName, + "action.0.ssm_automation.0.parameter.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.parameter.0.name", + dataSourceName, + "action.0.ssm_automation.0.parameter.0.name", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.parameter.0.values.#", + dataSourceName, + "action.0.ssm_automation.0.parameter.0.values.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.parameter.0.values.0", + dataSourceName, + "action.0.ssm_automation.0.parameter.0.values.0", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.parameter.0.values.1", + dataSourceName, + "action.0.ssm_automation.0.parameter.0.values.1", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.dynamic_parameters.#", + dataSourceName, + "action.0.ssm_automation.0.dynamic_parameters.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "action.0.ssm_automation.0.dynamic_parameters.anotherKey", + dataSourceName, + "action.0.ssm_automation.0.dynamic_parameters.anotherKey", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "integration.#", + dataSourceName, + "integration.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "integration.0.pagerduty.#", + dataSourceName, + "integration.0.pagerduty.#", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "integration.0.pagerduty.0.name", + dataSourceName, + "integration.0.pagerduty.0.name", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "integration.0.pagerduty.0.service_id", + dataSourceName, + "integration.0.pagerduty.0.service_id", + ), + resource.TestCheckResourceAttrPair( + resourceName, + "integration.0.pagerduty.0.secret_id", + dataSourceName, + "integration.0.pagerduty.0.secret_id", + ), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "tags.a", dataSourceName, "tags.a"), + resource.TestCheckResourceAttrPair(resourceName, "tags.b", dataSourceName, "tags.b"), + + acctest.MatchResourceAttrGlobalARN(dataSourceName, "arn", "ssm-incidents", regexp.MustCompile(`response-plan/+.`)), + ), + }, + }, + }) +} + +func testAccResponsePlanDataSourceConfig_basic( + name, + title, + topic1, + topic2, + displayName, + chatChannelTopic string) string { + //lintignore:AWSAT003 + //lintignore:AWSAT005 + return fmt.Sprintf(` +resource "aws_sns_topic" "topic1" {} +resource "aws_sns_topic" "topic2" {} +resource "aws_sns_topic" "channel_topic" {} + +resource "aws_ssmincidents_replication_set" "test_replication_set" { + region { + name = %[1]q + } +} + +resource "aws_iam_role" "role" { + assume_role_policy = < NOTE: A response plan implicitly depends on a replication set. If you configured your replication set in Terraform, +we recommend you add it to the `depends_on` argument for the Terraform ResponsePlan Resource. + +The following arguments are required: + +* `name` - (Required) The name of the response plan. + +The `incident_template` configuration block is required and supports the following arguments: + +* `title` - (Required) The title of a generated incident. +* `impact` - (Required) The impact value of a generated incident. The following values are supported: + * `1` - Severe Impact + * `2` - High Impact + * `3` - Medium Impact + * `4` - Low Impact + * `5` - No Impact +* `dedupe_string` - (Optional) A string used to stop Incident Manager from creating multiple incident records for the same incident. +* `incident_tags` - (Optional) The tags assigned to an incident template. When an incident starts, Incident Manager assigns the tags specified in the template to the incident. +* `summary` - (Optional) The summary of an incident. +* `notification_target` - (Optional) The Amazon Simple Notification Service (Amazon SNS) targets that this incident notifies when it is updated. The `notification_target` configuration block supports the following argument: + * `sns_topic_arn` - (Required) The ARN of the Amazon SNS topic. + +The following arguments are optional: + +* `tags` - (Optional) The tags applied to the response plan. +* `display_name` - (Optional) The long format of the response plan name. This field can contain spaces. +* `chat_channel` - (Optional) The Chatbot chat channel used for collaboration during an incident. +* `engagements` - (Optional) The Amazon Resource Name (ARN) for the contacts and escalation plans that the response plan engages during an incident. +* `action` - (Optional) The actions that the response plan starts at the beginning of an incident. + * `ssm_automation` - (Optional) The Systems Manager automation document to start as the runbook at the beginning of the incident. The following values are supported: + * `document_name` - (Required) The automation document's name. + * `role_arn` - (Required) The Amazon Resource Name (ARN) of the role that the automation document assumes when it runs commands. + * `document_version` - (Optional) The version of the automation document to use at runtime. + * `target_account` - (Optional) The account that the automation document runs in. This can be in either the management account or an application account. + * `parameter` - (Optional) The key-value pair parameters to use when the automation document runs. The following values are supported: + * `name` - The name of parameter. + * `values` - The values for the associated parameter name. + * `dynamic_parameters` - (Optional) The key-value pair to resolve dynamic parameter values when processing a Systems Manager Automation runbook. +* `integration` - (Optional) Information about third-party services integrated into the response plan. The following values are supported: + * `pagerduty` - (Optional) Details about the PagerDuty configuration for a response plan. The following values are supported: + * `name` - (Required) The name of the PagerDuty configuration. + * `service_id` - (Required) The ID of the PagerDuty service that the response plan associated with the incident at launch. + * `secret_id` - (Required) The ID of the AWS Secrets Manager secret that stores your PagerDuty key — either a General Access REST API Key or User Token REST API Key — and other user credentials. + +For more information about the constraints for each field, see [CreateResponsePlan](https://docs.aws.amazon.com/incident-manager/latest/APIReference/API_CreateResponsePlan.html) in the *AWS Systems Manager Incident Manager API Reference*. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The ARN of the response plan. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Import + +To import an Incident Manager response plan, specify the response plan ARN. You can find the response plan ARN in the AWS Management Console. Use the following command to run the import operation: + +``` +$ terraform import aws_ssmincidents_response_plan.responsePlanName ARNValue +```