diff --git a/internal/services/signalr/registration.go b/internal/services/signalr/registration.go index ba98c42e330d..d968194c42a0 100644 --- a/internal/services/signalr/registration.go +++ b/internal/services/signalr/registration.go @@ -19,6 +19,7 @@ func (r Registration) AssociatedGitHubLabel() string { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ CustomCertWebPubsubResource{}, + CustomCertSignalrServiceResource{}, } } diff --git a/internal/services/signalr/signalr_service_custom_certificate_resource.go b/internal/services/signalr/signalr_service_custom_certificate_resource.go new file mode 100644 index 000000000000..baaaf3b4a200 --- /dev/null +++ b/internal/services/signalr/signalr_service_custom_certificate_resource.go @@ -0,0 +1,213 @@ +package signalr + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/signalr/2023-02-01/signalr" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type CustomCertSignalrServiceResourceModel struct { + Name string `tfschema:"name"` + SignalRServiceId string `tfschema:"signalr_service_id"` + CustomCertId string `tfschema:"custom_certificate_id"` + CertificateVersion string `tfschema:"certificate_version"` +} + +type CustomCertSignalrServiceResource struct{} + +var _ sdk.Resource = CustomCertSignalrServiceResource{} + +func (r CustomCertSignalrServiceResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "signalr_service_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: signalr.ValidateSignalRID, + }, + + "custom_certificate_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.Any( + keyVaultValidate.NestedItemId, + keyVaultValidate.NestedItemIdWithOptionalVersion, + ), + }, + } +} + +func (r CustomCertSignalrServiceResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "certificate_version": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (r CustomCertSignalrServiceResource) ModelObject() interface{} { + return &CustomCertSignalrServiceResourceModel{} +} + +func (r CustomCertSignalrServiceResource) ResourceType() string { + return "azurerm_signalr_service_custom_certificate" +} + +func (r CustomCertSignalrServiceResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var customCertSignalrService CustomCertSignalrServiceResourceModel + if err := metadata.Decode(&customCertSignalrService); err != nil { + return err + } + client := metadata.Client.SignalR.SignalRClient + + signalRServiceId, err := signalr.ParseSignalRID(metadata.ResourceData.Get("signalr_service_id").(string)) + if err != nil { + return fmt.Errorf("parsing signalr service id error: %+v", err) + } + + keyVaultCertificateId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(metadata.ResourceData.Get("custom_certificate_id").(string)) + if err != nil { + return fmt.Errorf("parsing custom certificate id error: %+v", err) + } + + keyVaultUri := keyVaultCertificateId.KeyVaultBaseUrl + keyVaultSecretName := keyVaultCertificateId.Name + + id := signalr.NewCustomCertificateID(signalRServiceId.SubscriptionId, signalRServiceId.ResourceGroupName, signalRServiceId.SignalRName, metadata.ResourceData.Get("name").(string)) + + existing, err := client.CustomCertificatesGet(ctx, id) + if err != nil && !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing SignalR service custom cert error %s: %+v", id, err) + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + customCert := signalr.CustomCertificate{ + Properties: signalr.CustomCertificateProperties{ + KeyVaultBaseUri: keyVaultUri, + KeyVaultSecretName: keyVaultSecretName, + }, + } + + if certVersion := keyVaultCertificateId.Version; certVersion != "" { + if customCertSignalrService.CertificateVersion != "" && certVersion != customCertSignalrService.CertificateVersion { + return fmt.Errorf("certificate version in cert id is different from `certificate_version`") + } + customCert.Properties.KeyVaultSecretVersion = utils.String(certVersion) + } + + if err := client.CustomCertificatesCreateOrUpdateThenPoll(ctx, id, customCert); err != nil { + return fmt.Errorf("creating signalR custom certificate: %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r CustomCertSignalrServiceResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.SignalR.SignalRClient + keyVaultClient := metadata.Client.KeyVault + resourcesClient := metadata.Client.Resource + id, err := signalr.ParseCustomCertificateID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.CustomCertificatesGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("reading SignalR custom certificate %s: %+v", id, err) + } + + if resp.Model == nil { + return fmt.Errorf("retrieving %s: got nil model", *id) + } + + vaultBasedUri := resp.Model.Properties.KeyVaultBaseUri + certName := resp.Model.Properties.KeyVaultSecretName + + keyVaultIdRaw, err := keyVaultClient.KeyVaultIDFromBaseUrl(ctx, resourcesClient, vaultBasedUri) + if err != nil { + return fmt.Errorf("getting key vault base uri from %s: %+v", id, err) + } + vaultId, err := keyVaultParse.VaultID(*keyVaultIdRaw) + if err != nil { + return fmt.Errorf("parsing key vault %s: %+v", vaultId, err) + } + + signalrServiceId := signalr.NewSignalRID(id.SubscriptionId, id.ResourceGroupName, id.SignalRName).ID() + + certVersion := "" + if resp.Model.Properties.KeyVaultSecretVersion != nil { + certVersion = *resp.Model.Properties.KeyVaultSecretVersion + } + nestedItem, err := keyVaultParse.NewNestedItemID(vaultBasedUri, "certificates", certName, certVersion) + if err != nil { + return err + } + + certId := nestedItem.ID() + + state := CustomCertSignalrServiceResourceModel{ + Name: id.CustomCertificateName, + CustomCertId: certId, + SignalRServiceId: signalrServiceId, + CertificateVersion: utils.NormalizeNilableString(resp.Model.Properties.KeyVaultSecretVersion), + } + + return metadata.Encode(&state) + }, + } +} + +func (r CustomCertSignalrServiceResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.SignalR.SignalRClient + + id, err := signalr.ParseCustomCertificateID(metadata.ResourceData.Id()) + if err != nil { + return err + } + if _, err := client.CustomCertificatesDelete(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + return nil + }, + } +} + +func (r CustomCertSignalrServiceResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return signalr.ValidateCustomCertificateID +} diff --git a/internal/services/signalr/signalr_service_custom_certificate_resource_test.go b/internal/services/signalr/signalr_service_custom_certificate_resource_test.go new file mode 100644 index 000000000000..0df546e3ad7c --- /dev/null +++ b/internal/services/signalr/signalr_service_custom_certificate_resource_test.go @@ -0,0 +1,173 @@ +package signalr_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/signalr/2023-02-01/signalr" + "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" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type CustomCertSignalrServiceResource struct{} + +func TestAccCustomCertSignalrService_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_signalr_service_custom_certificate", "test") + r := CustomCertSignalrServiceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccCustomCertSignalrService_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_signalr_service_custom_certificate", "test") + r := CustomCertSignalrServiceResource{} + + 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 CustomCertSignalrServiceResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" { +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_signalr_service" "test" { + name = "acctestSignalR-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + name = "Premium_P1" + capacity = 1 + } + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_key_vault" "test" { + name = "acctestkeyvault%s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + certificate_permissions = [ + "Create", + "Delete", + "Get", + "Import", + "Purge", + "Recover", + "Update", + "List", + ] + + secret_permissions = [ + "Get", + "Set", + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_signalr_service.test.identity[0].principal_id + + certificate_permissions = [ + "Create", + "Delete", + "Get", + "Import", + "Purge", + "Recover", + "Update", + "List", + ] + + secret_permissions = [ + "Get", + "Set", + ] + } +} + +resource "azurerm_key_vault_certificate" "test" { + name = "acctestcert%s" + key_vault_id = azurerm_key_vault.test.id + + certificate { + contents = filebase64("testdata/certificate-to-import.pfx") + password = "" + } +} + +resource "azurerm_signalr_service_custom_certificate" "test" { + name = "signalr-cert-%s" + signalr_service_id = azurerm_signalr_service.test.id + custom_certificate_id = azurerm_key_vault_certificate.test.id + + depends_on = [azurerm_key_vault.test] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomString, data.RandomString, data.RandomString) +} + +func (r CustomCertSignalrServiceResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_signalr_service_custom_certificate" "import" { + name = azurerm_signalr_service_custom_certificate.test.name + signalr_service_id = azurerm_signalr_service_custom_certificate.test.signalr_service_id + custom_certificate_id = azurerm_signalr_service_custom_certificate.test.custom_certificate_id +} +`, r.basic(data)) +} + +func (r CustomCertSignalrServiceResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := signalr.ParseCustomCertificateID(state.ID) + if err != nil { + return nil, err + } + resp, err := client.SignalR.SignalRClient.CustomCertificatesGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + return utils.Bool(true), nil +} diff --git a/website/docs/r/signalr_service_custom_certificate.html.markdown b/website/docs/r/signalr_service_custom_certificate.html.markdown new file mode 100644 index 000000000000..b66280019b39 --- /dev/null +++ b/website/docs/r/signalr_service_custom_certificate.html.markdown @@ -0,0 +1,134 @@ +--- +subcategory: "Messaging" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_signalr_service_custom_certificate" +description: |- + Manages an Azure SignalR Custom Certificate. +--- + +# azurerm_signalr_service_custom_certificate + +Manages an Azure SignalR Custom Certificate. + +## Example Usage + +```hcl +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_signalr_service" "example" { + name = "example-signalr" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + name = "Premium_P1" + capacity = 1 + } + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_key_vault" "example" { + name = "example-keyvault" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "premium" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + certificate_permissions = [ + "Create", + "Get", + "List", + ] + + secret_permissions = [ + "Get", + "List", + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_signalr_service.test.identity[0].principal_id + + certificate_permissions = [ + "Create", + "Get", + "List", + ] + + secret_permissions = [ + "Get", + "List", + ] + } +} + +resource "azurerm_key_vault_certificate" "example" { + name = "imported-cert" + key_vault_id = azurerm_key_vault.example.id + + certificate { + contents = filebase64("certificate-to-import.pfx") + password = "" + } +} + +resource "azurerm_signalr_service_custom_certificate" "test" { + name = "example-cert" + signalr_service_id = azurerm_signalr_service.example.id + custom_certificate_id = azurerm_key_vault_certificate.example.id + + depends_on = [azurerm_key_vault_access_policy.example] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the SignalR Custom Certificate. Changing this forces a new resource to be created. + +* `signalr_service_id` - (Required) The SignalR ID of the SignalR Custom Certificate. Changing this forces a new resource to be created. + +-> **Note:** Custom Certificate is only available for SignalR Premium tier. Please enable managed identity in the corresponding SignalR Service and give the managed identity access to the key vault, the required permission is Get Certificate and Secret. + +* `custom_certificate_id` - (Required) The certificate id of the SignalR Custom Certificate service. Changing this forces a new resource to be created. + +-> **Note:** Self assigned certificate is not supported and the provisioning status will fail. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the SignalR Custom Certificate. + +* `certificate_version` - The certificate version of the SignalR Custom Certificate service. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Custom Certificate of the SignalR service +* `read` - (Defaults to 5 minutes) Used when retrieving the Custom Certificate of the SignalR service +* `delete` - (Defaults to 30 minutes) Used when deleting the Custom Certificate of the SignalR service + +## Import + +Custom Certificate for a SignalR service can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_signalr_service_custom_certificate.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.SignalRService/signalR/signalr1/customCertificates/cert1 +```