diff --git a/.changelog/22487.txt b/.changelog/22487.txt new file mode 100644 index 00000000000..c0e7fbdf287 --- /dev/null +++ b/.changelog/22487.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_appsync_domain_name +``` + +```release-note:new-resource +aws_appsync_domain_name_api_association +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3ea0cf7fb29..5587b56c6a3 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -828,12 +828,14 @@ func Provider() *schema.Provider { "aws_appstream_user": appstream.ResourceUser(), "aws_appstream_user_stack_association": appstream.ResourceUserStackAssociation(), - "aws_appsync_api_cache": appsync.ResourceAPICache(), - "aws_appsync_api_key": appsync.ResourceAPIKey(), - "aws_appsync_datasource": appsync.ResourceDataSource(), - "aws_appsync_function": appsync.ResourceFunction(), - "aws_appsync_graphql_api": appsync.ResourceGraphQLAPI(), - "aws_appsync_resolver": appsync.ResourceResolver(), + "aws_appsync_api_cache": appsync.ResourceAPICache(), + "aws_appsync_api_key": appsync.ResourceAPIKey(), + "aws_appsync_datasource": appsync.ResourceDataSource(), + "aws_appsync_domain_name": appsync.ResourceDomainName(), + "aws_appsync_domain_name_api_association": appsync.ResourceDomainNameApiAssociation(), + "aws_appsync_function": appsync.ResourceFunction(), + "aws_appsync_graphql_api": appsync.ResourceGraphQLAPI(), + "aws_appsync_resolver": appsync.ResourceResolver(), "aws_athena_database": athena.ResourceDatabase(), "aws_athena_named_query": athena.ResourceNamedQuery(), diff --git a/internal/service/appsync/appsync_test.go b/internal/service/appsync/appsync_test.go index 423ae1e39fd..4b89788ea00 100644 --- a/internal/service/appsync/appsync_test.go +++ b/internal/service/appsync/appsync_test.go @@ -1,6 +1,7 @@ package appsync_test import ( + "os" "testing" ) @@ -81,6 +82,15 @@ func TestAccAppSync_serial(t *testing.T) { "basic": testAccAppSyncApiCache_basic, "disappears": testAccAppSyncApiCache_disappears, }, + "DomainName": { + "basic": testAccAppSyncDomainName_basic, + "disappears": testAccAppSyncDomainName_disappears, + "description": testAccAppSyncDomainName_description, + }, + "DomainNameAssociation": { + "basic": testAccAppSyncDomainNameApiAssociation_basic, + "disappears": testAccAppSyncDomainNameApiAssociation_disappears, + }, } for group, m := range testCases { @@ -95,3 +105,15 @@ func TestAccAppSync_serial(t *testing.T) { }) } } + +func getAppsyncCertDomain(t *testing.T) string { + value := os.Getenv("AWS_APPSYNC_DOMAIN_NAME_CERTIFICATE_DOMAIN") + if value == "" { + t.Skip( + "Environment variable AWS_APPSYNC_DOMAIN_NAME_CERTIFICATE_DOMAIN is not set. " + + "This environment variable must be set to any non-empty value " + + "to enable the test.") + } + + return value +} diff --git a/internal/service/appsync/domain_name.go b/internal/service/appsync/domain_name.go new file mode 100644 index 00000000000..3a9487e5a29 --- /dev/null +++ b/internal/service/appsync/domain_name.go @@ -0,0 +1,145 @@ +package appsync + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceDomainName() *schema.Resource { + + return &schema.Resource{ + Create: resourceDomainNameCreate, + Read: resourceDomainNameRead, + Update: resourceDomainNameUpdate, + Delete: resourceDomainNameDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "appsync_domain_name": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "hosted_zone_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceDomainNameCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + params := &appsync.CreateDomainNameInput{ + CertificateArn: aws.String(d.Get("certificate_arn").(string)), + Description: aws.String(d.Get("description").(string)), + DomainName: aws.String(d.Get("domain_name").(string)), + } + + resp, err := conn.CreateDomainName(params) + if err != nil { + return fmt.Errorf("error creating Appsync Domain Name: %w", err) + } + + d.SetId(aws.StringValue(resp.DomainNameConfig.DomainName)) + + return resourceDomainNameRead(d, meta) +} + +func resourceDomainNameRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + domainName, err := FindDomainNameByID(conn, d.Id()) + if domainName == nil && !d.IsNewResource() { + log.Printf("[WARN] AppSync Domain Name (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting Appsync Domain Name %q: %w", d.Id(), err) + } + + d.Set("domain_name", domainName.DomainName) + d.Set("description", domainName.Description) + d.Set("certificate_arn", domainName.CertificateArn) + d.Set("hosted_zone_id", domainName.HostedZoneId) + d.Set("appsync_domain_name", domainName.AppsyncDomainName) + + return nil +} + +func resourceDomainNameUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + params := &appsync.UpdateDomainNameInput{ + DomainName: aws.String(d.Id()), + } + + if d.HasChange("description") { + params.Description = aws.String(d.Get("description").(string)) + } + + _, err := conn.UpdateDomainName(params) + if err != nil { + return fmt.Errorf("error updating Appsync Domain Name %q: %w", d.Id(), err) + } + + return resourceDomainNameRead(d, meta) + +} + +func resourceDomainNameDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + input := &appsync.DeleteDomainNameInput{ + DomainName: aws.String(d.Id()), + } + + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteDomainName(input) + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeConcurrentModificationException) { + return resource.RetryableError(fmt.Errorf("deleting Appsync Domain Name %q: %w", d.Id(), err)) + } + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + if tfresource.TimedOut(err) { + _, err = conn.DeleteDomainName(input) + } + if err != nil { + return fmt.Errorf("error deleting Appsync Domain Name %q: %w", d.Id(), err) + } + + return nil +} diff --git a/internal/service/appsync/domain_name_api_association.go b/internal/service/appsync/domain_name_api_association.go new file mode 100644 index 00000000000..f70d7a22e9a --- /dev/null +++ b/internal/service/appsync/domain_name_api_association.go @@ -0,0 +1,120 @@ +package appsync + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +func ResourceDomainNameApiAssociation() *schema.Resource { + + return &schema.Resource{ + Create: resourceDomainNameApiAssociationCreate, + Read: resourceDomainNameApiAssociationRead, + Update: resourceDomainNameApiAssociationUpdate, + Delete: resourceDomainNameApiAssociationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "api_id": { + Type: schema.TypeString, + Required: true, + }, + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceDomainNameApiAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + params := &appsync.AssociateApiInput{ + ApiId: aws.String(d.Get("api_id").(string)), + DomainName: aws.String(d.Get("domain_name").(string)), + } + + resp, err := conn.AssociateApi(params) + if err != nil { + return fmt.Errorf("error creating Appsync Domain Name API Association: %w", err) + } + + d.SetId(aws.StringValue(resp.ApiAssociation.DomainName)) + + if err := waitDomainNameApiAssociation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Appsync Domain Name API (%s) Association: %w", d.Id(), err) + } + + return resourceDomainNameApiAssociationRead(d, meta) +} + +func resourceDomainNameApiAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + association, err := FindDomainNameApiAssociationByID(conn, d.Id()) + if association == nil && !d.IsNewResource() { + log.Printf("[WARN] Appsync Domain Name API Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting Appsync Domain Name API Association %q: %w", d.Id(), err) + } + + d.Set("domain_name", association.DomainName) + d.Set("api_id", association.ApiId) + + return nil +} + +func resourceDomainNameApiAssociationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + params := &appsync.AssociateApiInput{ + ApiId: aws.String(d.Get("api_id").(string)), + DomainName: aws.String(d.Get("domain_name").(string)), + } + + _, err := conn.AssociateApi(params) + if err != nil { + return fmt.Errorf("error creating Appsync Domain Name API Association: %w", err) + } + + if err := waitDomainNameApiAssociation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Appsync Domain Name API (%s) Association: %w", d.Id(), err) + } + + return resourceDomainNameApiAssociationRead(d, meta) +} + +func resourceDomainNameApiAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).AppSyncConn + + input := &appsync.DisassociateApiInput{ + DomainName: aws.String(d.Id()), + } + _, err := conn.DisassociateApi(input) + if err != nil { + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeNotFoundException) { + return nil + } + return fmt.Errorf("error deleting Appsync Domain Name API Association: %w", err) + } + + if err := waitDomainNameApiDisassociation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Appsync Domain Name API (%s) Disassociation: %w", d.Id(), err) + } + + return nil +} diff --git a/internal/service/appsync/domain_name_api_association_test.go b/internal/service/appsync/domain_name_api_association_test.go new file mode 100644 index 00000000000..b60048772ad --- /dev/null +++ b/internal/service/appsync/domain_name_api_association_test.go @@ -0,0 +1,174 @@ +package appsync_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + 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/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfappsync "github.com/hashicorp/terraform-provider-aws/internal/service/appsync" +) + +func testAccAppSyncDomainNameApiAssociation_basic(t *testing.T) { + var providers []*schema.Provider + var association appsync.ApiAssociation + appsyncCertDomain := getAppsyncCertDomain(t) + + rName := sdkacctest.RandString(8) + resourceName := "aws_appsync_domain_name_api_association.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckDomainNameApiAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDomainNameApiAssociationConfig(appsyncCertDomain, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameApiAssociationExists(resourceName, &association), + resource.TestCheckResourceAttrPair(resourceName, "domain_name", "aws_appsync_domain_name.test", "domain_name"), + resource.TestCheckResourceAttrPair(resourceName, "api_id", "aws_appsync_graphql_api.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppsyncDomainNameApiAssociationUpdatedConfig(appsyncCertDomain, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameApiAssociationExists(resourceName, &association), + resource.TestCheckResourceAttrPair(resourceName, "domain_name", "aws_appsync_domain_name.test", "domain_name"), + resource.TestCheckResourceAttrPair(resourceName, "api_id", "aws_appsync_graphql_api.test2", "id"), + ), + }, + }, + }) +} + +func testAccAppSyncDomainNameApiAssociation_disappears(t *testing.T) { + var association appsync.ApiAssociation + var providers []*schema.Provider + appsyncCertDomain := getAppsyncCertDomain(t) + + rName := sdkacctest.RandString(8) + resourceName := "aws_appsync_domain_name_api_association.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckDomainNameApiAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDomainNameApiAssociationConfig(appsyncCertDomain, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameApiAssociationExists(resourceName, &association), + acctest.CheckResourceDisappears(acctest.Provider, tfappsync.ResourceDomainNameApiAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckDomainNameApiAssociationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).AppSyncConn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appsync_domain_name" { + continue + } + + association, err := tfappsync.FindDomainNameApiAssociationByID(conn, rs.Primary.ID) + if err == nil { + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeNotFoundException) { + return nil + } + return err + } + + if association != nil && aws.StringValue(association.DomainName) == rs.Primary.ID { + return fmt.Errorf("Appsync Domain Name ID %q still exists", rs.Primary.ID) + } + + return nil + + } + return nil +} + +func testAccCheckDomainNameApiAssociationExists(resourceName string, DomainNameApiAssociation *appsync.ApiAssociation) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Appsync Domain Name Not found in state: %s", resourceName) + } + conn := acctest.Provider.Meta().(*conns.AWSClient).AppSyncConn + + association, err := tfappsync.FindDomainNameApiAssociationByID(conn, rs.Primary.ID) + if err != nil { + return err + } + + if association == nil || association.DomainName == nil { + return fmt.Errorf("Appsync Domain Name %q not found", rs.Primary.ID) + } + + *DomainNameApiAssociation = *association + + return nil + } +} + +func testAccAppsyncDomainNameApiAssociationBaseConfig(domain, rName string) string { + return acctest.ConfigAlternateRegionProvider() + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + provider = "awsalternate" + domain = "*.%[1]s" + most_recent = true +} + +resource "aws_appsync_domain_name" "test" { + domain_name = "%[2]s.%[1]s" + certificate_arn = data.aws_acm_certificate.test.arn +} + +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %[2]q +} +`, domain, rName) +} + +func testAccAppsyncDomainNameApiAssociationConfig(domain, rName string) string { + return testAccAppsyncDomainNameApiAssociationBaseConfig(domain, rName) + ` +resource "aws_appsync_domain_name_api_association" "test" { + api_id = aws_appsync_graphql_api.test.id + domain_name = aws_appsync_domain_name.test.domain_name +} +` +} + +func testAccAppsyncDomainNameApiAssociationUpdatedConfig(domain, rName string) string { + return testAccAppsyncDomainNameApiAssociationBaseConfig(domain, rName) + fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test2" { + authentication_type = "API_KEY" + name = "%[1]s-2" +} + +resource "aws_appsync_domain_name_api_association" "test" { + api_id = aws_appsync_graphql_api.test2.id + domain_name = aws_appsync_domain_name.test.domain_name +} +`, rName) +} diff --git a/internal/service/appsync/domain_name_test.go b/internal/service/appsync/domain_name_test.go new file mode 100644 index 00000000000..c2b78024d01 --- /dev/null +++ b/internal/service/appsync/domain_name_test.go @@ -0,0 +1,190 @@ +package appsync_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + 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/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfappsync "github.com/hashicorp/terraform-provider-aws/internal/service/appsync" +) + +func testAccAppSyncDomainName_basic(t *testing.T) { + var providers []*schema.Provider + var domainName appsync.DomainNameConfig + appsyncCertDomain := getAppsyncCertDomain(t) + + rName := sdkacctest.RandString(8) + acmCertificateResourceName := "data.aws_acm_certificate.test" + resourceName := "aws_appsync_domain_name.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckDomainNameDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDomainNameBasicConfig(rName, appsyncCertDomain), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameExists(resourceName, &domainName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", acmCertificateResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAppSyncDomainName_description(t *testing.T) { + var providers []*schema.Provider + var domainName appsync.DomainNameConfig + appsyncCertDomain := getAppsyncCertDomain(t) + + rName := sdkacctest.RandString(8) + resourceName := "aws_appsync_domain_name.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckDomainNameDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDomainNameDescriptionConfig(rName, appsyncCertDomain, "description1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameExists(resourceName, &domainName), + resource.TestCheckResourceAttr(resourceName, "description", "description1"), + ), + }, + { + Config: testAccAppsyncDomainNameDescriptionConfig(rName, appsyncCertDomain, "description2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameExists(resourceName, &domainName), + resource.TestCheckResourceAttr(resourceName, "description", "description2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAppSyncDomainName_disappears(t *testing.T) { + var providers []*schema.Provider + var domainName appsync.DomainNameConfig + appsyncCertDomain := getAppsyncCertDomain(t) + + rName := sdkacctest.RandString(8) + resourceName := "aws_appsync_domain_name.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(appsync.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, appsync.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckDomainNameDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncDomainNameBasicConfig(rName, appsyncCertDomain), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainNameExists(resourceName, &domainName), + acctest.CheckResourceDisappears(acctest.Provider, tfappsync.ResourceDomainName(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckDomainNameDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).AppSyncConn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appsync_domain_name" { + continue + } + + domainName, err := tfappsync.FindDomainNameByID(conn, rs.Primary.ID) + if err == nil { + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeNotFoundException) { + return nil + } + return err + } + + if domainName != nil && aws.StringValue(domainName.DomainName) == rs.Primary.ID { + return fmt.Errorf("Appsync Domain Name ID %q still exists", rs.Primary.ID) + } + + return nil + + } + return nil +} + +func testAccCheckDomainNameExists(resourceName string, domainName *appsync.DomainNameConfig) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Appsync Domain Name Not found in state: %s", resourceName) + } + conn := acctest.Provider.Meta().(*conns.AWSClient).AppSyncConn + + domain, err := tfappsync.FindDomainNameByID(conn, rs.Primary.ID) + if err != nil { + return err + } + + if domain == nil || domain.DomainName == nil { + return fmt.Errorf("Appsync Domain Name %q not found", rs.Primary.ID) + } + + *domainName = *domain + + return nil + } +} + +func testAccAppsyncDomainNameBaseConfig(domain string) string { + return acctest.ConfigAlternateRegionProvider() + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + provider = "awsalternate" + domain = "*.%[1]s" + most_recent = true +} +`, domain) +} + +func testAccAppsyncDomainNameDescriptionConfig(rName, domain, desc string) string { + return testAccAppsyncDomainNameBaseConfig(domain) + fmt.Sprintf(` +resource "aws_appsync_domain_name" "test" { + domain_name = "%[2]s.%[1]s" + certificate_arn = data.aws_acm_certificate.test.arn + description = %[3]q +} +`, domain, rName, desc) +} + +func testAccAppsyncDomainNameBasicConfig(rName, domain string) string { + return testAccAppsyncDomainNameBaseConfig(domain) + fmt.Sprintf(` +resource "aws_appsync_domain_name" "test" { + domain_name = "%[2]s.%[1]s" + certificate_arn = data.aws_acm_certificate.test.arn +} +`, domain, rName) +} diff --git a/internal/service/appsync/find.go b/internal/service/appsync/find.go index c4a634b118b..11a83bc4a52 100644 --- a/internal/service/appsync/find.go +++ b/internal/service/appsync/find.go @@ -31,3 +31,51 @@ func FindApiCacheByID(conn *appsync.AppSync, id string) (*appsync.ApiCache, erro return out.ApiCache, nil } + +func FindDomainNameByID(conn *appsync.AppSync, id string) (*appsync.DomainNameConfig, error) { + input := &appsync.GetDomainNameInput{ + DomainName: aws.String(id), + } + out, err := conn.GetDomainName(input) + + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if out == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return out.DomainNameConfig, nil +} + +func FindDomainNameApiAssociationByID(conn *appsync.AppSync, id string) (*appsync.ApiAssociation, error) { + input := &appsync.GetApiAssociationInput{ + DomainName: aws.String(id), + } + out, err := conn.GetApiAssociation(input) + + if tfawserr.ErrCodeEquals(err, appsync.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if out == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return out.ApiAssociation, nil +} diff --git a/internal/service/appsync/status.go b/internal/service/appsync/status.go index f539fcf56db..be0633493c2 100644 --- a/internal/service/appsync/status.go +++ b/internal/service/appsync/status.go @@ -22,3 +22,19 @@ func StatusApiCache(conn *appsync.AppSync, name string) resource.StateRefreshFun return output, aws.StringValue(output.Status), nil } } + +func statusDomainNameApiAssociation(conn *appsync.AppSync, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindDomainNameApiAssociationByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.AssociationStatus), nil + } +} diff --git a/internal/service/appsync/sweep.go b/internal/service/appsync/sweep.go index 0fe60919fba..8a1bb342bd6 100644 --- a/internal/service/appsync/sweep.go +++ b/internal/service/appsync/sweep.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/sweep" @@ -19,6 +20,19 @@ func init() { Name: "aws_appsync_graphql_api", F: sweepGraphQLAPIs, }) + + resource.AddTestSweepers("aws_appsync_domain_name", &resource.Sweeper{ + Name: "aws_appsync_domain_name", + F: sweepDomainNames, + Dependencies: []string{ + "aws_appsync_domain_name_api_association", + }, + }) + + resource.AddTestSweepers("aws_appsync_domain_name_api_association", &resource.Sweeper{ + Name: "aws_appsync_domain_name_api_association", + F: sweepDomainNameAssociations, + }) } func sweepGraphQLAPIs(region string) error { @@ -27,6 +41,8 @@ func sweepGraphQLAPIs(region string) error { return fmt.Errorf("Error getting client: %s", err) } conn := client.(*conns.AWSClient).AppSyncConn + sweepResources := make([]*sweep.SweepResource, 0) + var errs *multierror.Error input := &appsync.ListGraphqlApisInput{} @@ -38,21 +54,76 @@ func sweepGraphQLAPIs(region string) error { } if err != nil { - return fmt.Errorf("Error retrieving AppSync GraphQL APIs: %s", err) + err := fmt.Errorf("error reading AppSync GraphQL API: %w", err) + log.Printf("[ERROR] %s", err) + errs = multierror.Append(errs, err) + break } for _, graphAPI := range output.GraphqlApis { + + r := ResourceGraphQLAPI() + d := r.Data(nil) + id := aws.StringValue(graphAPI.ApiId) - input := &appsync.DeleteGraphqlApiInput{ - ApiId: graphAPI.ApiId, - } + d.SetId(id) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + if err := sweep.SweepOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppSync GraphQL API %s: %w", region, err)) + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping AppSync GraphQL API sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func sweepDomainNames(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + conn := client.(*conns.AWSClient).AppSyncConn + sweepResources := make([]*sweep.SweepResource, 0) + var errs *multierror.Error + + input := &appsync.ListDomainNamesInput{} + + for { + output, err := conn.ListDomainNames(input) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping AppSync Domain Name sweep for %s: %s", region, err) + return nil + } + + if err != nil { + err := fmt.Errorf("error reading AppSync Domain Name: %w", err) + log.Printf("[ERROR] %s", err) + errs = multierror.Append(errs, err) + break + } + + for _, dm := range output.DomainNameConfigs { - log.Printf("[INFO] Deleting AppSync GraphQL API %s", id) - _, err := conn.DeleteGraphqlApi(input) + r := ResourceDomainName() + d := r.Data(nil) - if err != nil { - return fmt.Errorf("error deleting AppSync GraphQL API (%s): %s", id, err) - } + id := aws.StringValue(dm.DomainName) + d.SetId(id) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } if aws.StringValue(output.NextToken) == "" { @@ -62,5 +133,69 @@ func sweepGraphQLAPIs(region string) error { input.NextToken = output.NextToken } - return nil + if err := sweep.SweepOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppSync Domain Name %s: %w", region, err)) + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping AppSync Domain Name sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func sweepDomainNameAssociations(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + conn := client.(*conns.AWSClient).AppSyncConn + sweepResources := make([]*sweep.SweepResource, 0) + var errs *multierror.Error + + input := &appsync.ListDomainNamesInput{} + + for { + output, err := conn.ListDomainNames(input) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping AppSync Domain Name Association sweep for %s: %s", region, err) + return nil + } + + if err != nil { + err := fmt.Errorf("error reading AppSync Domain Name Association: %w", err) + log.Printf("[ERROR] %s", err) + errs = multierror.Append(errs, err) + break + } + + for _, dm := range output.DomainNameConfigs { + + r := ResourceDomainNameApiAssociation() + d := r.Data(nil) + + id := aws.StringValue(dm.DomainName) + d.SetId(id) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + if err := sweep.SweepOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppSync Domain Name Association %s: %w", region, err)) + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping AppSync Domain Name Association sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() } diff --git a/internal/service/appsync/wait.go b/internal/service/appsync/wait.go index ebc30da495f..4046655042c 100644 --- a/internal/service/appsync/wait.go +++ b/internal/service/appsync/wait.go @@ -8,8 +8,10 @@ import ( ) const ( - ApiCacheAvailableTimeout = 60 * time.Minute - ApiCacheDeletedTimeout = 60 * time.Minute + apiCacheAvailableTimeout = 60 * time.Minute + apiCacheDeletedTimeout = 60 * time.Minute + domainNameApiAssociationTimeout = 60 * time.Minute + domainNameApiDisassociationTimeout = 60 * time.Minute ) func waitApiCacheAvailable(conn *appsync.AppSync, id string) error { @@ -17,7 +19,7 @@ func waitApiCacheAvailable(conn *appsync.AppSync, id string) error { Pending: []string{appsync.ApiCacheStatusCreating, appsync.ApiCacheStatusModifying}, Target: []string{appsync.ApiCacheStatusAvailable}, Refresh: StatusApiCache(conn, id), - Timeout: ApiCacheAvailableTimeout, + Timeout: apiCacheAvailableTimeout, } _, err := stateConf.WaitForState() @@ -30,7 +32,33 @@ func waitApiCacheDeleted(conn *appsync.AppSync, id string) error { Pending: []string{appsync.ApiCacheStatusDeleting}, Target: []string{}, Refresh: StatusApiCache(conn, id), - Timeout: ApiCacheDeletedTimeout, + Timeout: apiCacheDeletedTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func waitDomainNameApiAssociation(conn *appsync.AppSync, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{appsync.AssociationStatusProcessing}, + Target: []string{appsync.AssociationStatusSuccess}, + Refresh: statusDomainNameApiAssociation(conn, id), + Timeout: domainNameApiAssociationTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func waitDomainNameApiDisassociation(conn *appsync.AppSync, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{appsync.AssociationStatusProcessing}, + Target: []string{}, + Refresh: statusDomainNameApiAssociation(conn, id), + Timeout: domainNameApiDisassociationTimeout, } _, err := stateConf.WaitForState() diff --git a/website/docs/r/appsync_domain_name.html.markdown b/website/docs/r/appsync_domain_name.html.markdown new file mode 100644 index 00000000000..ea07a00c934 --- /dev/null +++ b/website/docs/r/appsync_domain_name.html.markdown @@ -0,0 +1,44 @@ +--- +subcategory: "AppSync" +layout: "aws" +page_title: "AWS: aws_appsync_domain_name" +description: |- + Provides an AppSync Domain Name. +--- + +# Resource: aws_appsync_domain_name + +Provides an AppSync Domain Name. + +## Example Usage + +```terraform +resource "aws_appsync_domain_name" "example" { + domain_name = "api.example.com" + certificate_arn = aws_acm_certificate.example.arn +} +``` + +## Argument Reference + +The following arguments are supported: + +* `certificate_arn` - (Required) The Amazon Resource Name (ARN) of the certificate. This can be an Certificate Manager (ACM) certificate or an Identity and Access Management (IAM) server certificate. The certifiacte must reside in us-east-1. +* `description` - (Optional) A description of the Domain Name. +* `domain_name` - (Required) The domain name. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The Appsync Domain Name. +* `appsync_domain_name` - The domain name that AppSync provides. +* `hosted_zone_id` - The ID of your Amazon Route 53 hosted zone. + +## Import + +`aws_appsync_domain_name` can be imported using the AppSync domain name, e.g., + +``` +$ terraform import aws_appsync_domain_name.example example.com +``` diff --git a/website/docs/r/appsync_domain_name_api_association.html.markdown b/website/docs/r/appsync_domain_name_api_association.html.markdown new file mode 100644 index 00000000000..9c93a902159 --- /dev/null +++ b/website/docs/r/appsync_domain_name_api_association.html.markdown @@ -0,0 +1,42 @@ +--- +subcategory: "AppSync" +layout: "aws" +page_title: "AWS: aws_appsync_domain_name_api_association" +description: |- + Provides an AppSync API Association. +--- + +# Resource: aws_appsync_domain_name_api_association + +Provides an AppSync API Association. + +## Example Usage + +```terraform +resource "aws_appsync_domain_name_api_association" "example" { + api_id = aws_appsync_graphql_api.example.id + domain_name = aws_appsync_domain_name.example.domain_name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `api_id` - (Required) The API ID. +* `domain_name` - (Required) The Appsync domain name. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The Appsync domain name. + + +## Import + +`aws_appsync_domain_name_api_association` can be imported using the AppSync domain name, e.g., + +``` +$ terraform import aws_appsync_domain_name_api_association.example example.com +```