Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: aws_ce_anomaly_subscription #25224

Merged
merged 22 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
11735e2
Add Initial aws_ce_anomaly_subscription resource
brittandeyoung Jun 7, 2022
f4ad7f3
Merge branch 'f-aws_ce_anomaly_monitor' into f-aws_ce_anomaly_subscri…
brittandeyoung Jun 7, 2022
698a98c
Added working basic test
brittandeyoung Jun 7, 2022
b0df680
ce: Amend anomaly subscription test and resource
brittandeyoung Jun 7, 2022
45df942
ce: Add docs, Amend resource and Amend tests
brittandeyoung Jun 8, 2022
eb52305
ce: Amend anomaly subscription tests and docs
brittandeyoung Jun 8, 2022
086c04d
ce: Amend anomaly subscription
brittandeyoung Jun 9, 2022
9173b36
Merge branch 'main' into f-aws_ce_anomaly_subscription
brittandeyoung Jun 9, 2022
887677d
ce: Amend Anomaly Subscription
brittandeyoung Jun 9, 2022
4fe99ba
ce: Amend Anomaly Subscription tests
brittandeyoung Jun 9, 2022
0206fdb
Amend acceptance tests to use shared base config for standard monitor
zhelding Jun 15, 2022
310fd53
Amend schema validation
zhelding Jun 15, 2022
7f41f08
Add disappearance acceptance test for anomaly subscription
zhelding Jun 15, 2022
6fbc0c1
Amend acceptance tests
zhelding Jun 15, 2022
6e94c67
Allow for user-defined account_id input
zhelding Jun 21, 2022
414ce0c
Amend anomaly subscriber acceptance test to accommodate TypeSet
zhelding Jun 21, 2022
377356d
Amend spelling on cost anomaly subscription docs
zhelding Jun 21, 2022
56c9ae3
Specify account_id as an input in anomaly_subscription docs
zhelding Jun 21, 2022
8530329
Merge branch 'main' into f-aws_ce_anomaly_subscription
zhelding Jun 21, 2022
7fda8b8
Add newline between acceptance test functions
zhelding Jun 21, 2022
20c4413
Amend test config names to comply with semgrep rules
zhelding Jun 21, 2022
aa8b09d
Rearrange cost explorer resources to group those re: anomalies
zhelding Jun 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/25224.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_ce_anomaly_subscription
```
5 changes: 3 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1042,8 +1042,9 @@ func Provider() *schema.Provider {
"aws_budgets_budget": budgets.ResourceBudget(),
"aws_budgets_budget_action": budgets.ResourceBudgetAction(),

"aws_ce_anomaly_monitor": ce.ResourceAnomalyMonitor(),
"aws_ce_cost_category": ce.ResourceCostCategory(),
"aws_ce_cost_category": ce.ResourceCostCategory(),
"aws_ce_anomaly_monitor": ce.ResourceAnomalyMonitor(),
"aws_ce_anomaly_subscription": ce.ResourceAnomalySubscription(),

"aws_chime_voice_connector": chime.ResourceVoiceConnector(),
"aws_chime_voice_connector_group": chime.ResourceVoiceConnectorGroup(),
Expand Down
291 changes: 291 additions & 0 deletions internal/service/ce/anomaly_subscription.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
package ce

import (
"context"
"regexp"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/costexplorer"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
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"
)

func ResourceAnomalySubscription() *schema.Resource {
return &schema.Resource{
CreateContext: resourceAnomalySubscriptionCreate,
ReadContext: resourceAnomalySubscriptionRead,
UpdateContext: resourceAnomalySubscriptionUpdate,
DeleteContext: resourceAnomalySubscriptionDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`[\\S\\s]*`), "Must be a valid AWS Account ID matching expression: [\\S\\s]*"),
},
"arn": {
Type: schema.TypeString,
Computed: true,
},
"frequency": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice(costexplorer.AnomalySubscriptionFrequency_Values(), false),
},
"monitor_arn_list": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`arn:aws[-a-z0-9]*:[a-z0-9]+:[-a-z0-9]*:[0-9]{12}:[-a-zA-Z0-9/:_]+`), "Must be a valid anomaly monitor ARN"),
},
},
"subscriber": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": {
Type: schema.TypeString,
Required: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{costexplorer.SubscriberTypeEmail, costexplorer.SubscriberTypeSns}, false),
},
},
},
},
"threshold": {
Type: schema.TypeFloat,
Required: true,
ValidateFunc: validation.FloatAtLeast(0.0),
},
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.All(
validation.StringLenBetween(1, 1024),
validation.StringMatch(regexp.MustCompile(`[\\S\\s]*`), "Must be a valid Anomaly Subscription Name matching expression: [\\S\\s]*")),
},
"tags": tftags.TagsSchema(),
"tags_all": tftags.TagsSchemaComputed(),
},

CustomizeDiff: verify.SetTagsDiff,
}
}

func resourceAnomalySubscriptionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).CEConn
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{})))

input := &costexplorer.CreateAnomalySubscriptionInput{
AnomalySubscription: &costexplorer.AnomalySubscription{
SubscriptionName: aws.String(d.Get("name").(string)),
Frequency: aws.String(d.Get("frequency").(string)),
MonitorArnList: aws.StringSlice(expandAnomalySubscriptionMonitorARNList(d.Get("monitor_arn_list").([]interface{}))),
Subscribers: expandAnomalySubscriptionSubscribers(d.Get("subscriber").(*schema.Set).List()),
Threshold: aws.Float64(d.Get("threshold").(float64)),
},
}

if v, ok := d.GetOk("account_id"); ok {
input.AnomalySubscription.AccountId = aws.String(v.(string))
}

if len(tags) > 0 {
input.ResourceTags = Tags(tags.IgnoreAWS())
}

resp, err := conn.CreateAnomalySubscriptionWithContext(ctx, input)

if err != nil {
return names.DiagError(names.CE, names.ErrActionCreating, ResAnomalySubscription, d.Id(), err)
}

if resp == nil || resp.SubscriptionArn == nil {
return diag.Errorf("creating Cost Explorer Anomaly Subscription resource (%s): empty output", d.Get("name").(string))
}

d.SetId(aws.StringValue(resp.SubscriptionArn))

return resourceAnomalySubscriptionRead(ctx, d, meta)
}

func resourceAnomalySubscriptionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).CEConn
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig

subscription, err := FindAnomalySubscriptionByARN(ctx, conn, d.Id())

if !d.IsNewResource() && tfresource.NotFound(err) {
names.LogNotFoundRemoveState(names.CE, names.ErrActionReading, ResAnomalySubscription, d.Id())
d.SetId("")
return nil
}

if err != nil {
return names.DiagError(names.CE, names.ErrActionReading, ResAnomalySubscription, d.Id(), err)
}

d.Set("account_id", subscription.AccountId)
d.Set("arn", subscription.SubscriptionArn)
d.Set("frequency", subscription.Frequency)
d.Set("monitor_arn_list", subscription.MonitorArnList)
d.Set("subscriber", flattenAnomalySubscriptionSubscribers(subscription.Subscribers))
d.Set("threshold", subscription.Threshold)
d.Set("name", subscription.SubscriptionName)

tags, err := ListTags(conn, aws.StringValue(subscription.SubscriptionArn))
tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

if err != nil {
return names.DiagError(names.CE, names.ErrActionReading, ResTags, d.Id(), err)
}

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return names.DiagError(names.CE, names.ErrActionUpdating, ResTags, d.Id(), err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return names.DiagError(names.CE, names.ErrActionUpdating, ResTags, d.Id(), err)
}

return nil
}

func resourceAnomalySubscriptionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).CEConn
requestUpdate := false

input := &costexplorer.UpdateAnomalySubscriptionInput{
SubscriptionArn: aws.String(d.Id()),
}

if d.HasChange("frequency") {
input.Frequency = aws.String(d.Get("frequency").(string))
requestUpdate = true
}

if d.HasChange("monitor_arn_list") {
input.MonitorArnList = aws.StringSlice(d.Get("monitor_arn_list").([]string))
requestUpdate = true
}

if d.HasChange("subscriber") {
input.Subscribers = expandAnomalySubscriptionSubscribers(d.Get("subscriber").(*schema.Set).List())
requestUpdate = true
}

if d.HasChange("threshold") {
input.Threshold = aws.Float64(d.Get("threshold").(float64))
requestUpdate = true
}

if d.HasChange("tags") {
o, n := d.GetChange("tags")

if err := UpdateTags(conn, d.Id(), o, n); err != nil {
return names.DiagError(names.CE, names.ErrActionUpdating, ResTags, d.Id(), err)
}
}

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")

if err := UpdateTags(conn, d.Id(), o, n); err != nil {
return names.DiagError(names.CE, names.ErrActionUpdating, ResTags, d.Id(), err)
}
}

if requestUpdate {
_, err := conn.UpdateAnomalySubscriptionWithContext(ctx, input)

if err != nil {
return names.DiagError(names.CE, names.ErrActionUpdating, ResAnomalySubscription, d.Id(), err)
}
}

return resourceAnomalySubscriptionRead(ctx, d, meta)
}

func resourceAnomalySubscriptionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).CEConn

_, err := conn.DeleteAnomalySubscriptionWithContext(ctx, &costexplorer.DeleteAnomalySubscriptionInput{SubscriptionArn: aws.String(d.Id())})

if err != nil && tfawserr.ErrCodeEquals(err, costexplorer.ErrCodeResourceNotFoundException) {
return nil
}

if err != nil {
return names.DiagError(names.CE, names.ErrActionDeleting, ResAnomalySubscription, d.Id(), err)
}

return nil
}

func expandAnomalySubscriptionMonitorARNList(rawMonitorArnList []interface{}) []string {
if len(rawMonitorArnList) == 0 {
return nil
}

var monitorArns []string

for _, arn := range rawMonitorArnList {

monitorArns = append(monitorArns, arn.(string))
}

return monitorArns
}

func expandAnomalySubscriptionSubscribers(rawSubscribers []interface{}) []*costexplorer.Subscriber {
if len(rawSubscribers) == 0 {
return nil
}

var subscribers []*costexplorer.Subscriber

for _, sub := range rawSubscribers {
rawSubMap := sub.(map[string]interface{})
subscriber := &costexplorer.Subscriber{Address: aws.String(rawSubMap["address"].(string)), Type: aws.String(rawSubMap["type"].(string))}
subscribers = append(subscribers, subscriber)
}

return subscribers
}

func flattenAnomalySubscriptionSubscribers(subscribers []*costexplorer.Subscriber) []interface{} {
if subscribers == nil {
return []interface{}{}
}

var rawSubscribers []interface{}
for _, subscriber := range subscribers {
rawSubscriber := map[string]interface{}{
"address": aws.StringValue(subscriber.Address),
"type": aws.StringValue(subscriber.Type),
}

rawSubscribers = append(rawSubscribers, rawSubscriber)
}

return rawSubscribers
}
Loading