diff --git a/internal/services/communication/communication_service_email_domain_association_resource.go b/internal/services/communication/communication_service_email_domain_association_resource.go new file mode 100644 index 000000000000..ae63808872b0 --- /dev/null +++ b/internal/services/communication/communication_service_email_domain_association_resource.go @@ -0,0 +1,340 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package communication + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/communication/2023-03-31/communicationservices" + "github.com/hashicorp/go-azure-sdk/resource-manager/communication/2023-03-31/domains" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +var _ sdk.Resource = EmailDomainAssociationResource{} + +type EmailDomainAssociationResource struct{} + +type EmailDomainAssociationResourceModel struct { + CommunicationServiceId string `tfschema:"communication_service_id"` + EMailServiceDomainId string `tfschema:"email_service_domain_id"` +} + +func (EmailDomainAssociationResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "communication_service_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: communicationservices.ValidateCommunicationServiceID, + }, + "email_service_domain_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: domains.ValidateDomainID, + }, + } +} + +func (EmailDomainAssociationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (EmailDomainAssociationResource) ModelObject() interface{} { + return &EmailDomainAssociationResourceModel{} +} + +func (EmailDomainAssociationResource) ResourceType() string { + return "azurerm_communication_service_email_domain_association" +} + +func (r EmailDomainAssociationResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Communication.ServiceClient + domainClient := metadata.Client.Communication.DomainClient + + var model EmailDomainAssociationResourceModel + + if err := metadata.Decode(&model); err != nil { + return err + } + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(model.CommunicationServiceId) + if err != nil { + return err + } + + eMailServiceDomainId, err := domains.ParseDomainID(model.EMailServiceDomainId) + if err != nil { + return err + } + + locks.ByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + defer locks.UnlockByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + + locks.ByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + defer locks.UnlockByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + + existingEMailServiceDomain, err := domainClient.Get(ctx, *eMailServiceDomainId) + if err != nil && !response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("checking for the presence of existing %s: %+v", *eMailServiceDomainId, err) + } + + if response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("%s was not found", eMailServiceDomainId) + } + + existingCommunicationService, err := client.Get(ctx, *communicationServiceId) + if err != nil && !response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("checking for the presence of existing %s: %+v", communicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("%s was not found", communicationServiceId) + } + + if existingCommunicationService.Model == nil { + return fmt.Errorf("model for %s was nil", communicationServiceId) + } + + if existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("properties for %s was nil", communicationServiceId) + } + + domainList := make([]string, 0) + if existingDomainList := existingCommunicationService.Model.Properties.LinkedDomains; existingDomainList != nil { + domainList = pointer.From(existingDomainList) + } + + id := commonids.NewCompositeResourceID(communicationServiceId, eMailServiceDomainId) + + for _, v := range domainList { + tmpID, tmpErr := domains.ParseDomainIDInsensitively(v) + if tmpErr != nil { + return fmt.Errorf("parsing domain ID %q from LinkedDomains for %s: %+v", v, communicationServiceId, err) + } + + if strings.EqualFold(eMailServiceDomainId.ID(), tmpID.ID()) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + } + + domainList = append(domainList, eMailServiceDomainId.ID()) + + input := communicationservices.CommunicationServiceResourceUpdate{ + Properties: &communicationservices.CommunicationServiceUpdateProperties{ + LinkedDomains: pointer.To(domainList), + }, + } + + if _, err = client.Update(ctx, *communicationServiceId, input); err != nil { + return fmt.Errorf("updating %s: %+v", *communicationServiceId, err) + } + + metadata.SetID(id) + + return nil + }, + } +} + +func (EmailDomainAssociationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Communication.ServiceClient + domainClient := metadata.Client.Communication.DomainClient + + id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &communicationservices.CommunicationServiceId{}, &domains.DomainId{}) + if err != nil { + return err + } + + state := EmailDomainAssociationResourceModel{} + state.CommunicationServiceId = id.First.ID() + state.EMailServiceDomainId = id.Second.ID() + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(state.CommunicationServiceId) + if err != nil { + return err + } + + eMailServiceDomainId, err := domains.ParseDomainID(state.EMailServiceDomainId) + if err != nil { + return err + } + + locks.ByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + defer locks.UnlockByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + + locks.ByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + defer locks.UnlockByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + + existingEMailServiceDomain, err := domainClient.Get(ctx, *eMailServiceDomainId) + if err != nil && !response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("checking for the presence of existing EMail Service Domain %q: %+v", state.EMailServiceDomainId, err) + } + + if response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return metadata.MarkAsGone(id) + } + + existingCommunicationService, err := client.Get(ctx, *communicationServiceId) + if err != nil && !response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("checking for the presence of existing Communication Service %q: %+v", state.CommunicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return metadata.MarkAsGone(id) + } + + if existingCommunicationService.Model == nil { + return fmt.Errorf("model for %s was nil", state.CommunicationServiceId) + } + + if existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("properties for %s was nil", state.CommunicationServiceId) + } + + domainList := existingCommunicationService.Model.Properties.LinkedDomains + if domainList == nil { + return fmt.Errorf("checking for Domain Association %s for %s", *eMailServiceDomainId, *communicationServiceId) + } + + var found bool + + for _, v := range pointer.From(domainList) { + tmpID, tmpErr := domains.ParseDomainIDInsensitively(v) + if tmpErr != nil { + return fmt.Errorf("parsing domain ID %q from LinkedDomains for %s: %+v", v, communicationServiceId, err) + } + + if strings.EqualFold(eMailServiceDomainId.ID(), tmpID.ID()) { + found = true + + break + } + } + + if !found { + return metadata.MarkAsGone(id) + } + + return metadata.Encode(&state) + }, + } +} + +func (EmailDomainAssociationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Communication.ServiceClient + domainClient := metadata.Client.Communication.DomainClient + + var model EmailDomainAssociationResourceModel + + if err := metadata.Decode(&model); err != nil { + return err + } + + id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &communicationservices.CommunicationServiceId{}, &domains.DomainId{}) + if err != nil { + return err + } + + communicationServiceId := id.First + eMailServiceDomainId := id.Second + + locks.ByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + defer locks.UnlockByName(communicationServiceId.CommunicationServiceName, "azurerm_communication_service") + + locks.ByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + defer locks.UnlockByName(eMailServiceDomainId.DomainName, "azurerm_email_communication_service_domain") + + existingEMailServiceDomain, err := domainClient.Get(ctx, *eMailServiceDomainId) + if err != nil && !response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("checking for the presence of existing %s: %+v", *eMailServiceDomainId, err) + } + + if response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return metadata.MarkAsGone(id) + } + + existingCommunicationService, err := client.Get(ctx, *communicationServiceId) + if err != nil && !response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("checking for the presence of existing %s: %+v", *communicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return metadata.MarkAsGone(id) + } + + if existingCommunicationService.Model == nil { + return fmt.Errorf("model for %s was nil", model.CommunicationServiceId) + } + + if existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("properties for %s was nil", model.CommunicationServiceId) + } + + domainList := existingCommunicationService.Model.Properties.LinkedDomains + if domainList == nil { + return metadata.MarkAsGone(id) + } + + if !slices.Contains(*domainList, eMailServiceDomainId.ID()) { + return metadata.MarkAsGone(id) + } + + *domainList = slices.DeleteFunc(*domainList, func(domainID string) bool { + parsedDomainID, err := domains.ParseDomainIDInsensitively(domainID) + if err != nil { + return false + } + + return strings.EqualFold(parsedDomainID.ID(), eMailServiceDomainId.ID()) + }) + + input := communicationservices.CommunicationServiceResourceUpdate{ + Properties: &communicationservices.CommunicationServiceUpdateProperties{ + LinkedDomains: domainList, + }, + } + + if _, err := client.Update(ctx, *communicationServiceId, input); err != nil { + return fmt.Errorf("deleting Email Domain Association for %s from %s: %+v", *eMailServiceDomainId, *communicationServiceId, err) + } + + return nil + }, + } +} + +func (EmailDomainAssociationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return func(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := commonids.ParseCompositeResourceID(v, &communicationservices.CommunicationServiceId{}, &domains.DomainId{}); err != nil { + errors = append(errors, err) + } + + return + } +} diff --git a/internal/services/communication/communication_service_email_domain_association_resource_test.go b/internal/services/communication/communication_service_email_domain_association_resource_test.go new file mode 100644 index 000000000000..53bdb135f621 --- /dev/null +++ b/internal/services/communication/communication_service_email_domain_association_resource_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package communication_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/communication/2023-03-31/communicationservices" + "github.com/hashicorp/go-azure-sdk/resource-manager/communication/2023-03-31/domains" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type CommunicationServiceEmailDomainAssociationResource struct{} + +func TestAccCommunicationServiceEmailDomainAssociationResource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_communication_service_email_domain_association", "test") + r := CommunicationServiceEmailDomainAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCommunicationServiceEmailDomainAssociationResource_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_communication_service_email_domain_association", "test") + r := CommunicationServiceEmailDomainAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func (r CommunicationServiceEmailDomainAssociationResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := commonids.ParseCompositeResourceID(state.ID, &communicationservices.CommunicationServiceId{}, &domains.DomainId{}) + if err != nil { + return pointer.To(false), fmt.Errorf("parsing ID: %w", err) + } + + serviceClient := client.Communication.ServiceClient + existingCommunicationService, err := serviceClient.Get(ctx, *id.First) + if err != nil && !response.WasNotFound(existingCommunicationService.HttpResponse) { + return pointer.To(false), fmt.Errorf("checking for the presence of existing %s: %+v", id.First, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return pointer.To(false), fmt.Errorf("%s does not exist", id.First) + } + + input := existingCommunicationService + if input.Model != nil && input.Model.Properties != nil { + for _, v := range pointer.From(input.Model.Properties.LinkedDomains) { + tmpID, tmpErr := domains.ParseDomainID(v) + if tmpErr != nil { + return pointer.To(false), fmt.Errorf("parsing domain ID %q from LinkedDomains for %s: %+v", v, id.First, err) + } + if strings.EqualFold(id.Second.ID(), tmpID.ID()) { + return pointer.To(true), nil + } + } + } + + return pointer.To(false), nil +} + +func (r CommunicationServiceEmailDomainAssociationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_communication_service_email_domain_association" "test" { + communication_service_id = azurerm_communication_service.test.id + email_service_domain_id = azurerm_email_communication_service_domain.test.id +} +`, r.template(data)) +} + +func (r CommunicationServiceEmailDomainAssociationResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-communicationservice-%[1]d" + location = "%[2]s" +} + +resource "azurerm_communication_service" "test" { + name = "acctest-CommunicationService-%[1]d" + resource_group_name = azurerm_resource_group.test.name + data_location = "United States" + + tags = { + env = "Test2" + } +} + +resource "azurerm_email_communication_service" "test" { + name = "acctest-CommunicationService-%[1]d" + resource_group_name = azurerm_resource_group.test.name + data_location = "United States" +} + +resource "azurerm_email_communication_service_domain" "test" { + name = "AzureManagedDomain" + email_service_id = azurerm_email_communication_service.test.id + + domain_management = "AzureManaged" +} + +`, data.RandomInteger, data.Locations.Primary) +} + +func (r CommunicationServiceEmailDomainAssociationResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_communication_service_email_domain_association" "import" { + communication_service_id = azurerm_communication_service_email_domain_association.test.communication_service_id + email_service_domain_id = azurerm_communication_service_email_domain_association.test.email_service_domain_id +} +`, r.basic(data)) +} diff --git a/internal/services/communication/registration.go b/internal/services/communication/registration.go index e5c2ba26e497..85da0bec3480 100644 --- a/internal/services/communication/registration.go +++ b/internal/services/communication/registration.go @@ -37,6 +37,7 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ EmailCommunicationServiceDomainResource{}, EmailCommunicationServiceResource{}, + EmailDomainAssociationResource{}, CommunicationServiceResource{}, } } diff --git a/website/docs/r/communication_service_email_domain_association.html.markdown b/website/docs/r/communication_service_email_domain_association.html.markdown new file mode 100644 index 000000000000..08aa7cbdd0ee --- /dev/null +++ b/website/docs/r/communication_service_email_domain_association.html.markdown @@ -0,0 +1,74 @@ +--- +subcategory: "Communication" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_communication_service_email_domain_association" +description: |- + Manages a communication service email domain association. +--- + +# azurerm_communication_service_email_domain_association + +Manages a communication service email domain association. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "group1" + location = "West Europe" +} + +resource "azurerm_communication_service" "example" { + name = "CommunicationService1" + resource_group_name = azurerm_resource_group.example.name + data_location = "United States" +} + +resource "azurerm_email_communication_service" "example" { + name = "emailCommunicationService1" + resource_group_name = azurerm_resource_group.example.name + data_location = "United States" +} + +resource "azurerm_email_communication_service_domain" "example" { + name = "AzureManagedDomain" + email_service_id = azurerm_email_communication_service.example.id + + domain_management = "AzureManaged" +} + +resource "azurerm_communication_service_email_domain_association" "example" { + communication_service_id = azurerm_communication_service.example.id + email_service_domain_id = azurerm_email_communication_service_domain.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `communication_service_id` - (Required) The ID of the Communication Service. Changing this forces a new communication service email domain association to be created. + +* `email_service_domain_id` - (Required) The ID of the EMail Service Domain. Changing this forces a new communication service email domain association to be created. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the communication service email domain association. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 5 minutes) Used when creating the communication service email domain association. +* `read` - (Defaults to 5 minutes) Used when retrieving the communication service email domain association. +* `delete` - (Defaults to 5 minutes) Used when deleting the communication service email domain association. + +## Import + +Communication service email domain association can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_communication_service_email_domain_association.example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Communication/communicationServices/communicationService1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Communication/emailServices/emailCommunicationService1/domains/domain1" +``` \ No newline at end of file