diff --git a/internal/services/mssql/mssql_server_resource.go b/internal/services/mssql/mssql_server_resource.go index c3044d8022c0..6e61641be330 100644 --- a/internal/services/mssql/mssql_server_resource.go +++ b/internal/services/mssql/mssql_server_resource.go @@ -22,6 +22,9 @@ import ( "github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/serverconnectionpolicies" "github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/servers" "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + schemaValidation "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" @@ -34,7 +37,6 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" - "github.com/hashicorp/terraform-provider-azurerm/utils" ) func resourceMsSqlServer() *pluginsdk.Resource { @@ -49,6 +51,10 @@ func resourceMsSqlServer() *pluginsdk.Resource { return err }), + ValidateRawResourceConfigFuncs: []schema.ValidateRawResourceConfigFunc{ + schemaValidation.PreferWriteOnlyAttribute(cty.GetAttrPath("administrator_login_password"), cty.GetAttrPath("administrator_login_password_wo")), + }, + Timeouts: &pluginsdk.ResourceTimeout{ Create: pluginsdk.DefaultTimeout(60 * time.Minute), Read: pluginsdk.DefaultTimeout(5 * time.Minute), @@ -84,15 +90,30 @@ func resourceMsSqlServer() *pluginsdk.Resource { Computed: true, ForceNew: true, AtLeastOneOf: []string{"administrator_login", "azuread_administrator.0.azuread_authentication_only"}, - RequiredWith: []string{"administrator_login", "administrator_login_password"}, }, "administrator_login_password": { - Type: pluginsdk.TypeString, + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + AtLeastOneOf: []string{"administrator_login_password", "administrator_login_password_wo", "azuread_administrator.0.azuread_authentication_only"}, + ConflictsWith: []string{"administrator_login_password_wo"}, + }, + + "administrator_login_password_wo": { + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + WriteOnly: true, + AtLeastOneOf: []string{"administrator_login_password_wo", "administrator_login_password", "azuread_administrator.0.azuread_authentication_only"}, + ConflictsWith: []string{"administrator_login_password"}, + RequiredWith: []string{"administrator_login_password_wo_version"}, + }, + + "administrator_login_password_wo_version": { + Type: pluginsdk.TypeInt, Optional: true, - Sensitive: true, - AtLeastOneOf: []string{"administrator_login_password", "azuread_administrator.0.azuread_authentication_only"}, - RequiredWith: []string{"administrator_login", "administrator_login_password"}, + RequiredWith: []string{"administrator_login_password_wo"}, }, "azuread_administrator": { @@ -197,6 +218,8 @@ func resourceMsSqlServer() *pluginsdk.Resource { pluginsdk.CustomizeDiffShim(msSqlMinimumTLSVersionDiff), pluginsdk.CustomizeDiffShim(msSqlPasswordChangeWhenAADAuthOnly), + + pluginsdk.CustomizeDiffShim(msSqlAdministratorLoginPassword), ), } @@ -240,6 +263,11 @@ func resourceMsSqlServerCreate(d *pluginsdk.ResourceData, meta interface{}) erro return tf.ImportAsExistsError("azurerm_mssql_server", id.ID()) } + woAdminLoginPassword, err := pluginsdk.GetWriteOnly(d, "administrator_login_password_wo", cty.String) + if err != nil { + return err + } + props := servers.Server{ Location: location, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), @@ -251,11 +279,15 @@ func resourceMsSqlServerCreate(d *pluginsdk.ResourceData, meta interface{}) erro } if v := d.Get("administrator_login"); v.(string) != "" { - props.Properties.AdministratorLogin = utils.String(v.(string)) + props.Properties.AdministratorLogin = pointer.To(v.(string)) } if v := d.Get("administrator_login_password"); v.(string) != "" { - props.Properties.AdministratorLoginPassword = utils.String(v.(string)) + props.Properties.AdministratorLoginPassword = pointer.To(v.(string)) + } + + if !woAdminLoginPassword.IsNull() { + props.Properties.AdministratorLoginPassword = pointer.To(woAdminLoginPassword.AsString()) } // NOTE: You must set the admin before setting the values of the admin... @@ -389,6 +421,16 @@ func resourceMsSqlServerUpdate(d *pluginsdk.ResourceData, meta interface{}) erro payload.Properties.AdministratorLoginPassword = pointer.To(adminPassword) } + if d.HasChange("administrator_login_password_wo_version") { + woAdminLoginPassword, err := pluginsdk.GetWriteOnly(d, "administrator_login_password_wo", cty.String) + if err != nil { + return err + } + if !woAdminLoginPassword.IsNull() { + payload.Properties.AdministratorLoginPassword = pointer.To(woAdminLoginPassword.AsString()) + } + } + if d.HasChange("minimum_tls_version") { payload.Properties.MinimalTlsVersion = pointer.To(servers.MinimalTlsVersion(d.Get("minimum_tls_version").(string))) } @@ -524,6 +566,7 @@ func resourceMsSqlServerRead(d *pluginsdk.ResourceData, meta interface{}) error if props := model.Properties; props != nil { d.Set("version", props.Version) d.Set("administrator_login", props.AdministratorLogin) + d.Set("administrator_login_password_wo_version", d.Get("administrator_login_password_wo_version").(int)) d.Set("fully_qualified_domain_name", props.FullyQualifiedDomainName) // todo remove `|| *v == "None"` when https://github.com/Azure/azure-rest-api-specs/issues/24348 is addressed @@ -721,3 +764,21 @@ func msSqlPasswordChangeWhenAADAuthOnly(ctx context.Context, d *pluginsdk.Resour } return } + +// msSqlAdministratorLoginPassword checks to make sure that one of `administrator_login_password_wo` or `administrator_login_password` is set when `administrator_login` is specified. +func msSqlAdministratorLoginPassword(ctx context.Context, d *pluginsdk.ResourceDiff, _ interface{}) (err error) { + adminLogin := d.GetRawConfig().AsValueMap()["administrator_login"] + if !adminLogin.IsNull() && adminLogin.AsString() != "" { + woAdminLoginPassword, err := pluginsdk.GetWriteOnlyFromDiff(d, "administrator_login_password_wo", cty.String) + if err != nil { + return err + } + + password := d.GetRawConfig().AsValueMap()["administrator_login_password"] + + if woAdminLoginPassword.IsNull() && password.IsNull() { + return fmt.Errorf("expected `administrator_login_password` or `administrator_login_password_wo` to be set when `administrator_login` is specified") + } + } + return +} diff --git a/internal/services/mssql/mssql_server_resource_test.go b/internal/services/mssql/mssql_server_resource_test.go index 4d0a1bd38b91..c820f573c5c1 100644 --- a/internal/services/mssql/mssql_server_resource_test.go +++ b/internal/services/mssql/mssql_server_resource_test.go @@ -8,16 +8,20 @@ import ( "fmt" "testing" + "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/sql/2023-08-01-preview/servers" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" "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/features" + "github.com/hashicorp/terraform-provider-azurerm/internal/provider/framework" "github.com/hashicorp/terraform-provider-azurerm/internal/services/mssql/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azurerm/utils" ) type MsSqlServerResource struct{} @@ -305,6 +309,59 @@ func TestAccMsSqlServer_CMKServerTagsUpdate(t *testing.T) { }) } +func TestAccMsSqlServer_writeOnlyAdminLoginPassword(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_server", "test") + r := MsSqlServerResource{} + + resource.ParallelTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: framework.ProtoV5ProviderFactoriesInit(context.Background(), "azurerm"), + Steps: []resource.TestStep{ + { + Config: r.writeOnlyAdminLoginPassword(data, "7h1515K4711-secret", 1), + Check: check.That(data.ResourceName).ExistsInAzure(r), + }, + data.ImportStep("administrator_login_password_wo_version"), + { + Config: r.writeOnlyAdminLoginPassword(data, "7h1515K4711-updated", 2), + Check: check.That(data.ResourceName).ExistsInAzure(r), + }, + data.ImportStep("administrator_login_password_wo_version"), + }, + }) +} + +func TestAccMsSqlServer_updateToWriteOnlyPassword(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_mssql_server", "test") + r := MsSqlServerResource{} + + resource.ParallelTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: framework.ProtoV5ProviderFactoriesInit(context.Background(), "azurerm"), + Steps: []resource.TestStep{ + { + Config: r.basic(data), + Check: check.That(data.ResourceName).ExistsInAzure(r), + }, + data.ImportStep("administrator_login_password"), + { + Config: r.writeOnlyAdminLoginPassword(data, "7h1515K4711-secret", 1), + Check: check.That(data.ResourceName).ExistsInAzure(r), + }, + data.ImportStep("administrator_login_password", "administrator_login_password_wo_version"), + { + Config: r.basic(data), + Check: check.That(data.ResourceName).ExistsInAzure(r), + }, + data.ImportStep("administrator_login_password"), + }, + }) +} + func (MsSqlServerResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.ServerID(state.ID) if err != nil { @@ -321,7 +378,7 @@ func (MsSqlServerResource) Exists(ctx context.Context, client *clients.Client, s return nil, fmt.Errorf("reading SQL Server %q (Resource Group %q): %v", id.Name, id.ResourceGroup, err) } - return utils.Bool(resp.Model != nil), nil + return pointer.To(resp.Model != nil), nil } func (MsSqlServerResource) basic(data acceptance.TestData) string { @@ -1059,3 +1116,30 @@ resource "azurerm_key_vault_key" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomString) } + +func (MsSqlServerResource) writeOnlyAdminLoginPassword(data acceptance.TestData, secret string, version int) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-mssql-%[1]d" + location = "%[2]s" +} + +%s + +resource "azurerm_mssql_server" "test" { + name = "acctestsqlserver%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + version = "12.0" + administrator_login = "missadministrator" + administrator_login_password_wo_version = %[4]d + administrator_login_password_wo = ephemeral.azurerm_key_vault_secret.test.value + + outbound_network_restriction_enabled = true +} +`, data.RandomInteger, data.Locations.Primary, acceptance.WriteOnlyKeyVaultSecretTemplate(data, secret), version) +} diff --git a/internal/tf/pluginsdk/write_only.go b/internal/tf/pluginsdk/write_only.go index 49aff6215c96..d4bc2800f7da 100644 --- a/internal/tf/pluginsdk/write_only.go +++ b/internal/tf/pluginsdk/write_only.go @@ -22,3 +22,16 @@ func GetWriteOnly(d *ResourceData, name string, attributeType cty.Type) (*cty.Va } return pointer.To(value), nil } + +// GetWriteOnlyFromDiff gets a write only attribute from the diff, checking that it is of an expected type and subsequently returns it +func GetWriteOnlyFromDiff(d *ResourceDiff, name string, attributeType cty.Type) (*cty.Value, error) { + value, diags := d.GetRawConfigAt(cty.GetAttrPath(name)) + if diags.HasError() { + return nil, fmt.Errorf("retrieving write-only attribute `%s`: %+v", name, diags) + } + + if !value.Type().Equals(attributeType) { + return nil, fmt.Errorf("retrieving write-only attribute `%s`: value is not of type %v", name, attributeType) + } + return pointer.To(value), nil +} diff --git a/website/docs/r/mssql_server.html.markdown b/website/docs/r/mssql_server.html.markdown index 1fcfad3a0550..d6f9a269bfa7 100644 --- a/website/docs/r/mssql_server.html.markdown +++ b/website/docs/r/mssql_server.html.markdown @@ -137,7 +137,13 @@ The following arguments are supported: * `administrator_login` - (Optional) The administrator login name for the new server. Required unless `azuread_authentication_only` in the `azuread_administrator` block is `true`. When omitted, Azure will generate a default username which cannot be subsequently changed. Changing this forces a new resource to be created. -* `administrator_login_password` - (Optional) The password associated with the `administrator_login` user. Needs to comply with Azure's [Password Policy](https://msdn.microsoft.com/library/ms161959.aspx). Required unless `azuread_authentication_only` in the `azuread_administrator` block is `true`. +* `administrator_login_password` - (Optional) The password associated with the `administrator_login` user. Needs to comply with Azure's [Password Policy](https://msdn.microsoft.com/library/ms161959.aspx). + +* `administrator_login_password_wo` - (Optional, Write-Only) The Password associated with the `administrator_login` user. Needs to comply with Azure's [Password Policy](https://msdn.microsoft.com/library/ms161959.aspx). + +~> **Note:** Either `administrator_login_password` or `administrator_login_password_wo` is required unless `azuread_authentication_only` in the `azuread_administrator` block is `true`. + +* `administrator_login_password_wo_version` - (Optional) An integer value used to trigger an update for `administrator_login_password_wo`. This property should be incremented when updating `administrator_login_password_wo`. * `azuread_administrator` - (Optional) An `azuread_administrator` block as defined below.