diff --git a/.changelog/46931.txt b/.changelog/46931.txt new file mode 100644 index 000000000000..2e79cec8d9a6 --- /dev/null +++ b/.changelog/46931.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_workmail_domain +``` + +```release-note:new-resource +aws_workmail_default_domain +``` + +```release-note:new-list-resource +aws_workmail_domain +``` \ No newline at end of file diff --git a/internal/service/workmail/default_domain.go b/internal/service/workmail/default_domain.go new file mode 100644 index 000000000000..d42b72eca17a --- /dev/null +++ b/internal/service/workmail/default_domain.go @@ -0,0 +1,183 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail + +import ( + "context" + "fmt" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/workmail" + awstypes "github.com/aws/aws-sdk-go-v2/service/workmail/types" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_workmail_default_domain", name="Default Domain") +func newDefaultDomainResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &defaultDomainResource{} + + return r, nil +} + +const ( + ResNameDefaultDomain = "Default Domain" +) + +type defaultDomainResource struct { + framework.ResourceWithModel[defaultDomainResourceModel] + framework.WithNoOpDelete +} + +func (r *defaultDomainResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrDomainName: schema.StringAttribute{ + Required: true, + }, + "organization_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *defaultDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var plan defaultDomainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, r.putDefaultMailDomain(ctx, conn, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *defaultDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var state defaultDomainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + domainName, err := findDefaultDomainByOrgID(ctx, conn, state.OrganizationId.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.DomainName.String()) + return + } + + state.DomainName = flex.StringValueToFramework(ctx, domainName) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *defaultDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var plan defaultDomainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, r.putDefaultMailDomain(ctx, conn, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *defaultDomainResource) putDefaultMailDomain(ctx context.Context, conn *workmail.Client, plan *defaultDomainResourceModel) diag.Diagnostics { + var diags diag.Diagnostics + + var input workmail.UpdateDefaultMailDomainInput + smerr.AddEnrich(ctx, &diags, flex.Expand(ctx, plan, &input)) + if diags.HasError() { + return diags + } + + _, err := conn.UpdateDefaultMailDomain(ctx, &input) + if err != nil { + smerr.AddError(ctx, &diags, err, smerr.ID, plan.DomainName.String()) + } + + return diags +} + +func (r *defaultDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts, err := intflex.ExpandResourceId(req.ID, domainIDParts, false) + if err != nil { + resp.Diagnostics.Append(fwdiag.NewParsingResourceIDErrorDiagnostic(err)) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrDomainName), parts[1])...) +} + +func findDefaultDomainByOrgID(ctx context.Context, conn *workmail.Client, orgID string) (string, error) { + input := workmail.ListMailDomainsInput{ + OrganizationId: aws.String(orgID), + } + + pages := workmail.NewListMailDomainsPaginator(conn, &input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return "", smarterr.NewError(&retry.NotFoundError{ + LastError: err, + }) + } + return "", smarterr.NewError(err) + } + + for _, d := range page.MailDomains { + if d.DefaultDomain { + return aws.ToString(d.DomainName), nil + } + } + } + + return "", smarterr.NewError(&retry.NotFoundError{ + Message: fmt.Sprintf("no default domain found for WorkMail organization %s", orgID), + }) +} + +type defaultDomainResourceModel struct { + framework.WithRegionModel + OrganizationId types.String `tfsdk:"organization_id"` + DomainName types.String `tfsdk:"domain_name"` +} diff --git a/internal/service/workmail/default_domain_test.go b/internal/service/workmail/default_domain_test.go new file mode 100644 index 000000000000..461da1540c18 --- /dev/null +++ b/internal/service/workmail/default_domain_test.go @@ -0,0 +1,101 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfworkmail "github.com/hashicorp/terraform-provider-aws/internal/service/workmail" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWorkMailDefaultDomain_basic(t *testing.T) { + ctx := acctest.Context(t) + + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_workmail_default_domain.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccDefaultDomainConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDefaultDomainExists(ctx, t, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrDomainName, "aws_workmail_organization.test", "default_mail_domain"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccDefaultDomainImportStateIdFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organization_id", + }, + }, + }) +} + +func testAccDefaultDomainImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s,%s", rs.Primary.Attributes["organization_id"], rs.Primary.Attributes[names.AttrDomainName]), nil + } +} + +func testAccCheckDefaultDomainExists(ctx context.Context, t *testing.T, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDefaultDomain, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDefaultDomain, name, errors.New("not set")) + } + + orgID := rs.Primary.Attributes["organization_id"] + + conn := acctest.ProviderMeta(ctx, t).WorkMailClient(ctx) + + _, err := tfworkmail.FindDefaultDomainByOrgID(ctx, conn, orgID) + if err != nil { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDefaultDomain, rs.Primary.ID, err) + } + + return nil + } +} + +func testAccDefaultDomainConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_workmail_organization" "test" { + organization_alias = %[1]q + delete_directory = true +} + +resource "aws_workmail_default_domain" "test" { + organization_id = aws_workmail_organization.test.organization_id + domain_name = aws_workmail_organization.test.default_mail_domain +} +`, rName) +} diff --git a/internal/service/workmail/domain.go b/internal/service/workmail/domain.go new file mode 100644 index 000000000000..86e1b443e0f2 --- /dev/null +++ b/internal/service/workmail/domain.go @@ -0,0 +1,255 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail + +import ( + "context" + "fmt" + "strings" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/workmail" + awstypes "github.com/aws/aws-sdk-go-v2/service/workmail/types" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + inttypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_workmail_domain", name="Domain") +// @IdentityAttribute("organization_id") +// @IdentityAttribute("domain_name") +// @ImportIDHandler("domainImportID") +// @Testing(hasNoPreExistingResource=true) +// @Testing(importStateIdAttributes="organization_id;domain_name", importStateIdAttributesSep="flex.ResourceIdSeparator") +func newDomainResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &domainResource{} + + return r, nil +} + +const ( + ResNameDomain = "Domain" + domainIDParts = 2 +) + +type domainResource struct { + framework.ResourceWithModel[domainResourceModel] + framework.WithNoUpdate + framework.WithImportByIdentity +} + +func (r *domainResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrDomainName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "dkim_verification_status": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.DnsRecordVerificationStatus](), + Computed: true, + }, + "is_default": schema.BoolAttribute{ + Computed: true, + }, + "is_test_domain": schema.BoolAttribute{ + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "ownership_verification_status": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.DnsRecordVerificationStatus](), + Computed: true, + }, + "records": framework.ResourceComputedListOfObjectsAttribute[dnsRecordModel](ctx), + }, + } +} + +func (r *domainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var plan domainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + var input workmail.RegisterMailDomainInput + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input)) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.RegisterMailDomain(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.DomainName.String()) + return + } + + // Read back to populate computed fields + out, err := findDomainByOrgAndName(ctx, conn, plan.OrganizationId.ValueString(), plan.DomainName.ValueString()) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.DomainName.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, r.flatten(ctx, out, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *domainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var state domainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + out, err := findDomainByOrgAndName(ctx, conn, state.OrganizationId.ValueString(), state.DomainName.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.DomainName.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, r.flatten(ctx, out, &state)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *domainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().WorkMailClient(ctx) + + var state domainResourceModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + var input workmail.DeregisterMailDomainInput + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, &state, &input)) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.DeregisterMailDomain(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.MailDomainNotFoundException](err) { + return + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.DomainName.String()) + return + } +} + +func (r *domainResource) flatten(ctx context.Context, domain *workmail.GetMailDomainOutput, data *domainResourceModel) (diags diag.Diagnostics) { + diags.Append(flex.Flatten(ctx, domain, data)...) + + return diags +} + +func findDomainByOrgAndName(ctx context.Context, conn *workmail.Client, orgID, domainName string) (*workmail.GetMailDomainOutput, error) { + input := workmail.GetMailDomainInput{ + OrganizationId: aws.String(orgID), + DomainName: aws.String(domainName), + } + + out, err := conn.GetMailDomain(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.MailDomainNotFoundException](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + }) + } else if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + }) + } else if errs.IsA[*awstypes.OrganizationStateException](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + }) + } + + return nil, smarterr.NewError(err) + } + + if out == nil { + return nil, smarterr.NewError(&retry.NotFoundError{ + Message: fmt.Sprintf("WorkMail Domain %s in organization %s not found", domainName, orgID), + }) + } + + return out, nil +} + +var ( + _ inttypes.ImportIDParser = domainImportID{} +) + +type domainImportID struct{} + +func (domainImportID) Parse(id string) (string, map[string]any, error) { + orgID, domainName, found := strings.Cut(id, intflex.ResourceIdSeparator) + if !found { + return "", nil, fmt.Errorf("id %q should be in the format %s", id, intflex.ResourceIdSeparator) + } + + result := map[string]any{ + "organization_id": orgID, + names.AttrDomainName: domainName, + } + + return id, result, nil +} + +type domainResourceModel struct { + framework.WithRegionModel + DomainName types.String `tfsdk:"domain_name"` + DkimVerificationStatus fwtypes.StringEnum[awstypes.DnsRecordVerificationStatus] `tfsdk:"dkim_verification_status"` + IsDefault types.Bool `tfsdk:"is_default"` + IsTestDomain types.Bool `tfsdk:"is_test_domain"` + OrganizationId types.String `tfsdk:"organization_id"` + OwnershipVerificationStatus fwtypes.StringEnum[awstypes.DnsRecordVerificationStatus] `tfsdk:"ownership_verification_status"` + Records fwtypes.ListNestedObjectValueOf[dnsRecordModel] `tfsdk:"records"` +} + +type dnsRecordModel struct { + Hostname types.String `tfsdk:"hostname"` + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` +} diff --git a/internal/service/workmail/domain_identity_gen_test.go b/internal/service/workmail/domain_identity_gen_test.go new file mode 100644 index 000000000000..906d3985868f --- /dev/null +++ b/internal/service/workmail/domain_identity_gen_test.go @@ -0,0 +1,205 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package workmail_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWorkMailDomain_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_workmail_domain.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + CheckDestroy: testAccCheckDomainDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDomainExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.Region()), + "organization_id": knownvalue.NotNull(), + names.AttrDomainName: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("organization_id")), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrDomainName)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, "organization_id", names.AttrDomainName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organization_id", + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, "organization_id", names.AttrDomainName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrDomainName), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrDomainName), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccWorkMailDomain_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_workmail_domain.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.AlternateRegion()), + "organization_id": knownvalue.NotNull(), + names.AttrDomainName: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("organization_id")), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrDomainName)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionAttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, "organization_id", names.AttrDomainName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organization_id", + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionAttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, "organization_id", names.AttrDomainName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrDomainName), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrDomainName), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} diff --git a/internal/service/workmail/domain_list.go b/internal/service/workmail/domain_list.go new file mode 100644 index 000000000000..2fb81cf45afe --- /dev/null +++ b/internal/service/workmail/domain_list.go @@ -0,0 +1,135 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail + +import ( + "context" + "fmt" + "iter" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/workmail" + awstypes "github.com/aws/aws-sdk-go-v2/service/workmail/types" + "github.com/hashicorp/terraform-plugin-framework/list" + listschema "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/logging" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// Function annotations are used for list resource registration to the Provider. DO NOT EDIT. +// @FrameworkListResource("aws_workmail_domain") +func newDomainResourceAsListResource() list.ListResourceWithConfigure { + return &domainListResource{} +} + +var _ list.ListResource = &domainListResource{} + +type domainListResource struct { + domainResource + framework.WithList +} + +func (l *domainListResource) ListResourceConfigSchema(ctx context.Context, request list.ListResourceSchemaRequest, response *list.ListResourceSchemaResponse) { + response.Schema = listschema.Schema{ + Attributes: map[string]listschema.Attribute{ + "organization_id": listschema.StringAttribute{ + Required: true, + Description: "ID of the WorkMail organization to list domains from.", + }, + }, + } +} + +func (l *domainListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { + conn := l.Meta().WorkMailClient(ctx) + + var query listDomainModel + if request.Config.Raw.IsKnown() && !request.Config.Raw.IsNull() { + if diags := request.Config.Get(ctx, &query); diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + organizationID := query.OrganizationID.ValueString() + + tflog.Info(ctx, "Listing WorkMail Domains", map[string]any{ + "organization_id": organizationID, + }) + + stream.Results = func(yield func(list.ListResult) bool) { + input := workmail.ListMailDomainsInput{ + OrganizationId: aws.String(organizationID), + } + + for item, err := range listDomains(ctx, conn, &input) { + if err != nil { + result := fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + domainName := aws.ToString(item.DomainName) + ctx := tflog.SetField(ctx, logging.ResourceAttributeKey(names.AttrDomainName), domainName) + + out, err := findDomainByOrgAndName(ctx, conn, organizationID, domainName) + if err != nil { + result := fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + result := request.NewListResult(ctx) + + var data domainResourceModel + l.SetResult(ctx, l.Meta(), request.IncludeResource, &data, &result, func() { + result.Diagnostics.Append(l.flatten(ctx, out, &data)...) + if result.Diagnostics.HasError() { + return + } + data.OrganizationId = flex.StringValueToFramework(ctx, organizationID) + data.DomainName = flex.StringValueToFramework(ctx, domainName) + result.DisplayName = domainName + }) + + if result.Diagnostics.HasError() { + yield(list.ListResult{Diagnostics: result.Diagnostics}) + return + } + + if !yield(result) { + return + } + } + } +} + +type listDomainModel struct { + framework.WithRegionModel + OrganizationID types.String `tfsdk:"organization_id"` +} + +func listDomains(ctx context.Context, conn *workmail.Client, input *workmail.ListMailDomainsInput) iter.Seq2[awstypes.MailDomainSummary, error] { + return func(yield func(awstypes.MailDomainSummary, error) bool) { + pages := workmail.NewListMailDomainsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + yield(awstypes.MailDomainSummary{}, fmt.Errorf("listing WorkMail Domain resources: %w", err)) + return + } + + for _, item := range page.MailDomains { + if !yield(item, nil) { + return + } + } + } + } +} diff --git a/internal/service/workmail/domain_list_test.go b/internal/service/workmail/domain_list_test.go new file mode 100644 index 000000000000..7a306fdfc806 --- /dev/null +++ b/internal/service/workmail/domain_list_test.go @@ -0,0 +1,215 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + tfquerycheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/querycheck" + tfqueryfilter "github.com/hashicorp/terraform-provider-aws/internal/acctest/queryfilter" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWorkMailDomain_List_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName1 := "aws_workmail_domain.test[0]" + resourceName2 := "aws_workmail_domain.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + domainName1 := rName + "-0.example.com" + domainName2 := rName + "-1.example.com" + + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + CheckDestroy: testAccCheckDomainDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName1)), + + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName2)), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("aws_workmail_domain.test", map[string]knownvalue.Check{ + "organization_id": knownvalue.NotNull(), + names.AttrDomainName: knownvalue.StringExact(domainName1), + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.Region()), + }), + querycheck.ExpectResourceDisplayName("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), knownvalue.StringExact(domainName1)), + tfquerycheck.ExpectNoResourceObject("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks())), + + querycheck.ExpectIdentity("aws_workmail_domain.test", map[string]knownvalue.Check{ + "organization_id": knownvalue.NotNull(), + names.AttrDomainName: knownvalue.StringExact(domainName2), + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.Region()), + }), + querycheck.ExpectResourceDisplayName("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity2.Checks()), knownvalue.StringExact(domainName2)), + tfquerycheck.ExpectNoResourceObject("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity2.Checks())), + }, + }, + }, + }) +} + +func TestAccWorkMailDomain_List_includeResource(t *testing.T) { + ctx := acctest.Context(t) + + resourceName1 := "aws_workmail_domain.test[0]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + domainName1 := rName + "-0.example.com" + + identity1 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + CheckDestroy: testAccCheckDomainDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName1)), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_workmail_domain.test", identity1.Checks()), + querycheck.ExpectResourceDisplayName("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), knownvalue.StringExact(domainName1)), + querycheck.ExpectResourceKnownValues("aws_workmail_domain.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), []querycheck.KnownValueCheck{ + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName1)), + tfquerycheck.KnownValueCheck(tfjsonpath.New("organization_id"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New("dkim_verification_status"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New("ownership_verification_status"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New("is_default"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New("is_test_domain"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }), + }, + }, + }, + }) +} + +func TestAccWorkMailDomain_List_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName1 := "aws_workmail_domain.test[0]" + resourceName2 := "aws_workmail_domain.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + domainName1 := rName + "-0.example.com" + domainName2 := rName + "-1.example.com" + + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckMultipleRegion(t, 2) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName1)), + + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrDomainName), knownvalue.StringExact(domainName2)), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Domain/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_workmail_domain.test", identity1.Checks()), + + tfquerycheck.ExpectIdentityFunc("aws_workmail_domain.test", identity2.Checks()), + }, + }, + }, + }) +} diff --git a/internal/service/workmail/domain_test.go b/internal/service/workmail/domain_test.go new file mode 100644 index 000000000000..6e9920137f3a --- /dev/null +++ b/internal/service/workmail/domain_test.go @@ -0,0 +1,171 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package workmail_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfworkmail "github.com/hashicorp/terraform-provider-aws/internal/service/workmail" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWorkMailDomain_basic(t *testing.T) { + ctx := acctest.Context(t) + + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_workmail_domain.test" + domainName := fmt.Sprintf("%s.example.com", rName) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDomainDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccDomainConfig_basic(rName, domainName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDomainExists(ctx, t, resourceName), + resource.TestCheckResourceAttr(resourceName, names.AttrDomainName, domainName), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "dkim_verification_status"), + resource.TestCheckResourceAttrSet(resourceName, "ownership_verification_status"), + resource.TestCheckResourceAttrSet(resourceName, "is_default"), + resource.TestCheckResourceAttrSet(resourceName, "is_test_domain"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccDomainImportStateIdFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organization_id", + }, + }, + }) +} + +func TestAccWorkMailDomain_disappears(t *testing.T) { + ctx := acctest.Context(t) + + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_workmail_domain.test" + domainName := fmt.Sprintf("%s.example.com", rName) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.WorkMail) + }, + ErrorCheck: acctest.ErrorCheck(t, names.WorkMailServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDomainDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccDomainConfig_basic(rName, domainName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDomainExists(ctx, t, resourceName), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfworkmail.ResourceDomain, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccDomainImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s,%s", rs.Primary.Attributes["organization_id"], rs.Primary.Attributes[names.AttrDomainName]), nil + } +} + +func testAccCheckDomainDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).WorkMailClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_workmail_domain" { + continue + } + + orgID := rs.Primary.Attributes["organization_id"] + domainName := rs.Primary.Attributes[names.AttrDomainName] + + _, err := tfworkmail.FindDomainByOrgAndName(ctx, conn, orgID, domainName) + + if retry.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("WorkMail Domain %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckDomainExists(ctx context.Context, t *testing.T, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDomain, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDomain, name, errors.New("not set")) + } + + orgID := rs.Primary.Attributes["organization_id"] + domainName := rs.Primary.Attributes[names.AttrDomainName] + + conn := acctest.ProviderMeta(ctx, t).WorkMailClient(ctx) + + _, err := tfworkmail.FindDomainByOrgAndName(ctx, conn, orgID, domainName) + if err != nil { + return create.Error(names.WorkMail, create.ErrActionCheckingExistence, tfworkmail.ResNameDomain, rs.Primary.ID, err) + } + + return nil + } +} + +func testAccDomainConfig_basic(rName, domainName string) string { + return fmt.Sprintf(` +resource "aws_workmail_organization" "test" { + organization_alias = %[1]q + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + organization_id = aws_workmail_organization.test.organization_id + domain_name = %[2]q +} +`, rName, domainName) +} diff --git a/internal/service/workmail/exports_test.go b/internal/service/workmail/exports_test.go index 4b80fb215932..99afaca031c7 100644 --- a/internal/service/workmail/exports_test.go +++ b/internal/service/workmail/exports_test.go @@ -5,7 +5,11 @@ package workmail // Exports for use in tests only. var ( - ResourceOrganization = newOrganizationResource + ResourceOrganization = newOrganizationResource + ResourceDomain = newDomainResource + ResourceDefaultDomain = newDefaultDomainResource - FindOrganizationByID = findOrganizationByID + FindOrganizationByID = findOrganizationByID + FindDomainByOrgAndName = findDomainByOrgAndName + FindDefaultDomainByOrgID = findDefaultDomainByOrgID ) diff --git a/internal/service/workmail/service_package_gen.go b/internal/service/workmail/service_package_gen.go index 146fb7840678..373d1ed1bd5b 100644 --- a/internal/service/workmail/service_package_gen.go +++ b/internal/service/workmail/service_package_gen.go @@ -28,6 +28,26 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.S func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.ServicePackageFrameworkResource { return []*inttypes.ServicePackageFrameworkResource{ + { + Factory: newDefaultDomainResource, + TypeName: "aws_workmail_default_domain", + Name: "Default Domain", + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, + { + Factory: newDomainResource, + TypeName: "aws_workmail_domain", + Name: "Domain", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalParameterizedIdentity([]inttypes.IdentityAttribute{ + inttypes.StringIdentityAttribute("organization_id", true), + inttypes.StringIdentityAttribute(names.AttrDomainName, true), + }), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + ImportID: domainImportID{}, + }, + }, { Factory: newOrganizationResource, TypeName: "aws_workmail_organization", @@ -46,6 +66,16 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser func (p *servicePackage) FrameworkListResources(ctx context.Context) iter.Seq[*inttypes.ServicePackageFrameworkListResource] { return slices.Values([]*inttypes.ServicePackageFrameworkListResource{ + { + Factory: newDomainResourceAsListResource, + TypeName: "aws_workmail_domain", + Name: "Domain", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalParameterizedIdentity([]inttypes.IdentityAttribute{ + inttypes.StringIdentityAttribute("organization_id", true), + inttypes.StringIdentityAttribute(names.AttrDomainName, true), + }), + }, { Factory: newOrganizationResourceAsListResource, TypeName: "aws_workmail_organization", diff --git a/internal/service/workmail/testdata/Domain/basic/main_gen.tf b/internal/service/workmail/testdata/Domain/basic/main_gen.tf new file mode 100644 index 000000000000..85c1f3beeb9a --- /dev/null +++ b/internal/service/workmail/testdata/Domain/basic/main_gen.tf @@ -0,0 +1,18 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_workmail_organization" "test" { + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}.example.com" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} diff --git a/internal/service/workmail/testdata/Domain/list_basic/main.tf b/internal/service/workmail/testdata/Domain/list_basic/main.tf new file mode 100644 index 000000000000..71249b5b57d2 --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_basic/main.tf @@ -0,0 +1,26 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_workmail_organization" "test" { + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + count = var.resource_count + + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}-${count.index}.example.com" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/workmail/testdata/Domain/list_basic/query.tfquery.hcl b/internal/service/workmail/testdata/Domain/list_basic/query.tfquery.hcl new file mode 100644 index 000000000000..efb6e8caee4d --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_basic/query.tfquery.hcl @@ -0,0 +1,10 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_workmail_domain" "test" { + provider = aws + + config { + organization_id = aws_workmail_organization.test.organization_id + } +} diff --git a/internal/service/workmail/testdata/Domain/list_include_resource/main.tf b/internal/service/workmail/testdata/Domain/list_include_resource/main.tf new file mode 100644 index 000000000000..71249b5b57d2 --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_include_resource/main.tf @@ -0,0 +1,26 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_workmail_organization" "test" { + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + count = var.resource_count + + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}-${count.index}.example.com" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/workmail/testdata/Domain/list_include_resource/query.tfquery.hcl b/internal/service/workmail/testdata/Domain/list_include_resource/query.tfquery.hcl new file mode 100644 index 000000000000..570c41a4e8a6 --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_include_resource/query.tfquery.hcl @@ -0,0 +1,12 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_workmail_domain" "test" { + provider = aws + + include_resource = true + + config { + organization_id = aws_workmail_organization.test.organization_id + } +} diff --git a/internal/service/workmail/testdata/Domain/list_region_override/main.tf b/internal/service/workmail/testdata/Domain/list_region_override/main.tf new file mode 100644 index 000000000000..cba0b2369d7c --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_region_override/main.tf @@ -0,0 +1,35 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_workmail_organization" "test" { + region = var.region + + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + count = var.resource_count + region = var.region + + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}-${count.index}.example.com" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/workmail/testdata/Domain/list_region_override/query.tfquery.hcl b/internal/service/workmail/testdata/Domain/list_region_override/query.tfquery.hcl new file mode 100644 index 000000000000..570ba3881ca4 --- /dev/null +++ b/internal/service/workmail/testdata/Domain/list_region_override/query.tfquery.hcl @@ -0,0 +1,11 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_workmail_domain" "test" { + provider = aws + + config { + organization_id = aws_workmail_organization.test.organization_id + region = var.region + } +} diff --git a/internal/service/workmail/testdata/Domain/region_override/main_gen.tf b/internal/service/workmail/testdata/Domain/region_override/main_gen.tf new file mode 100644 index 000000000000..c1ddb6a0e1d8 --- /dev/null +++ b/internal/service/workmail/testdata/Domain/region_override/main_gen.tf @@ -0,0 +1,28 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_workmail_organization" "test" { + region = var.region + + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { + region = var.region + + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}.example.com" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/workmail/testdata/tmpl/domain_basic.gtpl b/internal/service/workmail/testdata/tmpl/domain_basic.gtpl new file mode 100644 index 000000000000..e34f0cc068af --- /dev/null +++ b/internal/service/workmail/testdata/tmpl/domain_basic.gtpl @@ -0,0 +1,11 @@ +resource "aws_workmail_organization" "test" { +{{- template "region" }} + organization_alias = var.rName + delete_directory = true +} + +resource "aws_workmail_domain" "test" { +{{- template "region" }} + organization_id = aws_workmail_organization.test.organization_id + domain_name = "${var.rName}.example.com" +} diff --git a/website/docs/list-resources/workmail_domain.html.markdown b/website/docs/list-resources/workmail_domain.html.markdown new file mode 100644 index 000000000000..c9996a5ba06d --- /dev/null +++ b/website/docs/list-resources/workmail_domain.html.markdown @@ -0,0 +1,30 @@ +--- +subcategory: "WorkMail" +layout: "aws" +page_title: "AWS: aws_workmail_domain" +description: |- + Lists WorkMail Domain resources. +--- + +# List Resource: aws_workmail_domain + +Lists WorkMail Domain resources. + +## Example Usage + +```terraform +list "aws_workmail_domain" "example" { + provider = aws + + config { + organization_id = aws_workmail_organization.example.organization_id + } +} +``` + +## Argument Reference + +This list resource supports the following arguments: + +* `organization_id` - (Required) ID of the WorkMail organization to list domains from. +* `region` - (Optional) Region to query. Defaults to provider region. diff --git a/website/docs/r/workmail_default_domain.html.markdown b/website/docs/r/workmail_default_domain.html.markdown new file mode 100644 index 000000000000..2ef9ca2768f7 --- /dev/null +++ b/website/docs/r/workmail_default_domain.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "WorkMail" +layout: "aws" +page_title: "AWS: aws_workmail_default_domain" +description: |- + Manages the default mail domain for an AWS WorkMail organization. +--- + +# Resource: aws_workmail_default_domain + +Manages the default mail domain for an AWS WorkMail organization. + +~> **NOTE:** This does not register a domain for workmail. This resource requires a verified domain name to be used as default domain for workmail organization. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_workmail_organization" "example" { + organization_alias = "example-org" +} + +resource "aws_workmail_default_domain" "example" { + organization_id = aws_workmail_organization.example.id + domain_name = aws_workmail_organization.example.default_mail_domain +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `domain_name` - (Required) Mail domain name to set as the default. +* `organization_id` - (Required) Identifier of the WorkMail organization. Changing this forces a new resource. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import WorkMail Default Domain using the organization ID. For example: + +```terraform +import { + to = aws_workmail_default_domain.example + id = "m-1234567890abcdef0" +} +``` + +Using `terraform import`, import WorkMail Default Domain using the organization ID. For example: + +```console +% terraform import aws_workmail_default_domain.example "m-1234567890abcdef0" +``` diff --git a/website/docs/r/workmail_domain.html.markdown b/website/docs/r/workmail_domain.html.markdown new file mode 100644 index 000000000000..e8b8bb0fc308 --- /dev/null +++ b/website/docs/r/workmail_domain.html.markdown @@ -0,0 +1,65 @@ +--- +subcategory: "WorkMail" +layout: "aws" +page_title: "AWS: aws_workmail_domain" +description: |- + Manages a mail domain registered to an AWS WorkMail organization. +--- + +# Resource: aws_workmail_domain + +Manages a mail domain registered to an AWS WorkMail organization. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_workmail_domain" "example" { + organization_id = aws_workmail_organization.example.id + domain_name = "example.com" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `domain_name` - (Required) Mail domain name to register. Changing this forces a new resource. +* `organization_id` - (Required) Identifier of the WorkMail organization. Changing this forces a new resource. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `dkim_verification_status` - DKIM verification status. Values: `PENDING`, `VERIFIED`, `FAILED`. +* `is_default` - Whether this domain is the default mail domain for the organization. +* `is_test_domain` - Whether this is the auto-provisioned test domain. +* `ownership_verification_status` - Domain ownership verification status. Values: `PENDING`, `VERIFIED`, `FAILED`. +* `records` - List of DNS records required for domain verification. See [`records`](#records) below. + +### `records` + +Each `records` block exports the following: + +* `hostname` - DNS record hostname. +* `type` - DNS record type (e.g. `CNAME`, `MX`, `TXT`). +* `value` - DNS record value. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import WorkMail Domain using `organization_id,domain_name`. For example: + +```terraform +import { + to = aws_workmail_domain.example + id = "m-1234567890abcdef0,example.com" +} +``` + +Using `terraform import`, import WorkMail Domain using `organization_id,domain_name`. For example: + +```console +% terraform import aws_workmail_domain.example "m-1234567890abcdef0,example.com" +```