diff --git a/.changelog/26123.txt b/.changelog/26123.txt new file mode 100644 index 00000000000..0ad95ba0a57 --- /dev/null +++ b/.changelog/26123.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_account_primary_contact +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 8f8aa8ecb59..b7298b2a33a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go v1.44.251 github.com/aws/aws-sdk-go-v2 v1.18.0 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 + github.com/aws/aws-sdk-go-v2/service/account v1.10.5 github.com/aws/aws-sdk-go-v2/service/auditmanager v1.24.6 github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.1.3 github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.11.10 diff --git a/go.sum b/go.sum index 8b26e92a76c..0cf89d13ca5 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7im github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 h1:J4xhFd6zHhdF9jPP0FQJ6WknzBboGMBNjKOv4iTuw4A= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29/go.mod h1:TwuqRBGzxjQJIwH16/fOZodwXt2Zxa9/cwJC5ke4j7s= +github.com/aws/aws-sdk-go-v2/service/account v1.10.5 h1:K+Od2Oz2nhQJx3e3Q+ziU11avIlINB0ngfsdc7O2X3M= +github.com/aws/aws-sdk-go-v2/service/account v1.10.5/go.mod h1:sxLUXrqYXCfOBPBBk0azv+UOoFsnrQ9G1ZcICrb9O+0= github.com/aws/aws-sdk-go-v2/service/auditmanager v1.24.6 h1:UNaqp6XOs26fmBNASN9SF23H3DytHaaRQ/hF/BJNnes= github.com/aws/aws-sdk-go-v2/service/auditmanager v1.24.6/go.mod h1:HtY67X+mN8oq2UMidOuIcXn+XWFyGYnpTvEoNGQBxc0= github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.1.3 h1:naHZ6PN42l4A8y1uICWSn4eILezIax7r6iSaqsL6tYg= diff --git a/internal/conns/awsclient_gen.go b/internal/conns/awsclient_gen.go index cba503ba373..9abcf7aec8b 100644 --- a/internal/conns/awsclient_gen.go +++ b/internal/conns/awsclient_gen.go @@ -4,6 +4,7 @@ package conns import ( "net/http" + "github.com/aws/aws-sdk-go-v2/service/account" "github.com/aws/aws-sdk-go-v2/service/auditmanager" "github.com/aws/aws-sdk-go-v2/service/cleanrooms" "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" @@ -39,7 +40,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/vpclattice" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/accessanalyzer" - "github.com/aws/aws-sdk-go/service/account" "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/alexaforbusiness" @@ -359,7 +359,7 @@ type AWSClient struct { apigatewaymanagementapiConn *apigatewaymanagementapi.ApiGatewayManagementApi apigatewayv2Conn *apigatewayv2.ApiGatewayV2 accessanalyzerConn *accessanalyzer.AccessAnalyzer - accountConn *account.Account + accountClient *account.Client alexaforbusinessConn *alexaforbusiness.AlexaForBusiness amplifyConn *amplify.Amplify amplifybackendConn *amplifybackend.AmplifyBackend @@ -700,8 +700,8 @@ func (client *AWSClient) AccessAnalyzerConn() *accessanalyzer.AccessAnalyzer { return client.accessanalyzerConn } -func (client *AWSClient) AccountConn() *account.Account { - return client.accountConn +func (client *AWSClient) AccountClient() *account.Client { + return client.accountClient } func (client *AWSClient) AlexaForBusinessConn() *alexaforbusiness.AlexaForBusiness { diff --git a/internal/conns/config_gen.go b/internal/conns/config_gen.go index 7c706bce642..e93c3af255d 100644 --- a/internal/conns/config_gen.go +++ b/internal/conns/config_gen.go @@ -3,6 +3,7 @@ package conns import ( aws_sdkv2 "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/account" "github.com/aws/aws-sdk-go-v2/service/auditmanager" "github.com/aws/aws-sdk-go-v2/service/cleanrooms" "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" @@ -38,7 +39,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/accessanalyzer" - "github.com/aws/aws-sdk-go/service/account" "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/alexaforbusiness" @@ -331,7 +331,6 @@ func (c *Config) sdkv1Conns(client *AWSClient, sess *session.Session) { client.apigatewaymanagementapiConn = apigatewaymanagementapi.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.APIGatewayManagementAPI])})) client.apigatewayv2Conn = apigatewayv2.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.APIGatewayV2])})) client.accessanalyzerConn = accessanalyzer.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.AccessAnalyzer])})) - client.accountConn = account.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.Account])})) client.alexaforbusinessConn = alexaforbusiness.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.AlexaForBusiness])})) client.amplifyConn = amplify.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.Amplify])})) client.amplifybackendConn = amplifybackend.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints[names.AmplifyBackend])})) @@ -610,6 +609,11 @@ func (c *Config) sdkv1Conns(client *AWSClient, sess *session.Session) { // sdkv2Conns initializes AWS SDK for Go v2 clients. func (c *Config) sdkv2Conns(client *AWSClient, cfg aws_sdkv2.Config) { + client.accountClient = account.NewFromConfig(cfg, func(o *account.Options) { + if endpoint := c.Endpoints[names.Account]; endpoint != "" { + o.EndpointResolver = account.EndpointResolverFromURL(endpoint) + } + }) client.auditmanagerClient = auditmanager.NewFromConfig(cfg, func(o *auditmanager.Options) { if endpoint := c.Endpoints[names.AuditManager]; endpoint != "" { o.EndpointResolver = auditmanager.EndpointResolverFromURL(endpoint) diff --git a/internal/service/account/account_test.go b/internal/service/account/account_test.go index 898f0d31c0b..d4664cd9fb5 100644 --- a/internal/service/account/account_test.go +++ b/internal/service/account/account_test.go @@ -15,6 +15,9 @@ func TestAccAccount_serial(t *testing.T) { "disappears": testAccAlternateContact_disappears, "AccountID": testAccAlternateContact_accountID, }, + "PrimaryContact": { + "basic": testAccPrimaryContact_basic, + }, } acctest.RunSerialTests2Levels(t, testCases, 0) diff --git a/internal/service/account/alternate_contact.go b/internal/service/account/alternate_contact.go index 29a3a19b84c..0f67e04aef7 100644 --- a/internal/service/account/alternate_contact.go +++ b/internal/service/account/alternate_contact.go @@ -6,14 +6,19 @@ import ( "log" "regexp" "strings" + "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/account" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/account" + "github.com/aws/aws-sdk-go-v2/service/account/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkretry "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "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" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/service/account/retry" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -25,29 +30,30 @@ func ResourceAlternateContact() *schema.Resource { ReadWithoutTimeout: resourceAlternateContactRead, UpdateWithoutTimeout: resourceAlternateContactUpdate, DeleteWithoutTimeout: resourceAlternateContactDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(alternateContactCreateTimeout), - Update: schema.DefaultTimeout(alternateContactUpdateTimeout), - Delete: schema.DefaultTimeout(alternateContactDeleteTimeout), + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), }, Schema: map[string]*schema.Schema{ - "alternate_contact_type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice(account.AlternateContactType_Values(), false), - }, "account_id": { Type: schema.TypeString, Optional: true, ForceNew: true, ValidateFunc: verify.ValidAccountID, }, + "alternate_contact_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.AlternateContactType](), + }, "email_address": { Type: schema.TypeString, Required: true, @@ -73,41 +79,44 @@ func ResourceAlternateContact() *schema.Resource { } func resourceAlternateContactCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - conn := meta.(*conns.AWSClient).AccountConn() + conn := meta.(*conns.AWSClient).AccountClient() + accountID := d.Get("account_id").(string) contactType := d.Get("alternate_contact_type").(string) + id := AlternateContactCreateResourceID(accountID, contactType) input := &account.PutAlternateContactInput{ - AlternateContactType: aws.String(contactType), + AlternateContactType: types.AlternateContactType(contactType), EmailAddress: aws.String(d.Get("email_address").(string)), Name: aws.String(d.Get("name").(string)), PhoneNumber: aws.String(d.Get("phone_number").(string)), Title: aws.String(d.Get("title").(string)), } - accountID := d.Get("account_id").(string) if accountID != "" { input.AccountId = aws.String(accountID) } - id := AlternateContactCreateResourceID(accountID, contactType) - log.Printf("[DEBUG] Creating Account Alternate Contact: %s", input) - _, err := conn.PutAlternateContactWithContext(ctx, input) + _, err := conn.PutAlternateContact(ctx, input) if err != nil { - return diag.Errorf("error creating Account Alternate Contact (%s): %s", id, err) + return diag.Errorf("creating Account Alternate Contact (%s): %s", id, err) } d.SetId(id) - if _, err := waitAlternateContactCreated(ctx, conn, accountID, contactType, d.Timeout(schema.TimeoutCreate)); err != nil { - return diag.Errorf("error waiting for Account Alternate Contact (%s) create: %s", d.Id(), err) + _, err = retry.UntilFoundN(ctx, d.Timeout(schema.TimeoutCreate), func() (*types.AlternateContact, error) { + return FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) + }, 2) //nolint:gomnd + + if err != nil { + return diag.Errorf("waiting for Account Alternate Contact (%s) create: %s", d.Id(), err) } return resourceAlternateContactRead(ctx, d, meta) } func resourceAlternateContactRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - conn := meta.(*conns.AWSClient).AccountConn() + conn := meta.(*conns.AWSClient).AccountClient() accountID, contactType, err := AlternateContactParseResourceID(d.Id()) @@ -115,7 +124,7 @@ func resourceAlternateContactRead(ctx context.Context, d *schema.ResourceData, m return diag.FromErr(err) } - output, err := FindAlternateContactByAccountIDAndContactType(ctx, conn, accountID, contactType) + output, err := FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] Account Alternate Contact (%s) not found, removing from state", d.Id()) @@ -124,7 +133,7 @@ func resourceAlternateContactRead(ctx context.Context, d *schema.ResourceData, m } if err != nil { - return diag.Errorf("error reading Account Alternate Contact (%s): %s", d.Id(), err) + return diag.Errorf("reading Account Alternate Contact (%s): %s", d.Id(), err) } d.Set("account_id", accountID) @@ -138,7 +147,7 @@ func resourceAlternateContactRead(ctx context.Context, d *schema.ResourceData, m } func resourceAlternateContactUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - conn := meta.(*conns.AWSClient).AccountConn() + conn := meta.(*conns.AWSClient).AccountClient() accountID, contactType, err := AlternateContactParseResourceID(d.Id()) @@ -152,7 +161,7 @@ func resourceAlternateContactUpdate(ctx context.Context, d *schema.ResourceData, title := d.Get("title").(string) input := &account.PutAlternateContactInput{ - AlternateContactType: aws.String(contactType), + AlternateContactType: types.AlternateContactType(contactType), EmailAddress: aws.String(email), Name: aws.String(name), PhoneNumber: aws.String(phone), @@ -163,22 +172,36 @@ func resourceAlternateContactUpdate(ctx context.Context, d *schema.ResourceData, input.AccountId = aws.String(accountID) } - log.Printf("[DEBUG] Updating Account Alternate Contact: %s", input) - _, err = conn.PutAlternateContactWithContext(ctx, input) + _, err = conn.PutAlternateContact(ctx, input) if err != nil { - return diag.Errorf("error updating Account Alternate Contact (%s): %s", d.Id(), err) + return diag.Errorf("updating Account Alternate Contact (%s): %s", d.Id(), err) } - if err := waitAlternateContactUpdated(ctx, conn, accountID, contactType, email, name, phone, title, d.Timeout(schema.TimeoutUpdate)); err != nil { - return diag.Errorf("error waiting for Account Alternate Contact (%s) update: %s", d.Id(), err) + _, err = retry.If(ctx, d.Timeout(schema.TimeoutUpdate), + func() (*types.AlternateContact, error) { + return FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) + }, + func(v *types.AlternateContact, err error) (bool, error) { + if err != nil { + return false, err + } + + equal := email == aws.ToString(v.EmailAddress) && name == aws.ToString(v.Name) && phone == aws.ToString(v.PhoneNumber) && title == aws.ToString(v.Title) + + return !equal, nil + }, + ) + + if err != nil { + return diag.Errorf("waiting for Account Alternate Contact (%s) update: %s", d.Id(), err) } return resourceAlternateContactRead(ctx, d, meta) } func resourceAlternateContactDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - conn := meta.(*conns.AWSClient).AccountConn() + conn := meta.(*conns.AWSClient).AccountClient() accountID, contactType, err := AlternateContactParseResourceID(d.Id()) @@ -187,31 +210,62 @@ func resourceAlternateContactDelete(ctx context.Context, d *schema.ResourceData, } input := &account.DeleteAlternateContactInput{ - AlternateContactType: aws.String(contactType), + AlternateContactType: types.AlternateContactType(contactType), } - if accountID != "" { input.AccountId = aws.String(accountID) } log.Printf("[DEBUG] Deleting Account Alternate Contact: %s", d.Id()) - _, err = conn.DeleteAlternateContactWithContext(ctx, input) + _, err = conn.DeleteAlternateContact(ctx, input) - if tfawserr.ErrCodeEquals(err, account.ErrCodeResourceNotFoundException) { + if errs.IsA[*types.ResourceNotFoundException](err) { return nil } if err != nil { - return diag.Errorf("error deleting Account Alternate Contact (%s): %s", d.Id(), err) + return diag.Errorf("deleting Account Alternate Contact (%s): %s", d.Id(), err) } - if err := waitAlternateContactDeleted(ctx, conn, accountID, contactType, d.Timeout(schema.TimeoutDelete)); err != nil { - return diag.Errorf("error waiting for Account Alternate Contact (%s) delete: %s", d.Id(), err) + _, err = retry.UntilNotFound(ctx, d.Timeout(schema.TimeoutDelete), func() (*types.AlternateContact, error) { + return FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) + }) + + if err != nil { + return diag.Errorf("waiting for Account Alternate Contact (%s) delete: %s", d.Id(), err) } return nil } +func FindAlternateContactByTwoPartKey(ctx context.Context, conn *account.Client, accountID, contactType string) (*types.AlternateContact, error) { + input := &account.GetAlternateContactInput{ + AlternateContactType: types.AlternateContactType(contactType), + } + if accountID != "" { + input.AccountId = aws.String(accountID) + } + + output, err := conn.GetAlternateContact(ctx, input) + + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil, &sdkretry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AlternateContact == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.AlternateContact, nil +} + const alternateContactResourceIDSeparator = "/" func AlternateContactCreateResourceID(accountID, contactType string) string { diff --git a/internal/service/account/alternate_contact_test.go b/internal/service/account/alternate_contact_test.go index d3426390f8c..535fde47c38 100644 --- a/internal/service/account/alternate_contact_test.go +++ b/internal/service/account/alternate_contact_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/service/account" + "github.com/aws/aws-sdk-go-v2/service/account/types" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" tfaccount "github.com/hashicorp/terraform-provider-aws/internal/service/account" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" ) func testAccAlternateContact_basic(t *testing.T) { @@ -26,7 +27,7 @@ func testAccAlternateContact_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, account.EndpointsID), + ErrorCheck: acctest.ErrorCheck(t, names.AccountEndpointID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckAlternateContactDestroy(ctx), Steps: []resource.TestStep{ @@ -72,7 +73,7 @@ func testAccAlternateContact_disappears(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, account.EndpointsID), + ErrorCheck: acctest.ErrorCheck(t, names.AccountEndpointID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckAlternateContactDestroy(ctx), Steps: []resource.TestStep{ @@ -104,7 +105,7 @@ func testAccAlternateContact_accountID(t *testing.T) { // nosemgrep:ci.account-i acctest.PreCheckOrganizationManagementAccount(ctx, t) testAccPreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, account.EndpointsID), + ErrorCheck: acctest.ErrorCheck(t, names.AccountEndpointID), ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), CheckDestroy: testAccCheckAlternateContactDestroy(ctx), Steps: []resource.TestStep{ @@ -143,7 +144,7 @@ func testAccAlternateContact_accountID(t *testing.T) { // nosemgrep:ci.account-i func testAccCheckAlternateContactDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).AccountConn() + conn := acctest.Provider.Meta().(*conns.AWSClient).AccountClient() for _, rs := range s.RootModule().Resources { if rs.Type != "aws_account_alternate_contact" { @@ -156,7 +157,7 @@ func testAccCheckAlternateContactDestroy(ctx context.Context) resource.TestCheck return err } - _, err = tfaccount.FindAlternateContactByAccountIDAndContactType(ctx, conn, accountID, contactType) + _, err = tfaccount.FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) if tfresource.NotFound(err) { continue @@ -190,9 +191,9 @@ func testAccCheckAlternateContactExists(ctx context.Context, n string) resource. return err } - conn := acctest.Provider.Meta().(*conns.AWSClient).AccountConn() + conn := acctest.Provider.Meta().(*conns.AWSClient).AccountClient() - _, err = tfaccount.FindAlternateContactByAccountIDAndContactType(ctx, conn, accountID, contactType) + _, err = tfaccount.FindAlternateContactByTwoPartKey(ctx, conn, accountID, contactType) return err } @@ -230,9 +231,9 @@ resource "aws_account_alternate_contact" "test" { } func testAccPreCheck(ctx context.Context, t *testing.T) { - conn := acctest.Provider.Meta().(*conns.AWSClient).AccountConn() + conn := acctest.Provider.Meta().(*conns.AWSClient).AccountClient() - _, err := tfaccount.FindAlternateContactByAccountIDAndContactType(ctx, conn, "", account.AlternateContactTypeOperations) + _, err := tfaccount.FindAlternateContactByTwoPartKey(ctx, conn, "", string(types.AlternateContactTypeOperations)) if acctest.PreCheckSkipError(err) { t.Skipf("skipping acceptance testing: %s", err) diff --git a/internal/service/account/find.go b/internal/service/account/find.go deleted file mode 100644 index 5a267f809d4..00000000000 --- a/internal/service/account/find.go +++ /dev/null @@ -1,40 +0,0 @@ -package account - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/account" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -func FindAlternateContactByAccountIDAndContactType(ctx context.Context, conn *account.Account, accountID, contactType string) (*account.AlternateContact, error) { // nosemgrep:ci.account-in-func-name - input := &account.GetAlternateContactInput{ - AlternateContactType: aws.String(contactType), - } - - if accountID != "" { - input.AccountId = aws.String(accountID) - } - - output, err := conn.GetAlternateContactWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, account.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil || output.AlternateContact == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output.AlternateContact, nil -} diff --git a/internal/service/account/primary_contact.go b/internal/service/account/primary_contact.go new file mode 100644 index 00000000000..fedc42faf49 --- /dev/null +++ b/internal/service/account/primary_contact.go @@ -0,0 +1,207 @@ +package account + +import ( + "context" + "log" + "regexp" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/account" + "github.com/aws/aws-sdk-go-v2/service/account/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_account_primary_contact") +func ResourcePrimaryContact() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourcePrimaryContactPut, + ReadWithoutTimeout: resourcePrimaryContactRead, + UpdateWithoutTimeout: resourcePrimaryContactPut, + DeleteWithoutTimeout: schema.NoopContext, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "address_line_1": { + Type: schema.TypeString, + Required: true, + }, + "address_line_2": { + Type: schema.TypeString, + Optional: true, + }, + "address_line_3": { + Type: schema.TypeString, + Optional: true, + }, + "city": { + Type: schema.TypeString, + Required: true, + }, + "company_name": { + Type: schema.TypeString, + Optional: true, + }, + "country_code": { + Type: schema.TypeString, + Required: true, + }, + "district_or_county": { + Type: schema.TypeString, + Optional: true, + }, + "full_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 64), + }, + "phone_number": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[\s0-9()+-]+$`), "must be a valid phone number"), + }, + "postal_code": { + Type: schema.TypeString, + Required: true, + }, + "state_or_region": { + Type: schema.TypeString, + Optional: true, + }, + "website_url": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourcePrimaryContactPut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).AccountClient() + + id := "default" + input := &account.PutContactInformationInput{ + ContactInformation: &types.ContactInformation{ + AddressLine1: aws.String(d.Get("address_line_1").(string)), + City: aws.String(d.Get("city").(string)), + CountryCode: aws.String(d.Get("country_code").(string)), + FullName: aws.String(d.Get("full_name").(string)), + PhoneNumber: aws.String(d.Get("phone_number").(string)), + PostalCode: aws.String(d.Get("postal_code").(string)), + }, + } + + if v, ok := d.GetOk("account_id"); ok { + id = v.(string) + input.AccountId = aws.String(id) + } + + if v, ok := d.GetOk("address_line_2"); ok { + input.ContactInformation.AddressLine2 = aws.String(v.(string)) + } + + if v, ok := d.GetOk("address_line_3"); ok { + input.ContactInformation.AddressLine3 = aws.String(v.(string)) + } + + if v, ok := d.GetOk("company_name"); ok { + input.ContactInformation.CompanyName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("district_or_county"); ok { + input.ContactInformation.DistrictOrCounty = aws.String(v.(string)) + } + + if v, ok := d.GetOk("state_or_region"); ok { + input.ContactInformation.StateOrRegion = aws.String(v.(string)) + } + + if v, ok := d.GetOk("website_url"); ok { + input.ContactInformation.WebsiteUrl = aws.String(v.(string)) + } + + _, err := conn.PutContactInformation(ctx, input) + + if err != nil { + return diag.Errorf("creating Account Primary Contact (%s): %s", id, err) + } + + if d.IsNewResource() { + d.SetId(id) + } + + return resourcePrimaryContactRead(ctx, d, meta) +} + +func resourcePrimaryContactRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).AccountClient() + + contactInformation, err := FindContactInformation(ctx, conn, d.Get("account_id").(string)) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Account Primary Contact (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading Account Primary Contact (%s): %s", d.Id(), err) + } + + d.Set("account_id", d.Get("account_id")) + d.Set("address_line_1", contactInformation.AddressLine1) + d.Set("address_line_2", contactInformation.AddressLine2) + d.Set("address_line_3", contactInformation.AddressLine3) + d.Set("city", contactInformation.City) + d.Set("company_name", contactInformation.CompanyName) + d.Set("country_code", contactInformation.CountryCode) + d.Set("district_or_county", contactInformation.DistrictOrCounty) + d.Set("full_name", contactInformation.FullName) + d.Set("phone_number", contactInformation.PhoneNumber) + d.Set("postal_code", contactInformation.PostalCode) + d.Set("state_or_region", contactInformation.StateOrRegion) + d.Set("website_url", contactInformation.WebsiteUrl) + + return nil +} + +func FindContactInformation(ctx context.Context, conn *account.Client, accountID string) (*types.ContactInformation, error) { + input := &account.GetContactInformationInput{} + if accountID != "" { + input.AccountId = aws.String(accountID) + } + + output, err := conn.GetContactInformation(ctx, input) + + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ContactInformation == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ContactInformation, nil +} diff --git a/internal/service/account/primary_contact_test.go b/internal/service/account/primary_contact_test.go new file mode 100644 index 00000000000..2cd447a62e1 --- /dev/null +++ b/internal/service/account/primary_contact_test.go @@ -0,0 +1,106 @@ +package account_test + +import ( + "context" + "fmt" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "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" + tfaccount "github.com/hashicorp/terraform-provider-aws/internal/service/account" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccPrimaryContact_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_account_primary_contact.test" + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.AccountEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccPrimaryConfig_basic(rName1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckPrimaryContactExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "account_id", ""), + resource.TestCheckResourceAttr(resourceName, "address_line_1", "123 Any Street"), + resource.TestCheckResourceAttr(resourceName, "city", "Seattle"), + resource.TestCheckResourceAttr(resourceName, "company_name", "Example Corp, Inc."), + resource.TestCheckResourceAttr(resourceName, "country_code", "US"), + resource.TestCheckResourceAttr(resourceName, "district_or_county", "King"), + resource.TestCheckResourceAttr(resourceName, "full_name", rName1), + resource.TestCheckResourceAttr(resourceName, "phone_number", "+64211111111"), + resource.TestCheckResourceAttr(resourceName, "postal_code", "98101"), + resource.TestCheckResourceAttr(resourceName, "state_or_region", "WA"), + resource.TestCheckResourceAttr(resourceName, "website_url", "https://www.examplecorp.com"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccPrimaryConfig_basic(rName2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckPrimaryContactExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "account_id", ""), + resource.TestCheckResourceAttr(resourceName, "address_line_1", "123 Any Street"), + resource.TestCheckResourceAttr(resourceName, "city", "Seattle"), + resource.TestCheckResourceAttr(resourceName, "company_name", "Example Corp, Inc."), + resource.TestCheckResourceAttr(resourceName, "country_code", "US"), + resource.TestCheckResourceAttr(resourceName, "district_or_county", "King"), + resource.TestCheckResourceAttr(resourceName, "full_name", rName2), + resource.TestCheckResourceAttr(resourceName, "phone_number", "+64211111111"), + resource.TestCheckResourceAttr(resourceName, "postal_code", "98101"), + resource.TestCheckResourceAttr(resourceName, "state_or_region", "WA"), + resource.TestCheckResourceAttr(resourceName, "website_url", "https://www.examplecorp.com"), + ), + }, + }, + }) +} + +func testAccCheckPrimaryContactExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Account Primary Contact ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).AccountClient() + + _, err := tfaccount.FindContactInformation(ctx, conn, rs.Primary.Attributes["account_id"]) + + return err + } +} + +func testAccPrimaryConfig_basic(name string) string { + return fmt.Sprintf(` +resource "aws_account_primary_contact" "test" { + address_line_1 = "123 Any Street" + city = "Seattle" + company_name = "Example Corp, Inc." + country_code = "US" + district_or_county = "King" + full_name = %[1]q + phone_number = "+64211111111" + postal_code = "98101" + state_or_region = "WA" + website_url = "https://www.examplecorp.com" +} +`, name) +} diff --git a/internal/service/account/retry/README.md b/internal/service/account/retry/README.md new file mode 100644 index 00000000000..359fc70ef2a --- /dev/null +++ b/internal/service/account/retry/README.md @@ -0,0 +1,13 @@ +# Retry Package + +The start of musings on a replacement for the Terraform Plugin SDK v2 `helper/retry` package. + +### Example Usage + +```go +for r := retry.Begin(); r.Continue(ctx); { + if doSomething() { + break + } +} +``` diff --git a/internal/service/account/retry/retry.go b/internal/service/account/retry/retry.go new file mode 100644 index 00000000000..6e5f94ac18c --- /dev/null +++ b/internal/service/account/retry/retry.go @@ -0,0 +1,76 @@ +package retry + +import ( + "context" + "math" + "math/rand" + "time" +) + +// Inspired by "github.com/ServiceWeaver/weaver/runtime/retry". + +// Options configure a retry loop. +// Before the ith iteration of the loop, retry.Continue() sleeps for a duraion of BackoffMinDuration * BackoffMultiplier**i, with added jitter. +type Options struct { + BackoffMinDuration time.Duration + BackoffMultiplier float64 // If specified, must be at least 1. +} + +var defaultOptions = Options{ + BackoffMinDuration: 10 * time.Millisecond, + BackoffMultiplier: 1.3, +} + +// Retry holds state for managing retry loops with exponential backoff and jitter. +type Retry struct { + options Options + attempt int +} + +// BeginWithOptions returns a new retry loop configured with the provided options. +func BeginWithOptions(options Options) *Retry { + return &Retry{options: options} +} + +// Begin returns a new retry loop configured with the default options. +func Begin() *Retry { + return BeginWithOptions(defaultOptions) +} + +// Continue sleeps for an exponentially increasing interval (with jitter). +// It stops its sleep early and returns false if context becomes done. +// If the return value is false, ctx.Err() is guaranteed to be non-nil. +// The first call does not sleep. +func (r *Retry) Continue(ctx context.Context) bool { + if r.attempt != 0 { + randomizedSleep(ctx, r.backoffDelay()) + } + r.attempt++ + return ctx.Err() == nil +} + +// Reset resets a Retry to its initial state. +// It's useful when you only want to retry a failing operation. +func (r *Retry) Reset() { + r.attempt = 0 +} + +func (r *Retry) backoffDelay() time.Duration { + mult := math.Pow(r.options.BackoffMultiplier, float64(r.attempt)) + return time.Duration(float64(r.options.BackoffMinDuration) * mult) +} + +func randomizedSleep(ctx context.Context, d time.Duration) { + const jitter = 0.4 + mult := 1 - jitter*rand.Float64() // Subtract up to 40%. + sleep(ctx, time.Duration(float64(d)*mult)) +} + +func sleep(ctx context.Context, d time.Duration) { + t := time.NewTimer(d) + select { + case <-ctx.Done(): + t.Stop() + case <-t.C: + } +} diff --git a/internal/service/account/retry/retry_test.go b/internal/service/account/retry/retry_test.go new file mode 100644 index 00000000000..241f45c3e29 --- /dev/null +++ b/internal/service/account/retry/retry_test.go @@ -0,0 +1,102 @@ +package retry + +import ( + "context" + "math" + "testing" + "time" +) + +// Inspired by "github.com/ServiceWeaver/weaver/runtime/retry". + +func TestRetry(t *testing.T) { + t.Parallel() + + ctx, cf := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) + defer cf() + var gaps []time.Duration + last := time.Now() + for r := Begin(); r.Continue(ctx); { + now := time.Now() + gap := now.Sub(last) + t.Logf("gap %v", gap) + gaps = append(gaps, gap) + last = now + } + if len(gaps) >= 100 { + t.Fatalf("too many retries (%d) in one second", len(gaps)) + } + if gaps[0] >= 100*time.Millisecond { + t.Fatalf("first retry took too long: %v", gaps[0]) + } + if gaps[len(gaps)-1] < 5*gaps[1] { + t.Fatalf("retries did not increase significantly: %v", gaps) + } +} + +func TestSleepFor(t *testing.T) { + t.Parallel() + + const N = 20 + const delay = time.Millisecond * 10 + const minDelay = delay - delay/4 + const maxDelay = delay * 2 + + over := 0 + for i := 0; i < N; i++ { + start := time.Now() + sleep(context.Background(), delay) + elapsed := time.Since(start) + t.Logf("sleep duration: %v", elapsed) + if elapsed < minDelay { + t.Errorf("sleep returned too early: %v, expecting %v", elapsed, delay) + } else if elapsed > maxDelay { + // Allow a couple of over-shoots to reduce flakiness due to slowness. + over++ + if over > 2 { + t.Errorf("sleep returned too late: %v, expecting %v", elapsed, delay) + } + } + } +} + +func TestSleepCancellation(t *testing.T) { + t.Parallel() + + const cancelDelay = time.Millisecond * 10 + const sleepDelay = time.Second + ctx, cf := context.WithTimeout(context.Background(), cancelDelay) + defer cf() + start := time.Now() + sleep(ctx, sleepDelay) + elapsed := time.Since(start) + if elapsed >= sleepDelay { + t.Errorf("sleep not cancelled") + } +} + +func TestRandomization(t *testing.T) { + t.Parallel() + + const N = 20 + const delay = time.Millisecond * 10 + sumSquares := 0.0 + var sum time.Duration + for i := 0; i < N; i++ { + start := time.Now() + randomizedSleep(context.Background(), delay) + elapsed := time.Since(start) + t.Logf("sleep duration: %v", elapsed) + diff := float64(elapsed - delay) + sum += elapsed + sumSquares += diff * diff + } + mean := time.Duration(float64(sum) / N) + if mean < delay/2 || mean >= delay*2 { + t.Errorf("average sleep interval was too different from specified delay (%v vs. %v)", mean, delay) + } + stdDevFraction := math.Sqrt(sumSquares/N) / float64(delay) + if stdDevFraction < 0.05 { + t.Errorf("sleep interval was too consistent (+- %.1f%%)", stdDevFraction*100) + } +} diff --git a/internal/service/account/retry/wrappers.go b/internal/service/account/retry/wrappers.go new file mode 100644 index 00000000000..836e825a370 --- /dev/null +++ b/internal/service/account/retry/wrappers.go @@ -0,0 +1,78 @@ +package retry + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +// If retries an operation until the timeout elapses or predicate indicates otherwise. +func If[T any](ctx context.Context, timeout time.Duration, op func() (T, error), predicate func(T, error) (bool, error)) (T, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for r := Begin(); r.Continue(ctx); { + t, err := op() + if retry, err := predicate(t, err); !retry { + return t, err + } + } + + var t T + return t, ctx.Err() +} + +// UntilFoundN retries an operation if it returns a retry.NotFoundError. +func UntilFoundN[T any](ctx context.Context, timeout time.Duration, op func() (T, error), continuousTargetOccurence int) (T, error) { + if continuousTargetOccurence < 1 { + continuousTargetOccurence = 1 + } + + targetOccurence := 0 + + t, err := If(ctx, timeout, op, func(_ T, err error) (bool, error) { + if err == nil { + targetOccurence++ + + if continuousTargetOccurence == targetOccurence { + return false, nil + } + + return true, nil + } + + if tfresource.NotFound(err) { + targetOccurence = 0 + + return true, err + } + + return false, err + }) + + return t, err +} + +// UntilNotFound retries an operation until it returns a retry.NotFoundError. +func UntilNotFound[T any](ctx context.Context, timeout time.Duration, op func() (T, error)) (T, error) { + t, err := If(ctx, timeout, op, func(_ T, err error) (bool, error) { + if err == nil { + return true, nil + } + + if tfresource.NotFound(err) { + return false, nil + } + + return false, err + }) + + if errors.Is(err, context.DeadlineExceeded) { + return t, fmt.Errorf("found resource: %w", err) + } + + return t, err +} diff --git a/internal/service/account/service_package_gen.go b/internal/service/account/service_package_gen.go index 2315fe234da..eb49ffe3a68 100644 --- a/internal/service/account/service_package_gen.go +++ b/internal/service/account/service_package_gen.go @@ -29,6 +29,10 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceAlternateContact, TypeName: "aws_account_alternate_contact", }, + { + Factory: ResourcePrimaryContact, + TypeName: "aws_account_primary_contact", + }, } } diff --git a/internal/service/account/status.go b/internal/service/account/status.go deleted file mode 100644 index f8907fda02c..00000000000 --- a/internal/service/account/status.go +++ /dev/null @@ -1,55 +0,0 @@ -package account - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/account" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -const ( - statusFound = "FOUND" - statusUpdated = "UPDATED" - statusNotUpdated = "NOT_UPDATED" -) - -func statusAlternateContact(ctx context.Context, conn *account.Account, accountID, contactType string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindAlternateContactByAccountIDAndContactType(ctx, conn, accountID, contactType) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - return output, statusFound, nil - } -} - -func statusAlternateContactUpdate(ctx context.Context, conn *account.Account, accountID, contactType, email, name, phone, title string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindAlternateContactByAccountIDAndContactType(ctx, conn, accountID, contactType) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - if email == aws.StringValue(output.EmailAddress) && - name == aws.StringValue(output.Name) && - phone == aws.StringValue(output.PhoneNumber) && - title == aws.StringValue(output.Title) { - return output, statusUpdated, nil - } - - return output, statusNotUpdated, nil - } -} diff --git a/internal/service/account/wait.go b/internal/service/account/wait.go deleted file mode 100644 index a5e1a005288..00000000000 --- a/internal/service/account/wait.go +++ /dev/null @@ -1,62 +0,0 @@ -package account - -import ( - "context" - "time" - - "github.com/aws/aws-sdk-go/service/account" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" -) - -const ( - alternateContactCreateTimeout = 5 * time.Minute - alternateContactUpdateTimeout = 5 * time.Minute - alternateContactDeleteTimeout = 5 * time.Minute -) - -func waitAlternateContactCreated(ctx context.Context, conn *account.Account, accountID, contactType string, timeout time.Duration) (*account.AlternateContact, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: []string{statusFound}, - Refresh: statusAlternateContact(ctx, conn, accountID, contactType), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*account.AlternateContact); ok { - return output, err - } - - return nil, err -} - -func waitAlternateContactUpdated(ctx context.Context, conn *account.Account, accountID, contactType, email, name, phone, title string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusNotUpdated}, - Target: []string{statusUpdated}, - Refresh: statusAlternateContactUpdate(ctx, conn, accountID, contactType, email, name, phone, title), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - _, err := stateConf.WaitForStateContext(ctx) - - return err -} - -func waitAlternateContactDeleted(ctx context.Context, conn *account.Account, accountID, contactType string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusFound}, - Target: []string{}, - Refresh: statusAlternateContact(ctx, conn, accountID, contactType), - Timeout: timeout, - } - - _, err := stateConf.WaitForStateContext(ctx) - - return err -} diff --git a/internal/service/kms/wait.go b/internal/service/kms/wait.go index 7aa5689f76e..38731a5df5d 100644 --- a/internal/service/kms/wait.go +++ b/internal/service/kms/wait.go @@ -45,7 +45,7 @@ func WaitIAMPropagation[T any](ctx context.Context, f func() (T, error)) (T, err return zero, err } - return outputRaw.(T), err + return outputRaw.(T), nil } func WaitKeyDeleted(ctx context.Context, conn *kms.KMS, id string) (*kms.KeyMetadata, error) { diff --git a/internal/tfresource/retry.go b/internal/tfresource/retry.go index 7df14b826d1..f2b1d93d47f 100644 --- a/internal/tfresource/retry.go +++ b/internal/tfresource/retry.go @@ -18,7 +18,7 @@ import ( // If the error is not retryable, returns a bool value of `false` and either no error (success state) or an error (not necessarily the error passed as the argument). type Retryable func(error) (bool, error) -// RetryWhen retries the function `f` when the error it returns satisfies `predicate`. +// RetryWhen retries the function `f` when the error it returns satisfies `retryable`. // `f` is retried until `timeout` expires. func RetryWhen(ctx context.Context, timeout time.Duration, f func() (interface{}, error), retryable Retryable) (interface{}, error) { var output interface{} diff --git a/names/names.go b/names/names.go index 07af87cca5a..ea6aa91eced 100644 --- a/names/names.go +++ b/names/names.go @@ -22,6 +22,7 @@ import ( // This "should" be defined by the AWS Go SDK v2, but currently isn't. const ( + AccountEndpointID = "account" AuditManagerEndpointID = "auditmanager" CloudWatchLogsEndpointID = "logs" ComprehendEndpointID = "comprehend" diff --git a/names/names_data.csv b/names/names_data.csv index afbaa0d49fb..db87855aba9 100644 --- a/names/names_data.csv +++ b/names/names_data.csv @@ -1,5 +1,5 @@ AWSCLIV2Command,AWSCLIV2CommandNoDashes,GoV1Package,GoV2Package,ProviderPackageActual,ProviderPackageCorrect,SplitPackageRealPackage,Aliases,ProviderNameUpper,GoV1ClientTypeName,SkipClientGenerate,ClientSDKV1,ClientSDKV2,ResourcePrefixActual,ResourcePrefixCorrect,FilePrefix,DocPrefix,HumanFriendly,Brand,Exclude,AllowedSubcategory,DeprecatedEnvVar,EnvVar,Note -account,account,account,account,,account,,,Account,Account,,1,,,aws_account_,,account_,Account Management,AWS,,,,, +account,account,account,account,,account,,,Account,Account,,,2,,aws_account_,,account_,Account Management,AWS,,,,, acm,acm,acm,acm,,acm,,,ACM,ACM,,1,,,aws_acm_,,acm_,ACM (Certificate Manager),AWS,,,,, acm-pca,acmpca,acmpca,acmpca,,acmpca,,,ACMPCA,ACMPCA,,1,,,aws_acmpca_,,acmpca_,ACM PCA (Certificate Manager Private Certificate Authority),AWS,,,,, alexaforbusiness,alexaforbusiness,alexaforbusiness,alexaforbusiness,,alexaforbusiness,,,AlexaForBusiness,AlexaForBusiness,,1,,,aws_alexaforbusiness_,,alexaforbusiness_,Alexa for Business,,,,,, diff --git a/website/docs/r/account_primary_contact.html.markdown b/website/docs/r/account_primary_contact.html.markdown new file mode 100644 index 00000000000..96f38e01bc4 --- /dev/null +++ b/website/docs/r/account_primary_contact.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "Account Management" +layout: "aws" +page_title: "AWS: aws_account_primary_contact" +description: |- + Manages the specified primary contact information associated with an AWS Account. +--- + +# Resource: aws_account_primary_contact + +Manages the specified primary contact information associated with an AWS Account. + +## Example Usage + +```terraform +resource "aws_account_primary_contact" "test" { + address_line_1 = "123 Any Street" + city = "Seattle" + company_name = "Example Corp, Inc." + country_code = "US" + district_or_county = "King" + full_name = "My Name" + phone_number = "+64211111111" + postal_code = "98101" + state_or_region = "WA" + website_url = "https://www.examplecorp.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Optional) The ID of the target account when managing member accounts. Will manage current user's account by default if omitted. +* `address_line_1` - (Required) The first line of the primary contact address. +* `address_line_2` - (Optional) The second line of the primary contact address, if any. +* `address_line_3` - (Optional) The third line of the primary contact address, if any. +* `city` - (Required) The city of the primary contact address. +* `company_name` - (Optional) The name of the company associated with the primary contact information, if any. +* `country_code` - (Required) The ISO-3166 two-letter country code for the primary contact address. +* `district_or_county` - (Optional) The district or county of the primary contact address, if any. +* `full_name` - (Required) The full name of the primary contact address. +* `phone_number` - (Required) The phone number of the primary contact information. The number will be validated and, in some countries, checked for activation. +* `postal_code` - (Required) The postal code of the primary contact address. +* `state_or_region` - (Optional) The state or region of the primary contact address. This field is required in selected countries. +* `website_url` - (Optional) The URL of the website associated with the primary contact information, if any. + +## Attributes Reference + +No additional attributes are exported.