From 8254226255844e4fbccc813a31a3b8dd9d066a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= <mail@jkroepke.de> Date: Sat, 22 Jun 2024 18:08:30 +0200 Subject: [PATCH] New Resource: `azurerm_communication_service_email_domain_association` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de> --- ...rvice_email_domain_association_resource.go | 312 ++++++++++++++++++ ..._email_domain_association_resource_test.go | 210 ++++++++++++ .../services/communication/registration.go | 1 + ...ice_email_domain_association.html.markdown | 50 +++ 4 files changed, 573 insertions(+) create mode 100644 internal/services/communication/communication_service_email_domain_association_resource.go create mode 100644 internal/services/communication/communication_service_email_domain_association_resource_test.go create mode 100644 website/docs/r/communication_service_email_domain_association.html.markdown 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 0000000000000..435dbea85b1e7 --- /dev/null +++ b/internal/services/communication/communication_service_email_domain_association_resource.go @@ -0,0 +1,312 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package communication + +import ( + "context" + "fmt" + "log" + "slices" + "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 = CommunicationServiceEmailDomainAssociationResource{} + +type CommunicationServiceEmailDomainAssociationResource struct{} + +type CommunicationServiceEmailDomainAssociationResourceModel struct { + CommunicationServiceId string `tfschema:"communication_service_id"` + EMailServiceDomainId string `tfschema:"email_service_domain_id"` +} + +func (CommunicationServiceEmailDomainAssociationResource) 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 (CommunicationServiceEmailDomainAssociationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (CommunicationServiceEmailDomainAssociationResource) ModelObject() interface{} { + return &CommunicationServiceEmailDomainAssociationResourceModel{} +} + +func (CommunicationServiceEmailDomainAssociationResource) ResourceType() string { + return "azurerm_communication_service_email_domain_association" +} + +func (r CommunicationServiceEmailDomainAssociationResource) 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 CommunicationServiceEmailDomainAssociationResourceModel + + if err := metadata.Decode(&model); err != nil { + return err + } + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(model.CommunicationServiceId) + if err != nil { + return fmt.Errorf("parsing Communication Service ID: %w", err) + } + + eMailServiceDomainId, err := domains.ParseDomainID(model.EMailServiceDomainId) + if err != nil { + return fmt.Errorf("parsing EMail Service Domain ID: %w", 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", model.EMailServiceDomainId, err) + } + + if response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("EMail Service Domain %q does not exsits", model.EMailServiceDomainId) + } + + 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", model.CommunicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("Communication Service %q does not exists", model.CommunicationServiceId) + } + + if existingCommunicationService.Model == nil || existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("model/properties for %s was nil", model.CommunicationServiceId) + } + + domainList := existingCommunicationService.Model.Properties.LinkedDomains + if domainList == nil { + domainList = pointer.FromSliceOfStrings(make([]string, 0, 1)) + } + + if slices.Contains(*domainList, eMailServiceDomainId.ID()) { + return fmt.Errorf("EMail Service Domain %q is already associated with Communication Service %q", model.EMailServiceDomainId, model.CommunicationServiceId) + } + + *domainList = append(*domainList, eMailServiceDomainId.ID()) + existingCommunicationService.Model.Properties.LinkedDomains = domainList + + input := communicationservices.CommunicationServiceResourceUpdate{ + Properties: &communicationservices.CommunicationServiceUpdateProperties{ + LinkedDomains: domainList, + }, + } + + if _, err := client.Update(ctx, *communicationServiceId, input); err != nil { + return fmt.Errorf("updating %s: %+v", *communicationServiceId, err) + } + + id := commonids.NewCompositeResourceID(communicationServiceId, eMailServiceDomainId) + metadata.SetID(id) + + return nil + }, + } +} + +func (CommunicationServiceEmailDomainAssociationResource) 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 := CommunicationServiceEmailDomainAssociationResourceModel{} + state.CommunicationServiceId = id.First.ID() + state.EMailServiceDomainId = id.Second.ID() + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(state.CommunicationServiceId) + if err != nil { + return fmt.Errorf("parsing Communication Service ID: %w", err) + } + + eMailServiceDomainId, err := domains.ParseDomainID(state.EMailServiceDomainId) + if err != nil { + return fmt.Errorf("parsing EMail Service Domain ID: %w", 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 fmt.Errorf("EMail Service Domain %q does not exsits", state.EMailServiceDomainId) + } + + 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 fmt.Errorf("Communication Service %q does not exsits", state.CommunicationServiceId) + } + + if existingCommunicationService.Model == nil || existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("model/properties for %s was nil", state.CommunicationServiceId) + } + + domainList := existingCommunicationService.Model.Properties.LinkedDomains + if domainList == nil { + domainList = pointer.FromSliceOfStrings(make([]string, 0, 1)) + } + + if !slices.Contains(*domainList, eMailServiceDomainId.ID()) { + log.Printf("EMail Service Domain %q does not exsits in %q, removing from state.", eMailServiceDomainId, communicationServiceId) + err := metadata.MarkAsGone(id) + if err != nil { + return err + } + } + + return metadata.Encode(&state) + }, + } +} + +func (CommunicationServiceEmailDomainAssociationResource) 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 CommunicationServiceEmailDomainAssociationResourceModel + + if err := metadata.Decode(&model); err != nil { + return err + } + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(model.CommunicationServiceId) + if err != nil { + return fmt.Errorf("parsing Communication Service ID: %w", err) + } + + eMailServiceDomainId, err := domains.ParseDomainID(model.EMailServiceDomainId) + if err != nil { + return fmt.Errorf("parsing EMail Service Domain ID: %w", 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", model.EMailServiceDomainId, err) + } + + if response.WasNotFound(existingEMailServiceDomain.HttpResponse) { + return fmt.Errorf("EMail Service Domain %q does not exsits", model.EMailServiceDomainId) + } + + 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", model.CommunicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("Communication Service %q does not exsits", model.CommunicationServiceId) + } + + if existingCommunicationService.Model == nil || existingCommunicationService.Model.Properties == nil { + return fmt.Errorf("model/properties for %s was nil", model.CommunicationServiceId) + } + + domainList := existingCommunicationService.Model.Properties.LinkedDomains + if domainList == nil { + domainList = pointer.FromSliceOfStrings(make([]string, 0, 1)) + } + + id := commonids.NewCompositeResourceID(communicationServiceId, eMailServiceDomainId) + metadata.SetID(id) + + if !slices.Contains(*domainList, eMailServiceDomainId.ID()) { + return nil + } + + *domainList = slices.DeleteFunc(*domainList, func(n string) bool { + return n == eMailServiceDomainId.ID() + }) + + existingCommunicationService.Model.Properties.LinkedDomains = domainList + + input := communicationservices.CommunicationServiceResourceUpdate{ + Properties: &communicationservices.CommunicationServiceUpdateProperties{ + LinkedDomains: domainList, + }, + } + + if _, err := client.Update(ctx, *communicationServiceId, input); err != nil { + return fmt.Errorf("updating %s: %+v", *communicationServiceId, err) + } + + return nil + }, + } +} + +func (CommunicationServiceEmailDomainAssociationResource) 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 0000000000000..7f429d480284f --- /dev/null +++ b/internal/services/communication/communication_service_email_domain_association_resource_test.go @@ -0,0 +1,210 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package communication_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "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), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_communication_service_email_domain_association"), + }, + }) +} + +func TestAccCommunicationServiceEmailDomainAssociationResource_deleted(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.CheckWithClient(r.destroy), + ), + ExpectNonEmptyPlan: true, + }, + }) +} + +func (r CommunicationServiceEmailDomainAssociationResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + exists := false + + id, err := commonids.ParseCompositeResourceID(state.ID, &communicationservices.CommunicationServiceId{}, &domains.DomainId{}) + if err != nil { + return &exists, 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 &exists, fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", id.First, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return &exists, fmt.Errorf("PrivateEndpoint %q does not exsits", id.First) + } + + input := existingCommunicationService + if input.Model != nil && input.Model.Properties != nil && input.Model.Properties.LinkedDomains != nil { + exists = slices.Contains(*input.Model.Properties.LinkedDomains, id.Second.ID()) + } + + return &exists, 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.test.id + email_service_domain_id = azurerm_email_communication_service_domain.test.id +} +`, r.basic(data)) +} + +func (r CommunicationServiceEmailDomainAssociationResource) destroy(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) error { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(15*time.Minute)) + defer cancel() + + communicationServiceId, err := communicationservices.ParseCommunicationServiceID(state.Attributes["communication_service_id"]) + if err != nil { + return err + } + + eMailServiceDomainId, err := domains.ParseDomainID(state.Attributes["email_service_domain_id"]) + if err != nil { + return err + } + + serviceClient := client.Communication.ServiceClient + + existingCommunicationService, err := serviceClient.Get(ctx, *communicationServiceId) + if err != nil && !response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("checking for the presence of existing CommunicationService %q: %+v", communicationServiceId, err) + } + + if response.WasNotFound(existingCommunicationService.HttpResponse) { + return fmt.Errorf("CommunicationService %q does not exsits", communicationServiceId) + } + + if existingCommunicationService.Model == nil || existingCommunicationService.Model.Properties == nil || existingCommunicationService.Model.Properties.LinkedDomains == nil { + return fmt.Errorf("model/properties/application security groups was missing for %s", communicationServiceId) + } + + // flag: application security group exists in private endpoint configuration + LinkedDomainInCommService := false + + input := existingCommunicationService + LinkedDomainsList := *input.Model.Properties.LinkedDomains + newLinkedDomainsList := make([]string, 0) + for idx, value := range LinkedDomainsList { + if value == eMailServiceDomainId.ID() { + newLinkedDomainsList = append(newLinkedDomainsList, LinkedDomainsList[:idx]...) + newLinkedDomainsList = append(newLinkedDomainsList, LinkedDomainsList[idx+1:]...) + LinkedDomainInCommService = true + break + } + } + if LinkedDomainInCommService { + input.Model.Properties.LinkedDomains = &newLinkedDomainsList + } else { + return fmt.Errorf("deletion failed, EmailServiceDomain %q does not linked with CommunicationService %q", eMailServiceDomainId, communicationServiceId) + } + + if err = serviceClient.CreateOrUpdateThenPoll(ctx, *communicationServiceId, *input.Model); err != nil { + return fmt.Errorf("creating %s: %+v", communicationServiceId, err) + } + + return nil +} diff --git a/internal/services/communication/registration.go b/internal/services/communication/registration.go index e5c2ba26e4977..0f58c62e4d2a7 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{}, + CommunicationServiceEmailDomainAssociationResource{}, 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 0000000000000..0c43419584532 --- /dev/null +++ b/website/docs/r/communication_service_email_domain_association.html.markdown @@ -0,0 +1,50 @@ +--- +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_communication_service_email_domain_association" "example" { + communication_service_id = "TODO" + email_service_domain_id = "TODO" +} +``` + +## 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