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

r/servicecatalog_budget_resource_association: New resource #19452

Merged
merged 11 commits into from
May 20, 2021
Merged
29 changes: 29 additions & 0 deletions aws/internal/service/servicecatalog/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,32 @@ func ProductPortfolioAssociation(conn *servicecatalog.ServiceCatalog, acceptLang

return result, err
}

func BudgetResourceAssociation(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) (*servicecatalog.BudgetDetail, error) {
input := &servicecatalog.ListBudgetsForResourceInput{
ResourceId: aws.String(resourceID),
}

var result *servicecatalog.BudgetDetail

err := conn.ListBudgetsForResourcePages(input, func(page *servicecatalog.ListBudgetsForResourceOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, budget := range page.Budgets {
if budget == nil {
continue
}

if aws.StringValue(budget.BudgetName) == budgetName {
result = budget
return false
}
}

return !lastPage
})

return result, err
}
14 changes: 14 additions & 0 deletions aws/internal/service/servicecatalog/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ func ProductPortfolioAssociationParseID(id string) (string, string, string, erro
func ProductPortfolioAssociationCreateID(acceptLanguage, portfolioID, productID string) string {
return strings.Join([]string{acceptLanguage, portfolioID, productID}, ":")
}

func BudgetResourceAssociationParseID(id string) (string, string, error) {
parts := strings.SplitN(id, ":", 2)

if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("unexpected format of ID (%s), budgetName:resourceID", id)
}

return parts[0], parts[1], nil
}

func BudgetResourceAssociationID(budgetName, resourceID string) string {
return strings.Join([]string{budgetName, resourceID}, ":")
}
24 changes: 24 additions & 0 deletions aws/internal/service/servicecatalog/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,27 @@ func ServiceActionStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id
return output.ServiceActionDetail, servicecatalog.StatusAvailable, nil
}
}

func BudgetResourceAssociationStatus(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := finder.BudgetResourceAssociation(conn, budgetName, resourceID)

if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) {
return nil, StatusNotFound, &resource.NotFoundError{
Message: fmt.Sprintf("tag option resource association not found (%s): %s", tfservicecatalog.BudgetResourceAssociationID(budgetName, resourceID), err),
}
}

if err != nil {
return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing tag option resource association: %w", err)
}

if output == nil {
return nil, StatusNotFound, &resource.NotFoundError{
Message: fmt.Sprintf("finding tag option resource association (%s): empty response", tfservicecatalog.BudgetResourceAssociationID(budgetName, resourceID)),
}
}

return output, servicecatalog.StatusAvailable, err
}
}
33 changes: 33 additions & 0 deletions aws/internal/service/servicecatalog/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const (
ServiceActionReadyTimeout = 3 * time.Minute
ServiceActionDeleteTimeout = 3 * time.Minute

BudgetResourceAssociationReadyTimeout = 3 * time.Minute
BudgetResourceAssociationDeleteTimeout = 3 * time.Minute

StatusNotFound = "NOT_FOUND"
StatusUnavailable = "UNAVAILABLE"

Expand Down Expand Up @@ -300,3 +303,33 @@ func ServiceActionDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, i

return err
}

func BudgetResourceAssociationReady(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) (*servicecatalog.BudgetDetail, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{StatusNotFound, StatusUnavailable},
Target: []string{servicecatalog.StatusAvailable},
Refresh: BudgetResourceAssociationStatus(conn, budgetName, resourceID),
Timeout: BudgetResourceAssociationReadyTimeout,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*servicecatalog.BudgetDetail); ok {
return output, err
}

return nil, err
}

