Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: azurerm_servicebus_namespace_customer_managed_key #28888

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/provider/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ func SupportedTypedServices() []sdk.TypedServiceRegistration {
search.Registration{},
securitycenter.Registration{},
sentinel.Registration{},
servicebus.Registration{},
serviceconnector.Registration{},
servicefabricmanaged.Registration{},
servicenetworking.Registration{},
Expand Down
12 changes: 12 additions & 0 deletions internal/services/servicebus/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource {

return resources
}

// DataSources returns the typed DataSources supported by this service
func (r Registration) DataSources() []sdk.DataSource {
return []sdk.DataSource{}
}

// Resources returns the typed Resources supported by this service
func (r Registration) Resources() []sdk.Resource {
return []sdk.Resource{
ServiceBusNamespaceCustomerManagedKeyResource{},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package servicebus

import (
"context"
"fmt"
"log"
"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/servicebus/2022-10-01-preview/namespaces"
"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"
)

type ServiceBusNamespaceCustomerManagedKeyResource struct{}

type ServiceBusNamespaceCustomerManagedKeyModel struct {
NamespaceID string `tfschema:"namespace_id"`
KeyVaultKeyID string `tfschema:"key_vault_key_id"`
IdentityID string `tfschema:"identity_id"`
InfrastructureEncryptionEnabled bool `tfschema:"infrastructure_encryption_enabled"`
}

var _ sdk.ResourceWithUpdate = ServiceBusNamespaceCustomerManagedKeyResource{}

func (r ServiceBusNamespaceCustomerManagedKeyResource) ModelObject() interface{} {
return &ServiceBusNamespaceCustomerManagedKeyModel{}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) ResourceType() string {
return "azurerm_servicebus_namespace_customer_managed_key"
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return namespaces.ValidateNamespaceID
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"namespace_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: namespaces.ValidateNamespaceID,
},
"key_vault_key_id": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion,
},
"identity_id": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: commonids.ValidateUserAssignedIdentityID,
},
"infrastructure_encryption_enabled": {
Type: pluginsdk.TypeBool,
Optional: true,
ForceNew: true,
},
}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.ServiceBus.NamespacesClient
var cmk ServiceBusNamespaceCustomerManagedKeyModel

if err := metadata.Decode(&cmk); err != nil {
return err
}

id, err := namespaces.ParseNamespaceID(cmk.NamespaceID)
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return fmt.Errorf("%s was not found", *id)
}
return fmt.Errorf("retrieving %s: %+v", *id, err)
}

if resp.Model == nil {
return fmt.Errorf("retrieving %s: `model` is nil", *id)
}

if resp.Model.Properties != nil && resp.Model.Properties.Encryption != nil && resp.Model.Properties.Encryption.KeyVaultProperties != nil && len(*resp.Model.Properties.Encryption.KeyVaultProperties) > 0 {
return metadata.ResourceRequiresImport(r.ResourceType(), *id)
}

payload := resp.Model

keyId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(cmk.KeyVaultKeyID)
if err != nil {
return err
}

payload.Properties.Encryption = &namespaces.Encryption{
RequireInfrastructureEncryption: pointer.To(cmk.InfrastructureEncryptionEnabled),
KeySource: pointer.To(namespaces.KeySourceMicrosoftPointKeyVault),
KeyVaultProperties: &[]namespaces.KeyVaultProperties{
{
KeyName: pointer.To(keyId.Name),
KeyVersion: pointer.To(keyId.Version),
KeyVaultUri: pointer.To(keyId.KeyVaultBaseUrl),
},
},
}

if cmk.IdentityID != "" {
identityId, err := commonids.ParseUserAssignedIdentityID(cmk.IdentityID)
if err != nil {
return err
}
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(identityId.ID()),
Comment on lines +124 to +129
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation function on the property will ensure that the user input is a user assigned identity ID with correct casing in the static segments, so there isn't strictly a need to parse that here if we're just passing the string value of it to the API

Suggested change
identityId, err := commonids.ParseUserAssignedIdentityID(cmk.IdentityID)
if err != nil {
return err
}
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(identityId.ID()),
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(cmk.IdentityID),

}
}

if err := client.CreateOrUpdateThenPoll(ctx, *id, *payload); err != nil {
return fmt.Errorf("creating Customer Managed Key for %s: %+v", *id, err)
}

