Skip to content

Commit

Permalink
managedhsm: introducing dedicated Resource ID Parsers for the Data Pl…
Browse files Browse the repository at this point in the history
…ane Versioned and Versionless Key IDs (#25601)

* managedhsm: introducing dedicated Resource ID Parsers for the Data Plane Versioned and Versionless Key IDs

* `managedhsms`: updating the Resource ID parser for Managed HSM Role Assignment

This switches to using the Resource ID the Resource actually uses rather than this apparent Terraform unique value?

* `managedhsm`: refactoring the Managed HSM Role Assignment Resource

This now uses `managed_hsm_id` to discover the Managed HSM rather than the Data Plane URI - which mirrors the pattern used elsewhere.
This is important for two reasons:

1. We don't support provisioning resources across Subscriptions - a unique Provider instance needs to be used for each Subscription
2. This allows us to determine when the Managed HSM in question has been removed out-of-band due to limitations in Go's networking layer

* `managedhsm`: updating the Parser tests/adding extra tests covering the Parse function directly

This was tested via the validate, but was missing tests covering this directly

* `managedhsm`: refactoring the Role Definition resource

* `managedhsm`: refactoring the Managed HSM Role Definition Data Source

* `managedhsm`: tests covering the Managed HSM Role Definition Data Source

* `managedhsm`: implementing the helpers package

* `managedhsm`: populating the cache and removing during creation/deletion

* imports

* managedhsm: move cache population into own method

* managedhsm: validate port number if present in URI

* managedhsm: test fixes, comments

* managedhsm: azurerm_key_vault_managed_hardware_security_module test cleanup

* managedhsm: don't trim the scope when outputting role definition/assignment IDs

This causes "/" to become "", and "/keys" to become "keys"

We only trim the first leading slash when parsing, not when outputting.

* managedhsm: eventual consistency workarounds for role definitions/assignments

* managedhsm: test for invalid port handling in ID migrations

* managedhsm: scope not a valid property for this resource

* managedhsm: acceptance test fixes

* linting

* managedhsm: base URI should have trailing slash

* managedhsm: fix hsm role definition data source tests

* remove check

---------

Co-authored-by: Tom Bamford <[email protected]>
Co-authored-by: kt <[email protected]>
  • Loading branch information
3 people authored May 2, 2024
1 parent b9c77b8 commit 97ac087
Show file tree
Hide file tree
Showing 40 changed files with 3,142 additions and 886 deletions.
169 changes: 169 additions & 0 deletions internal/services/managedhsm/client/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package client

import (
"context"
"fmt"
"strings"
"sync"

"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse"
)

var (
cache = map[string]managedHSMDetail{}
keysmith = &sync.RWMutex{}
lock = map[string]*sync.RWMutex{}
)

type managedHSMDetail struct {
managedHSMId managedhsms.ManagedHSMId
dataPlaneBaseUri string
}

func (c *Client) AddToCache(managedHsmId managedhsms.ManagedHSMId, dataPlaneUri string) {
cacheKey := c.cacheKeyForManagedHSM(managedHsmId.ManagedHSMName)
keysmith.Lock()
cache[cacheKey] = managedHSMDetail{
managedHSMId: managedHsmId,
dataPlaneBaseUri: dataPlaneUri,
}
keysmith.Unlock()
}

func (c *Client) BaseUriForManagedHSM(ctx context.Context, managedHsmId managedhsms.ManagedHSMId) (*string, error) {
cacheKey := c.cacheKeyForManagedHSM(managedHsmId.ManagedHSMName)
keysmith.Lock()
if lock[cacheKey] == nil {
lock[cacheKey] = &sync.RWMutex{}
}
keysmith.Unlock()
lock[cacheKey].Lock()
defer lock[cacheKey].Unlock()

if v, ok := cache[cacheKey]; ok {
return &v.dataPlaneBaseUri, nil
}

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

dataPlaneUri := ""
if model := resp.Model; model != nil {
if model.Properties.HsmUri != nil {
dataPlaneUri = *model.Properties.HsmUri
}
}
if dataPlaneUri == "" {
return nil, fmt.Errorf("retrieving %s: `properties.HsmUri` was nil", managedHsmId)
}

c.AddToCache(managedHsmId, dataPlaneUri)
return &dataPlaneUri, nil
}

func (c *Client) Purge(managedHSMId managedhsms.ManagedHSMId) {
cacheKey := c.cacheKeyForManagedHSM(managedHSMId.ManagedHSMName)
keysmith.Lock()
if lock[cacheKey] == nil {
lock[cacheKey] = &sync.RWMutex{}
}
keysmith.Unlock()
lock[cacheKey].Lock()
delete(cache, cacheKey)
lock[cacheKey].Unlock()
}

func (c *Client) ManagedHSMIDFromBaseUrl(ctx context.Context, subscriptionId commonids.SubscriptionId, managedHsmBaseUrl string, domainSuffix *string) (*managedhsms.ManagedHSMId, error) {
endpoint, err := parse.ManagedHSMEndpoint(managedHsmBaseUrl, domainSuffix)
if err != nil {
return nil, err
}

cacheKey := c.cacheKeyForManagedHSM(endpoint.ManagedHSMName)
keysmith.Lock()
if lock[cacheKey] == nil {
lock[cacheKey] = &sync.RWMutex{}
}
keysmith.Unlock()
lock[cacheKey].Lock()
defer lock[cacheKey].Unlock()

// Check the cache to determine if we have an entry for this Managed HSM
if v, ok := cache[cacheKey]; ok {
return &v.managedHSMId, nil
}

// Populate the cache if not found
if err = c.populateCache(ctx, subscriptionId); err != nil {
return nil, err
}

// Now that the cache has been repopulated, check if we have the Managed HSM or not
if v, ok := cache[cacheKey]; ok {
return &v.managedHSMId, nil
}

// We haven't found it, but Data Sources and Resources need to handle this error separately
return nil, nil
}

func (c *Client) cacheKeyForManagedHSM(name string) string {
return strings.ToLower(name)
}

func (c *Client) populateCache(ctx context.Context, subscriptionId commonids.SubscriptionId) error {
// Pull out the list of Managed HSMs available within the Subscription to re-populate the cache
//
// Whilst we've historically used the Resources API to query the single Managed HSM in question
// this endpoint has caching related issues - and whilst the ResourceGraph API has been suggested
// as an alternative that fixes this, we've seen similar caching issues there.
// Therefore, we're falling back on querying all the Managed HSMs within the specified Subscription, which
// comes from the `KeyVault` Resource Provider rather than the `Resources` Resource Provider - which
// is an approach we've used previously, but now with better caching.
//
// Whilst querying ALL Managed HSMs within a Subscription IS excessive where only a single Managed HSM
// is used - having the cache populated (one-time, per Provider launch) should alleviate problems
// in Terraform Configurations defining a large number of Managed HSM related items.
//
// Note that we will only populate the cache on demand where HSM resources are actually being managed, to avoid
// the overhead in configurations where HSM is not used.
//
// Finally, it's worth noting that we intentionally List ALL the Managed HSMs within a Subscription
// to be able to cache ALL of them - prior to looking up the specific Managed HSM we're interested
// in from the freshly populated cache.
// This fixes an issue in the previous implementation where the Cache was being repeatedly semi-populated
// until the specified Managed HSM was found, at which point we skipped populating the cache, which
// affected both the `Resources` API implementation:
// https://github.com/hashicorp/terraform-provider-azurerm/blob/3e88e5e74e12577d785f10298281b1b3c172254f/internal/services/keyvault/client/helpers.go#L133-L173
// and the `ListBySubscription` endpoint:
// https://github.com/hashicorp/terraform-provider-azurerm/blob/a5e728dc62e832e74d7bb0f40a79af0ae5a79e1e/azurerm/helpers/azure/key_vault.go#L42-L89

opts := managedhsms.DefaultListBySubscriptionOperationOptions()
results, err := c.ManagedHsmClient.ListBySubscriptionComplete(ctx, subscriptionId, opts)
if err != nil {
return fmt.Errorf("listing the Managed HSMs within %s: %+v", subscriptionId, err)
}
for _, item := range results.Items {
if item.Id == nil || item.Properties.HsmUri == nil {
continue
}

// Populate the Managed HSM into the cache
managedHsm, err := managedhsms.ParseManagedHSMIDInsensitively(*item.Id)
if err != nil {
return fmt.Errorf("parsing %q as a Managed HSM ID: %+v", *item.Id, err)
}
dataPlaneUri := *item.Properties.HsmUri
c.AddToCache(*managedHsm, dataPlaneUri)
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,13 @@ func resourceKeyVaultManagedHardwareSecurityModule() *pluginsdk.Resource {
}

func resourceArmKeyVaultManagedHardwareSecurityModuleCreate(d *pluginsdk.ResourceData, meta interface{}) error {
kvClient := meta.(*clients.Client).ManagedHSMs
hsmClient := kvClient.ManagedHsmClient
client := meta.(*clients.Client).ManagedHSMs
subscriptionId := meta.(*clients.Client).Account.SubscriptionId
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

id := managedhsms.NewManagedHSMID(subscriptionId, d.Get("resource_group_name").(string), d.Get("name").(string))
existing, err := hsmClient.Get(ctx, id)
existing, err := client.ManagedHsmClient.Get(ctx, id)
if err != nil {
if !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
Expand Down Expand Up @@ -221,20 +220,33 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleCreate(d *pluginsdk.Resourc
hsm.Properties.TenantId = pointer.To(tenantId)
}

if err := hsmClient.CreateOrUpdateThenPoll(ctx, id, hsm); err != nil {
if err := client.ManagedHsmClient.CreateOrUpdateThenPoll(ctx, id, hsm); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}

d.SetId(id.ID())

dataPlaneUri := ""
resp, err := client.ManagedHsmClient.Get(ctx, id)
if err != nil {
return fmt.Errorf("retrieving %s: %+v", id, err)
}
if model := resp.Model; model != nil && model.Properties != nil && model.Properties.HsmUri != nil {
dataPlaneUri = *model.Properties.HsmUri
}
if dataPlaneUri == "" {
return fmt.Errorf("retrieving %s: `properties.HsmUri` was nil", id)
}
client.AddToCache(id, dataPlaneUri)

// security domain download to activate this module
if ok := d.HasChange("security_domain_key_vault_certificate_ids"); ok {
// get hsm uri
resp, err := hsmClient.Get(ctx, id)
resp, err := client.ManagedHsmClient.Get(ctx, id)
if err != nil || resp.Model == nil || resp.Model.Properties == nil || resp.Model.Properties.HsmUri == nil {
return fmt.Errorf("got nil HSMUri for %s: %+v", id, err)
}
encData, err := securityDomainDownload(ctx, kvClient, *resp.Model.Properties.HsmUri, d.Get("security_domain_key_vault_certificate_ids").([]interface{}), d.Get("security_domain_quorum").(int))
encData, err := securityDomainDownload(ctx, client, *resp.Model.Properties.HsmUri, d.Get("security_domain_key_vault_certificate_ids").([]interface{}), d.Get("security_domain_quorum").(int))
if err != nil {
return fmt.Errorf("downloading security domain for %q: %+v", id, err)
}
Expand Down Expand Up @@ -358,7 +370,7 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleRead(d *pluginsdk.ResourceD
}

func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.ResourceData, meta interface{}) error {
hsmClient := meta.(*clients.Client).ManagedHSMs.ManagedHsmClient
client := meta.(*clients.Client).ManagedHSMs
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand All @@ -367,8 +379,8 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc
return err
}

// We need to grab the keyvault hsm to see if purge protection is enabled prior to deletion
resp, err := hsmClient.Get(ctx, *id)
// We need to grab the managed hsm to see if purge protection is enabled prior to deletion
resp, err := client.ManagedHsmClient.Get(ctx, *id)
if err != nil {
return fmt.Errorf("retrieving %s: %+v", id, err)
}
Expand All @@ -382,7 +394,7 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc
}
}

if err := hsmClient.DeleteThenPoll(ctx, *id); err != nil {
if err := client.ManagedHsmClient.DeleteThenPoll(ctx, *id); err != nil {
return fmt.Errorf("deleting %s: %+v", id, err)
}

Expand All @@ -397,16 +409,18 @@ func resourceArmKeyVaultManagedHardwareSecurityModuleDelete(d *pluginsdk.Resourc
// try to purge again if managed HSM still exists after 1 minute
// for API issue: https://github.com/Azure/azure-rest-api-specs/issues/27138
purgeId := managedhsms.NewDeletedManagedHSMID(id.SubscriptionId, loc, id.ManagedHSMName)
if _, err := hsmClient.PurgeDeleted(ctx, purgeId); err != nil {
if _, err := client.ManagedHsmClient.PurgeDeleted(ctx, purgeId); err != nil {
return fmt.Errorf("purging %s: %+v", id, err)
}

purgePoller := custompollers.NewHSMPurgePoller(hsmClient, purgeId)
purgePoller := custompollers.NewHSMPurgePoller(client.ManagedHsmClient, purgeId)
poller := pollers.NewPoller(purgePoller, time.Second*30, pollers.DefaultNumberOfDroppedConnectionsToAllow)
if err := poller.PollUntilDone(ctx); err != nil {
return fmt.Errorf("waiting for %s to be purged: %+v", id, err)
}

client.Purge(*id)

return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,36 @@ import (
type KeyVaultManagedHardwareSecurityModuleResource struct{}

func TestAccKeyVaultManagedHardwareSecurityModule(t *testing.T) {
// NOTE: this is a combined test rather than separate split out tests due to
// Azure only being able provision against one instance at a time
// @manicminer: these tests are sequential due to low service limits for Managed HSM
// (max 5 instances per subscription as of 2024-04-23)
// and to try and maintain a little headroom for cleanup after failed tests
acceptance.RunTestsInSequence(t, map[string]map[string]func(t *testing.T){
"dataSource": {
"basic": testAccDataSourceKeyVaultManagedHardwareSecurityModule_basic,
},
"resource": {
"data_source": testAccDataSourceKeyVaultManagedHardwareSecurityModule_basic,
"basic": testAccKeyVaultManagedHardwareSecurityModule_basic,
"update": testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport,
"complete": testAccKeyVaultManagedHardwareSecurityModule_complete,
"download": testAccKeyVaultManagedHardwareSecurityModule_download,
"role_define": testAccKeyVaultManagedHardwareSecurityModule_roleDefinition,
"role_assign": testAccKeyVaultManagedHardwareSecurityModule_roleAssignment,
"basic": testAccKeyVaultManagedHardwareSecurityModule_basic,
"update": testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport,
"complete": testAccKeyVaultManagedHardwareSecurityModule_complete,
"download": testAccKeyVaultManagedHardwareSecurityModule_download,
},
"roleAssignments": {
"builtInRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_builtInRole,
"customRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_customRole,

// TODO: uses `vault_base_url`, these 2 can be removed in 4.0
"legacyBuiltInRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyBuiltInRole,
"legacyCustomRole": testAccKeyVaultManagedHardwareSecurityModuleRoleAssignment_legacyCustomRole,
},
"roleDefinitions": {
"basic": testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic,

// TODO: uses `vault_base_url`, this can be removed in 4.0
"legacyWithUpdate": testAccKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacyWithUpdate,
},
"roleDefinitionDataSource": {
"basic": testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_basic,
"legacy": testAccDataSourceKeyVaultManagedHardwareSecurityModuleRoleDefinition_legacy,
},
})
}
Expand Down Expand Up @@ -78,50 +97,6 @@ func testAccKeyVaultManagedHardwareSecurityModule_download(t *testing.T) {
})
}

func testAccKeyVaultManagedHardwareSecurityModule_roleDefinition(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_definition", "test")
r := KeyVaultMHSMRoleDefinitionResource{}

data.ResourceSequentialTest(t, r, []acceptance.TestStep{
{
Config: r.withRoleDefinition(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
{
Config: r.withRoleDefinitionUpdate(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func testAccKeyVaultManagedHardwareSecurityModule_roleAssignment(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_role_assignment", "test")
r := KeyVaultManagedHSMRoleAssignmentResource{}

data.ResourceSequentialTest(t, r, []acceptance.TestStep{
{
Config: r.withRoleAssignment(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
{
Config: r.withBuiltInRoleAssignment(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module", "test")
r := KeyVaultManagedHardwareSecurityModuleResource{}
Expand Down
Loading

0 comments on commit 97ac087

Please sign in to comment.