func BudgetResourceAssociationDeleted(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) error {
stateConf := &resource.StateChangeConf{
Pending: []string{servicecatalog.StatusAvailable},
Target: []string{StatusNotFound, StatusUnavailable},
Refresh: BudgetResourceAssociationStatus(conn, budgetName, resourceID),
Timeout: BudgetResourceAssociationDeleteTimeout,
}

_, err := stateConf.WaitForState()

return err
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@ func Provider() *schema.Provider {
"aws_securityhub_organization_admin_account": resourceAwsSecurityHubOrganizationAdminAccount(),
"aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(),
"aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(),
"aws_servicecatalog_budget_resource_association": resourceAwsServiceCatalogBudgetResourceAssociation(),
"aws_servicecatalog_constraint": resourceAwsServiceCatalogConstraint(),
"aws_servicecatalog_organizations_access": resourceAwsServiceCatalogOrganizationsAccess(),
"aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(),
Expand Down
146 changes: 146 additions & 0 deletions aws/resource_aws_servicecatalog_budget_resource_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package aws

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/servicecatalog"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsServiceCatalogBudgetResourceAssociation() *schema.Resource {
return &schema.Resource{
Create: resourceAwsServiceCatalogBudgetResourceAssociationCreate,
Read: resourceAwsServiceCatalogBudgetResourceAssociationRead,
Delete: resourceAwsServiceCatalogBudgetResourceAssociationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"budget_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"resource_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

func resourceAwsServiceCatalogBudgetResourceAssociationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).scconn

input := &servicecatalog.AssociateBudgetWithResourceInput{
BudgetName: aws.String(d.Get("budget_name").(string)),
ResourceId: aws.String(d.Get("resource_id").(string)),
}

var output *servicecatalog.AssociateBudgetWithResourceOutput
err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError {
var err error

output, err = conn.AssociateBudgetWithResource(input)

if tfawserr.ErrMessageContains(err, servicecatalog.ErrCodeInvalidParametersException, "profile does not exist") {
return resource.RetryableError(err)
}

if err != nil {
return resource.NonRetryableError(err)
}

return nil
})

if tfresource.TimedOut(err) {
output, err = conn.AssociateBudgetWithResource(input)
}

if err != nil {
return fmt.Errorf("error associating Service Catalog Budget with Resource: %w", err)
}

if output == nil {
return fmt.Errorf("error creating Service Catalog Budget Resource Association: empty response")
}

d.SetId(tfservicecatalog.BudgetResourceAssociationID(d.Get("budget_name").(string), d.Get("resource_id").(string)))

return resourceAwsServiceCatalogBudgetResourceAssociationRead(d, meta)
}

func resourceAwsServiceCatalogBudgetResourceAssociationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).scconn

budgetName, resourceID, err := tfservicecatalog.BudgetResourceAssociationParseID(d.Id())

if err != nil {
return fmt.Errorf("could not parse ID (%s): %w", d.Id(), err)
}

output, err := waiter.BudgetResourceAssociationReady(conn, budgetName, resourceID)

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] Service Catalog Budget Resource Association (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error describing Service Catalog Budget Resource Association (%s): %w", d.Id(), err)
}

if output == nil {
return fmt.Errorf("error getting Service Catalog Budget Resource Association (%s): empty response", d.Id())
}

d.Set("resource_id", resourceID)
d.Set("budget_name", output.BudgetName)

return nil
}

func resourceAwsServiceCatalogBudgetResourceAssociationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).scconn

budgetName, resourceID, err := tfservicecatalog.BudgetResourceAssociationParseID(d.Id())

if err != nil {
return fmt.Errorf("could not parse ID (%s): %w", d.Id(), err)
}

input := &servicecatalog.DisassociateBudgetFromResourceInput{
ResourceId: aws.String(resourceID),
BudgetName: aws.String(budgetName),
}

_, err = conn.DisassociateBudgetFromResource(input)

if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) {
return nil
}

if err != nil {
return fmt.Errorf("error disassociating Service Catalog Budget from Resource (%s): %w", d.Id(), err)
}

err = waiter.BudgetResourceAssociationDeleted(conn, budgetName, resourceID)

if err != nil && !tfresource.NotFound(err) {
return fmt.Errorf("error waiting for Service Catalog Budget Resource Disassociation (%s): %w", d.Id(), err)
}

return nil
}
Loading