metadata.SetID(id)
return nil
},
}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.ServiceBus.NamespacesClient

id, err := namespaces.ParseNamespaceID(metadata.ResourceData.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}
return fmt.Errorf("retrieving %s: %+v", *id, err)
}

var state ServiceBusNamespaceCustomerManagedKeyModel
state.NamespaceID = id.ID()

if props := resp.Model.Properties; props != nil && props.Encryption != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also nil check model here:

Suggested change
if props := resp.Model.Properties; props != nil && props.Encryption != nil {
if model := resp.Model; model != nil {
if props := model.Properties; props != nil && props.Encryption != nil {
...
}

encryption := props.Encryption
if keyVaultProperties := encryption.KeyVaultProperties; keyVaultProperties != nil && len(*keyVaultProperties) > 0 {
if identity := (*keyVaultProperties)[0].Identity; identity != nil {
identityId, err := commonids.ParseUserAssignedIdentityIDInsensitively(pointer.From(identity.UserAssignedIdentity))
if err != nil {
return err
}
state.IdentityID = identityId.ID()
}

keyVaultKeyId, err := keyVaultParse.NewNestedItemID(pointer.From((*keyVaultProperties)[0].KeyVaultUri), keyVaultParse.NestedItemTypeKey, pointer.From((*keyVaultProperties)[0].KeyName), pointer.From((*keyVaultProperties)[0].KeyVersion))
if err != nil {
return fmt.Errorf("parsing `key_vault_key_id`: %+v", err)
}
state.KeyVaultKeyID = keyVaultKeyId.ID()
}
state.InfrastructureEncryptionEnabled = pointer.From(encryption.RequireInfrastructureEncryption)
}
return metadata.Encode(&state)
},
}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.ServiceBus.NamespacesClient
var cmk ServiceBusNamespaceCustomerManagedKeyModel

if err := metadata.Decode(&cmk); err != nil {
return err
}

id, err := namespaces.ParseNamespaceID(cmk.NamespaceID)
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return fmt.Errorf("%s was not found", *id)
}
return fmt.Errorf("retrieving %s: %+v", *id, err)
}

if resp.Model == nil {
return fmt.Errorf("retrieving %s: `model` is nil", *id)
}

if resp.Model.Properties == nil {
return fmt.Errorf("retrieving %s: `properties` is nil", *id)
}

if resp.Model.Properties.Encryption == nil || resp.Model.Properties.Encryption.KeyVaultProperties == nil || len(*resp.Model.Properties.Encryption.KeyVaultProperties) == 0 {
return fmt.Errorf("retrieving %s: Customer Managed Key was not found", *id)
}

payload := resp.Model

if metadata.ResourceData.HasChange("key_vault_key_id") {
keyId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(cmk.KeyVaultKeyID)
if err != nil {
return err
}

(*payload.Properties.Encryption.KeyVaultProperties)[0].KeyName = pointer.To(keyId.Name)
(*payload.Properties.Encryption.KeyVaultProperties)[0].KeyVersion = pointer.To(keyId.Version)
(*payload.Properties.Encryption.KeyVaultProperties)[0].KeyVaultUri = pointer.To(keyId.KeyVaultBaseUrl)
}

if metadata.ResourceData.HasChange("identity_id") {
identityId, err := commonids.ParseUserAssignedIdentityID(cmk.IdentityID)
if err != nil {
return err
}
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(identityId.ID()),
}
Comment on lines +239 to +245
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
identityId, err := commonids.ParseUserAssignedIdentityID(cmk.IdentityID)
if err != nil {
return err
}
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(identityId.ID()),
}
(*payload.Properties.Encryption.KeyVaultProperties)[0].Identity = &namespaces.UserAssignedIdentityProperties{
UserAssignedIdentity: pointer.To(cmk.IdentityID),
}

}

if err := client.CreateOrUpdateThenPoll(ctx, *id, *payload); err != nil {
return fmt.Errorf("updating Customer Managed Key for %s: %+v", *id, err)
}

return nil
},
}
}

func (r ServiceBusNamespaceCustomerManagedKeyResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
log.Printf(`[INFO] Customer Managed Keys cannot be removed from Servicebus Namespaces once added. To remove the Customer Managed Key, delete and recreate the parent Servicebus Namespace`)
return nil
},
}
}
Loading
Loading