From e961bb40837f3c2172f9f5bab60b7e9f40cd0d45 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 13 Mar 2025 11:11:57 -0500 Subject: [PATCH] Framework SQL User resource draft --- .../terraform/acctest/vcr_utils.go | 11 + mmv1/third_party/terraform/go.mod | 1 + mmv1/third_party/terraform/go.sum | 2 + .../services/sql/fw_resource_sql_user.go | 507 ++++++++++++++++++ .../services/sql/fw_resource_sql_user_test.go | 90 ++++ 5 files changed, 611 insertions(+) create mode 100644 mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go create mode 100644 mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index e4fd00c15339..078d78b95110 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/fwprovider" tpgprovider "github.com/hashicorp/terraform-provider-google/google/provider" "github.com/hashicorp/terraform-provider-google/google/services/compute" + "github.com/hashicorp/terraform-provider-google/google/services/sql" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" @@ -31,6 +32,8 @@ import ( "github.com/dnaeon/go-vcr/recorder" "github.com/hashicorp/terraform-plugin-framework/datasource" + fwResource "github.com/hashicorp/terraform-plugin-framework/resource" + fwDiags "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -407,6 +410,14 @@ func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() dataso return ds } +// Resources overrides the provider's Resources function so that we can append test-specific resources +// Similar to the Datasources override +func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { + rs := p.FrameworkProvider.Resources(ctx) + rs = append(rs, sql.NewSQLUserFWResource) // google_fw_sql_user + return rs +} + // GetSDKProvider gets the SDK provider for use in acceptance tests // If VCR is in use, the configure function is overwritten. // See usage in MuxedProviders diff --git a/mmv1/third_party/terraform/go.mod b/mmv1/third_party/terraform/go.mod index 165b15a4bf83..69f7b361fc01 100644 --- a/mmv1/third_party/terraform/go.mod +++ b/mmv1/third_party/terraform/go.mod @@ -79,6 +79,7 @@ require ( github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.22.0 // indirect + github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/mmv1/third_party/terraform/go.sum b/mmv1/third_party/terraform/go.sum index 387e9f17ac72..0b2e96edc871 100644 --- a/mmv1/third_party/terraform/go.sum +++ b/mmv1/third_party/terraform/go.sum @@ -177,6 +177,8 @@ github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4 github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 h1:I/N0g/eLZ1ZkLZXUQ0oRSXa8YG/EF0CEuQP1wXdrzKw= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0/go.mod h1:t339KhmxnaF4SzdpxmqW8HnQBHVGYazwtfxU0qCs4eE= github.com/hashicorp/terraform-plugin-framework-validators v0.9.0 h1:LYz4bXh3t7bTEydXOmPDPupRRnA480B/9+jV8yZvxBA= github.com/hashicorp/terraform-plugin-framework-validators v0.9.0/go.mod h1:+BVERsnfdlhYR2YkXMBtPnmn9UsL19U3qUtSZ+Y/5MY= github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= diff --git a/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go new file mode 100644 index 000000000000..5e536900be96 --- /dev/null +++ b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go @@ -0,0 +1,507 @@ +package sql + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + "github.com/hashicorp/terraform-provider-google/google/transport" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +var ( + _ resource.Resource = &SQLUserFWResource{} + _ resource.ResourceWithConfigure = &SQLUserFWResource{} +) + +func NewSQLUserFWResource() resource.Resource { + return &SQLUserFWResource{} +} + +type SQLUserFWResource struct { + client *sqladmin.Service + providerConfig *transport_tpg.Config +} + +type SQLUserModel struct { + Id types.String `tfsdk:"id"` + Project types.String `tfsdk:"project"` + Name types.String `tfsdk:"name"` + Host types.String `tfsdk:"host"` + Instance types.String `tfsdk:"instance"` + Password types.String `tfsdk:"password"` + // PasswordWO types.String `tfsdk:"password_wo"` + // PasswordWOVersion types.String `tfsdk:"password_wo_version"` + Type types.String `tfsdk:"type"` + // SqlServerUserDetails types.List `tfsdk:"sql_server_user_details"` + // PasswordPolicy types.List `tfsdk:"password_policy"` + // DeletionPolicy types.String `tfsdk:"deletion_policy"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// Metadata returns the resource type name. +func (d *SQLUserFWResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_fw_sql_user" +} + +func (r *SQLUserFWResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*transport_tpg.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = p.NewSqlAdminClient(p.UserAgent) + if resp.Diagnostics.HasError() { + return + } + r.providerConfig = p +} + +func (d *SQLUserFWResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A resource to represent a SQL User object.", + + Attributes: map[string]schema.Attribute{ + "project": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "host": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "instance": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: `The name of the user. Changing this forces a new resource to be created.`, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + SQLUserNameIAMPlanModifier(), + }, + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "type": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + // TODO DiffSuppressFunc: tpgresource.EmptyOrDefaultStringSuppress("BUILT_IN"), + }, + }, + // This is included for backwards compatibility with the original, SDK-implemented resource. + "id": schema.StringAttribute{ + Description: "Project identifier", + MarkdownDescription: "Project identifier", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + }), + }, + } +} + +func (r *SQLUserFWResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project := fwresource.GetProjectFramework(data.Project, types.StringValue(r.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + nameData, diags := data.Name.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + instanceData, diags := data.Instance.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + hostData, diags := data.Host.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + typeData, diags := data.Type.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + passwordData, diags := data.Password.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + createTimeout, diags := data.Timeouts.Create(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user := &sqladmin.User{ + Name: nameData.ValueString(), + Instance: instanceData.ValueString(), + Password: passwordData.ValueString(), + Host: hostData.ValueString(), + Type: typeData.ValueString(), + } + + transport_tpg.MutexStore.Lock(instanceMutexKey(project.ValueString(), instanceData.ValueString())) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project.ValueString(), instanceData.ValueString())) + + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + // TODO host check logic + + var op *sqladmin.Operation + var err error + insertFunc := func() error { + op, err = r.client.Users.Insert(project.ValueString(), instanceData.ValueString(), + user).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: insertFunc, + Timeout: createTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failed to insert "+ + "user %s into instance %s", nameData.ValueString(), instanceData.ValueString()), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project.ValueString(), "Insert User", r.client.UserAgent, createTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failure waiting to insert "+ + "user %s into instance %s", nameData.ValueString(), instanceData.ValueString()), err.Error()) + return + } + + tflog.Trace(ctx, "created sql user resource") + + // This will include a double-slash (//) for postgres instances, + // for which user.Host is an empty string. That's okay. + data.Id = types.StringValue(fmt.Sprintf("%s/%s/%s", user.Name, user.Host, user.Instance)) + data.Project = project + + // read back sql user + r.SQLUserRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SQLUserFWResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + tflog.Trace(ctx, "read sql user resource") + + // read back sql user + r.SQLUserRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SQLUserFWResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var old, new SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + resp.Diagnostics.Append(req.State.Get(ctx, &old)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.Plan.Get(ctx, &new)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + if !old.Password.Equal(new.Password) { + project := new.Project.ValueString() + instance := new.Instance.ValueString() + name := new.Name.ValueString() + host := new.Host.ValueString() + password := new.Password.ValueString() + + updateTimeout, diags := new.Timeouts.Update(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user := &sqladmin.User{ + Name: name, + Instance: instance, + Password: password, + } + transport_tpg.MutexStore.Lock(instanceMutexKey(project, instance)) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project, instance)) + var op *sqladmin.Operation + var err error + updateFunc := func() error { + op, err = r.client.Users.Update(project, instance, user).Host(host).Name(name).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: updateFunc, + Timeout: updateTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failed to update"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project, "Update User", r.client.UserAgent, updateTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failure waiting for update"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + // read back sql user + r.SQLUserRefresh(ctx, &new, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &new)...) +} + +func (r *SQLUserFWResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data SQLUserModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project := data.Project.ValueString() + instance := data.Instance.ValueString() + name := data.Name.ValueString() + host := data.Host.ValueString() + + deleteTimeout, diags := data.Timeouts.Delete(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + transport_tpg.MutexStore.Lock(instanceMutexKey(project, instance)) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project, instance)) + var op *sqladmin.Operation + var err error + deleteFunc := func() error { + op, err = r.client.Users.Delete(project, instance).Host(host).Name(name).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: deleteFunc, + Timeout: deleteTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failed to delete"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project, "Delete User", r.client.UserAgent, deleteTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failure waiting to delete "+ + "user %s", name), err.Error()) + return + } +} + +func (r *SQLUserFWResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + + // TODO recreate all import cases + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: project/instance/host/name. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("host"), idParts[2])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[3])...) +} + +func (r *SQLUserFWResource) SQLUserRefresh(ctx context.Context, data *SQLUserModel, state *tfsdk.State, diag *diag.Diagnostics) { + userReadResp, err := r.client.Users.Get(data.Project.ValueString(), data.Instance.ValueString(), data.Name.ValueString()).Host(data.Host.ValueString()).Do() + if err != nil { + // Treat HTTP 404 Not Found status as a signal to recreate resource + // and return early + if userReadResp != nil && transport.IsGoogleApiErrorWithCode(err, userReadResp.HTTPStatusCode) { + tflog.Trace(ctx, "sql user resource not found, removing from state") + state.RemoveResource(ctx) + return + } + diag.AddError(fmt.Sprintf("Error, failure waiting to read "+ + "user %s", data.Name.ValueString()), err.Error()) + return + } + + id := fmt.Sprintf("projects/%s/global/networks/%s", userReadResp.Project, userReadResp.Name) + data.Id = types.StringValue(id) + data.Project = types.StringValue(userReadResp.Project) + data.Instance = types.StringValue(userReadResp.Instance) + if userReadResp.Host != "" { + data.Host = types.StringValue(userReadResp.Host) + } + if userReadResp.Type != "" { + data.Type = types.StringValue(userReadResp.Type) + } +} + +// Plan Modifiers +func SQLUserNameIAMPlanModifier() planmodifier.String { + return &sqlUserNameIAMPlanModifier{} +} + +type sqlUserNameIAMPlanModifier struct { +} + +func (d *sqlUserNameIAMPlanModifier) Description(ctx context.Context) string { + return "Suppresses name diffs for IAM user types." +} +func (d *sqlUserNameIAMPlanModifier) MarkdownDescription(ctx context.Context) string { + return d.Description(ctx) +} + +// Plan modifier to emulate the SDK diffSuppressIamUserName +func (d *sqlUserNameIAMPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Retrieve relevant fields + var oldName types.String + diags := req.State.GetAttribute(ctx, path.Root("name"), &oldName) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var newName types.String + diags = req.Plan.GetAttribute(ctx, path.Root("name"), &newName) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var userType types.String + diags = req.Plan.GetAttribute(ctx, path.Root("type"), &userType) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Old diff suppress logic + strippedNewName := strings.Split(newName.ValueString(), "@")[0] + + if oldName.ValueString() == strippedNewName && strings.Contains(userType.ValueString(), "IAM") { + // Suppress the diff by setting the planned value to the old value + resp.PlanValue = oldName + } +} diff --git a/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go new file mode 100644 index 000000000000..80e78c4a0316 --- /dev/null +++ b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go @@ -0,0 +1,90 @@ +package sql_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccSqlUserFW_mysql(t *testing.T) { + // Multiple fine-grained resources + acctest.SkipIfVcr(t) + t.Parallel() + + instance := fmt.Sprintf("tf-test-%d", acctest.RandInt(t)) + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlUserDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlUserFW_mysql(instance, "password"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user2"), + ), + }, + { + // Update password + Config: testGoogleSqlUserFW_mysql(instance, "new_password"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user2"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user3"), + ), + }, + { + ResourceName: "google_fw_sql_user.user2", + ImportStateId: fmt.Sprintf("%s/%s/gmail.com/admin", envvar.GetTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + { + ResourceName: "google_fw_sql_user.user3", + ImportStateId: fmt.Sprintf("%s/%s/10.0.0.0/24/admin", envvar.GetTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + +func testGoogleSqlUserFW_mysql(instance, password string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + settings { + tier = "db-f1-micro" + } +} + +resource "google_fw_sql_user" "user1" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "google.com" + password = "%s" +} + +resource "google_fw_sql_user" "user2" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "gmail.com" + password = "hunter2" +} + +resource "google_fw_sql_user" "user3" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "10.0.0.0/24" + password = "hunter3" +} +`, instance, password) +}