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

[BUG:] azurerm_mssql_elasticpool/azurerm_mssql_database - update the behavior of the enclave_type field. #25508

Merged
merged 6 commits into from
Apr 5, 2024
Merged
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
4 changes: 2 additions & 2 deletions internal/services/mssql/helper/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ func FindDatabaseReplicationPartners(ctx context.Context, databasesClient *datab
}

if partnerDatabase.Id != nil && partnerDatabase.Properties != nil && partnerDatabase.Properties.PreferredEnclaveType != nil {
if primaryEnclaveType == *partnerDatabase.Properties.PreferredEnclaveType {
if primaryEnclaveType != "" && primaryEnclaveType == *partnerDatabase.Properties.PreferredEnclaveType {
log.Printf("[INFO] Found Partner %s", partnerDatabaseId)
partnerDatabases = append(partnerDatabases, *partnerDatabase)
} else {
log.Printf("[INFO] Mismatch of possible Partner Database based on enclave type (%s vs %s) for %s", string(primaryEnclaveType), string(*partnerDatabase.Properties.PreferredEnclaveType), id)
log.Printf("[INFO] Mismatch of possible Partner Database based on enclave type (%q vs %q) for %s", primaryEnclaveType, string(*partnerDatabase.Properties.PreferredEnclaveType), id)
}
}
}
Expand Down
64 changes: 42 additions & 22 deletions internal/services/mssql/mssql_database_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/backupshorttermretentionpolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/databases"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/databasesecurityalertpolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/elasticpools"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/geobackuppolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/longtermretentionpolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-02-01-preview/servers"
Expand Down Expand Up @@ -69,9 +70,20 @@ func resourceMsSqlDatabase() *pluginsdk.Resource {

CustomizeDiff: pluginsdk.CustomDiffWithAll(
pluginsdk.ForceNewIfChange("sku_name", func(ctx context.Context, old, new, _ interface{}) bool {
// "hyperscale can not change to other sku
// hyperscale can not be changed to another sku
return strings.HasPrefix(old.(string), "HS") && !strings.HasPrefix(new.(string), "HS")
}),
pluginsdk.ForceNewIfChange("enclave_type", func(ctx context.Context, old, new, _ interface{}) bool {
// enclave_type cannot be removed once it has been set
// but can be changed between VBS and Default...
// this Diff will not work until 4.0 when we remove
// the computed property from the field scheam.
if old.(string) != "" && new.(string) == "" {
return true
}

return false
}),
func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
transparentDataEncryption := d.Get("transparent_data_encryption_enabled").(bool)
skuName := d.Get("sku_name").(string)
Expand Down Expand Up @@ -102,10 +114,12 @@ func resourceMsSqlDatabaseImporter(ctx context.Context, d *pluginsdk.ResourceDat
return nil, err
}

enclaveType := databases.AlwaysEncryptedEnclaveTypeDefault
if v := d.Get("enclave_type").(string); v != "" {
enclaveType = databases.AlwaysEncryptedEnclaveTypeVBS
// NOTE: The service default is actually nil/empty which indicates enclave is disabled. the value `Default` is NOT the default.
var enclaveType databases.AlwaysEncryptedEnclaveType
if v, ok := d.GetOk("enclave_type"); ok && v.(string) != "" {
enclaveType = databases.AlwaysEncryptedEnclaveType(v.(string))
}
d.Set("enclave_type", enclaveType)

partnerDatabases, err := helper.FindDatabaseReplicationPartners(ctx, client, legacyreplicationLinksClient, resourcesClient, *id, enclaveType, []sql.ReplicationRole{sql.ReplicationRolePrimary})
if err != nil {
Expand Down Expand Up @@ -200,11 +214,10 @@ func resourceMsSqlDatabaseCreate(d *pluginsdk.ResourceData, meta interface{}) er
locks.ByID(id.ID())
defer locks.UnlockByID(id.ID())

// NOTE: Set the default value, if the field exists in the config the only value
// that it could be is 'VBS'...
enclaveType := databases.AlwaysEncryptedEnclaveTypeDefault
if _, ok := d.GetOk("enclave_type"); ok {
enclaveType = databases.AlwaysEncryptedEnclaveTypeVBS
// NOTE: The service default is actually nil/empty which indicates enclave is disabled. the value `Default` is NOT the default.
var enclaveType databases.AlwaysEncryptedEnclaveType
if v, ok := d.GetOk("enclave_type"); ok && v.(string) != "" {
enclaveType = databases.AlwaysEncryptedEnclaveType(v.(string))
}

skuName := d.Get("sku_name").(string)
Expand Down Expand Up @@ -247,7 +260,7 @@ func resourceMsSqlDatabaseCreate(d *pluginsdk.ResourceData, meta interface{}) er
}

// NOTE: If the database is being added to an elastic pool, we need to GET the elastic pool and check
// if the 'enclave_type' match. If they don't we need to raise an error stating that they must match.
// if the 'enclave_type' matches. If they don't we need to raise an error stating that they must match.
elasticPoolId := d.Get("elastic_pool_id").(string)
if elasticPoolId != "" {
elasticId, err := commonids.ParseSqlElasticPoolID(elasticPoolId)
Expand Down Expand Up @@ -291,7 +304,7 @@ func resourceMsSqlDatabaseCreate(d *pluginsdk.ResourceData, meta interface{}) er
}

// NOTE: The 'PreferredEnclaveType' field cannot be passed to the APIs Create if the 'sku_name' is a DW or DC-series SKU...
if !strings.HasPrefix(strings.ToLower(skuName), "dw") && !strings.Contains(strings.ToLower(skuName), "_dc_") {
if !strings.HasPrefix(strings.ToLower(skuName), "dw") && !strings.Contains(strings.ToLower(skuName), "_dc_") && enclaveType != "" {
input.Properties.PreferredEnclaveType = pointer.To(enclaveType)
}

Expand Down Expand Up @@ -687,15 +700,17 @@ func resourceMsSqlDatabaseUpdate(d *pluginsdk.ResourceData, meta interface{}) er
}

if d.HasChange("enclave_type") {
enclaveType := databases.AlwaysEncryptedEnclaveTypeDefault
if v := d.Get("enclave_type").(string); v != "" {
enclaveType = databases.AlwaysEncryptedEnclaveTypeVBS
var enclaveType databases.AlwaysEncryptedEnclaveType
if v, ok := d.GetOk("enclave_type"); ok && v.(string) != "" {
enclaveType = databases.AlwaysEncryptedEnclaveType(v.(string))
}

// The 'PreferredEnclaveType' field cannot be passed to the APIs Update if the
// 'sku_name' is a DW or DC-series SKU...
if !strings.HasPrefix(strings.ToLower(skuName), "dw") && !strings.Contains(strings.ToLower(skuName), "_dc_") {
if !strings.HasPrefix(strings.ToLower(skuName), "dw") && !strings.Contains(strings.ToLower(skuName), "_dc_") && enclaveType != "" {
props.PreferredEnclaveType = pointer.To(enclaveType)
} else {
props.PreferredEnclaveType = nil
}

// If the database belongs to an elastic pool, we need to GET the elastic pool and check
Expand All @@ -712,12 +727,14 @@ func resourceMsSqlDatabaseUpdate(d *pluginsdk.ResourceData, meta interface{}) er
return fmt.Errorf("retrieving %s: %s", elasticId, err)
}

var elasticEnclaveType elasticpools.AlwaysEncryptedEnclaveType
if elasticPool.Model != nil && elasticPool.Model.Properties != nil && elasticPool.Model.Properties.PreferredEnclaveType != nil {
elasticEnclaveType := string(pointer.From(elasticPool.Model.Properties.PreferredEnclaveType))
databaseEnclaveType := string(enclaveType)
elasticEnclaveType = pointer.From(elasticPool.Model.Properties.PreferredEnclaveType)
}

if !strings.EqualFold(elasticEnclaveType, databaseEnclaveType) {
return fmt.Errorf("updating the %s with enclave type %q to the %s with enclave type %q is not supported. Before updating a database that belongs to an elastic pool please ensure that the 'enclave_type' is the same for both the database and the elastic pool", id, databaseEnclaveType, elasticId, elasticEnclaveType)
if elasticEnclaveType != "" || enclaveType != "" {
if !strings.EqualFold(string(elasticEnclaveType), string(enclaveType)) {
return fmt.Errorf("updating the %s with enclave type %q to the %s with enclave type %q is not supported. Before updating a database that belongs to an elastic pool please ensure that the 'enclave_type' is the same for both the database and the elastic pool", id, enclaveType, elasticId, elasticEnclaveType)
}
}
}
Expand Down Expand Up @@ -776,7 +793,7 @@ func resourceMsSqlDatabaseUpdate(d *pluginsdk.ResourceData, meta interface{}) er

// Place a lock for the current database so any partner resources can't bump its SKU out of band
if skuName != "" {
existingEnclaveType := databases.AlwaysEncryptedEnclaveTypeDefault
var existingEnclaveType databases.AlwaysEncryptedEnclaveType
if model := existing.Model; model != nil && model.Properties != nil && model.Properties.PreferredEnclaveType != nil {
existingEnclaveType = *model.Properties.PreferredEnclaveType
}
Expand Down Expand Up @@ -1128,8 +1145,9 @@ func resourceMsSqlDatabaseRead(d *pluginsdk.ResourceData, meta interface{}) erro
ledgerEnabled = *props.IsLedgerOn
}

// NOTE: Always set the PreferredEnclaveType to an empty string unless it isn't 'Default'...
if v := props.PreferredEnclaveType; v != nil && pointer.From(v) != databases.AlwaysEncryptedEnclaveTypeDefault {
// NOTE: Always set the PreferredEnclaveType to an empty string
// if not in the properties that were returned from Azure...
if v := props.PreferredEnclaveType; v != nil {
enclaveType = string(pointer.From(v))
}

Expand Down Expand Up @@ -1506,8 +1524,10 @@ func resourceMsSqlDatabaseSchema() map[string]*pluginsdk.Schema {
"enclave_type": {
Type: pluginsdk.TypeString,
Optional: true,
Computed: true, // TODO: Remove Computed in 4.0
ValidateFunc: validation.StringInSlice([]string{
string(databases.AlwaysEncryptedEnclaveTypeVBS),
string(databases.AlwaysEncryptedEnclaveTypeDefault),
}, false),
},

Expand Down
18 changes: 14 additions & 4 deletions internal/services/mssql/mssql_database_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestAccMsSqlDatabase_complete(t *testing.T) {
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("license_type").HasValue("LicenseIncluded"),
check.That(data.ResourceName).Key("max_size_gb").HasValue("2"),
check.That(data.ResourceName).Key("enclave_type").IsEmpty(),
check.That(data.ResourceName).Key("enclave_type").HasValue("Default"),
check.That(data.ResourceName).Key("tags.%").HasValue("1"),
check.That(data.ResourceName).Key("tags.ENV").HasValue("Staging"),
),
Expand Down Expand Up @@ -844,12 +844,13 @@ func TestAccMsSqlDatabase_enclaveType(t *testing.T) {
}

func TestAccMsSqlDatabase_enclaveTypeUpdate(t *testing.T) {
// NOTE: Once the enclave_type field has be set it cannot be changed...
data := acceptance.BuildTestData(t, "azurerm_mssql_database", "test")
r := MsSqlDatabaseResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.enclaveType(data, ""),
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("enclave_type").IsEmpty(),
Expand All @@ -865,10 +866,18 @@ func TestAccMsSqlDatabase_enclaveTypeUpdate(t *testing.T) {
},
data.ImportStep(),
{
Config: r.enclaveType(data, ""),
Config: r.enclaveType(data, ` enclave_type = "Default"`),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("enclave_type").IsEmpty(),
check.That(data.ResourceName).Key("enclave_type").HasValue("Default"),
),
},
data.ImportStep(),
{
Config: r.enclaveType(data, ` enclave_type = "VBS"`),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("enclave_type").HasValue("VBS"),
),
},
data.ImportStep(),
Expand Down Expand Up @@ -1067,6 +1076,7 @@ resource "azurerm_mssql_database" "test" {
license_type = "LicenseIncluded"
max_size_gb = 2
sku_name = "GP_Gen5_2"
enclave_type = "Default"

storage_account_type = "Zone"

Expand Down
46 changes: 29 additions & 17 deletions internal/services/mssql/mssql_elasticpool_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ func resourceMsSqlElasticPool() *pluginsdk.Resource {
"enclave_type": {
Type: pluginsdk.TypeString,
Optional: true,
Computed: true, // TODO: Remove Computed in 4.0
ValidateFunc: validation.StringInSlice([]string{
string(databases.AlwaysEncryptedEnclaveTypeVBS),
string(databases.AlwaysEncryptedEnclaveTypeDefault),
}, false),
},

Expand All @@ -200,13 +202,27 @@ func resourceMsSqlElasticPool() *pluginsdk.Resource {
"tags": commonschema.Tags(),
},

CustomizeDiff: pluginsdk.CustomizeDiffShim(func(ctx context.Context, diff *pluginsdk.ResourceDiff, v interface{}) error {
if err := helper.MSSQLElasticPoolValidateSKU(diff); err != nil {
return err
}
CustomizeDiff: pluginsdk.CustomDiffWithAll(
func(ctx context.Context, diff *pluginsdk.ResourceDiff, v interface{}) error {
if err := helper.MSSQLElasticPoolValidateSKU(diff); err != nil {
return err
}

return nil
}),
return nil
},

pluginsdk.ForceNewIfChange("enclave_type", func(ctx context.Context, old, new, _ interface{}) bool {
// enclave_type cannot be removed once it has been set
// but can be changed between VBS and Default...
// this Diff will not work until 4.0 when we remove
// the computed property from the field scheam.
if old.(string) != "" && new.(string) == "" {
return true
}

return false
}),
),
}
}

Expand Down Expand Up @@ -236,13 +252,6 @@ func resourceMsSqlElasticPoolCreateUpdate(d *pluginsdk.ResourceData, meta interf
location := azure.NormalizeLocation(d.Get("location").(string))
sku := expandMsSqlElasticPoolSku(d)

// NOTE: Set the default value, if the field exists in the config the only value
// that it could be is 'VBS'...
enclaveType := elasticpools.AlwaysEncryptedEnclaveTypeDefault
if v, ok := d.GetOk("enclave_type"); ok {
enclaveType = elasticpools.AlwaysEncryptedEnclaveType(v.(string))
}

maintenanceConfigId := publicmaintenanceconfigurations.NewPublicMaintenanceConfigurationID(subscriptionId, d.Get("maintenance_configuration_name").(string))
elasticPool := elasticpools.ElasticPool{
Name: pointer.To(id.ElasticPoolName),
Expand All @@ -252,12 +261,17 @@ func resourceMsSqlElasticPoolCreateUpdate(d *pluginsdk.ResourceData, meta interf
Properties: &elasticpools.ElasticPoolProperties{
LicenseType: pointer.To(elasticpools.ElasticPoolLicenseType(d.Get("license_type").(string))),
PerDatabaseSettings: expandMsSqlElasticPoolPerDatabaseSettings(d),
PreferredEnclaveType: pointer.To(enclaveType),
ZoneRedundant: pointer.To(d.Get("zone_redundant").(bool)),
MaintenanceConfigurationId: pointer.To(maintenanceConfigId.ID()),
PreferredEnclaveType: nil,
},
}

// NOTE: The service default is actually nil/empty which indicates enclave is disabled. the value `Default` is NOT the default.
if v, ok := d.GetOk("enclave_type"); ok && v.(string) != "" {
elasticPool.Properties.PreferredEnclaveType = pointer.To(elasticpools.AlwaysEncryptedEnclaveType(v.(string)))
}

if d.HasChange("max_size_gb") {
if v, ok := d.GetOk("max_size_gb"); ok {
maxSizeBytes := v.(float64) * 1073741824
Expand Down Expand Up @@ -308,9 +322,7 @@ func resourceMsSqlElasticPoolRead(d *pluginsdk.ResourceData, meta interface{}) e

if props := model.Properties; props != nil {
enclaveType := ""

// NOTE: Always set the PreferredEnclaveType to an empty string unless it isn't 'Default'...
if v := props.PreferredEnclaveType; v != nil && pointer.From(v) != elasticpools.AlwaysEncryptedEnclaveTypeDefault {
if v := props.PreferredEnclaveType; v != nil {
enclaveType = string(pointer.From(v))
}
d.Set("enclave_type", enclaveType)
Expand Down
13 changes: 11 additions & 2 deletions internal/services/mssql/mssql_elasticpool_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ func TestAccMsSqlElasticPool_vCoreToStandardDTU(t *testing.T) {
}

func TestAccMsSqlElasticPool_enclaveTypeUpdate(t *testing.T) {
// NOTE: Once the enclave_type has be set it cannot be removed...
data := acceptance.BuildTestData(t, "azurerm_mssql_elasticpool", "test")
r := MsSqlElasticPoolResource{}

Expand All @@ -390,10 +391,18 @@ func TestAccMsSqlElasticPool_enclaveTypeUpdate(t *testing.T) {
},
data.ImportStep("max_size_gb"),
{
Config: r.basicDTU(data, ""),
Config: r.basicDTU(data, `enclave_type = "Default"`),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("enclave_type").IsEmpty(),
check.That(data.ResourceName).Key("enclave_type").HasValue("Default"),
),
},
data.ImportStep("max_size_gb"),
{
Config: r.basicDTU(data, `enclave_type = "VBS"`),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("enclave_type").HasValue("VBS"),
),
},
data.ImportStep("max_size_gb"),
Expand Down
10 changes: 6 additions & 4 deletions website/docs/r/mssql_database.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,13 @@ The following arguments are supported:

* `elastic_pool_id` - (Optional) Specifies the ID of the elastic pool containing this database.

* `enclave_type` - (Optional) Specifies the type of enclave to be used by the database. Possible value `VBS`.
* `enclave_type` - (Optional) Specifies the type of enclave to be used by the elastic pool. When `enclave_type` is not specified (e.g., the default) enclaves are not enabled on the database. <!-- TODO: Uncomment in 4.0: Once enabled (e.g., by specifying `Default` or `VBS`) removing the `enclave_type` field from the configuration file will force the creation of a new resource.--> Possible values are `Default` or `VBS`.

~> **NOTE:** `enclave_type` is currently not supported for DW (e.g, DataWarehouse) and DC-series SKUs.
-> **NOTE:** `enclave_type` is currently not supported for DW (e.g, DataWarehouse) and DC-series SKUs.

~> **NOTE:** Geo Replicated and Failover databases must have the same `enclave_type`.
-> **NOTE:** Geo Replicated and Failover databases must have the same `enclave_type`.

~> **NOTE:** The default value for the `enclave_type` field is unset not `Default`.

* `geo_backup_enabled` - (Optional) A boolean that specifies if the Geo Backup Policy is enabled. Defaults to `true`.

Expand Down Expand Up @@ -236,7 +238,7 @@ The following arguments are supported:

* `sku_name` - (Optional) Specifies the name of the SKU used by the database. For example, `GP_S_Gen5_2`,`HS_Gen4_1`,`BC_Gen5_2`, `ElasticPool`, `Basic`,`S0`, `P2` ,`DW100c`, `DS100`. Changing this from the HyperScale service tier to another service tier will create a new resource.

~> **NOTE:** The default `sku_name` value may differ between Azure locations depending on local availability of Gen4/Gen5 capacity. When databases are replicated using the `creation_source_database_id` property, the source (primary) database cannot have a higher SKU service tier than any secondary databases. When changing the `sku_name` of a database having one or more secondary databases, this resource will first update any secondary databases as necessary. In such cases it's recommended to use the same `sku_name` in your configuration for all related databases, as not doing so may cause an unresolvable diff during subsequent plans.
-> **NOTE:** The default `sku_name` value may differ between Azure locations depending on local availability of Gen4/Gen5 capacity. When databases are replicated using the `creation_source_database_id` property, the source (primary) database cannot have a higher SKU service tier than any secondary databases. When changing the `sku_name` of a database having one or more secondary databases, this resource will first update any secondary databases as necessary. In such cases it's recommended to use the same `sku_name` in your configuration for all related databases, as not doing so may cause an unresolvable diff during subsequent plans.

* `storage_account_type` - (Optional) Specifies the storage account type used to store backups for this database. Possible values are `Geo`, `GeoZone`, `Local` and `Zone`. Defaults to `Geo`.

Expand Down
Loading
Loading