diff --git a/.changelog/4231.txt b/.changelog/4231.txt new file mode 100644 index 0000000000..fe1bc95fd2 --- /dev/null +++ b/.changelog/4231.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +sql: added `deletion_policy` field to `google_sql_user` to enable abandoning users rather than deleting them +``` diff --git a/google-beta/resource_sql_user.go b/google-beta/resource_sql_user.go index 3e73ed17c3..f336872f43 100644 --- a/google-beta/resource_sql_user.go +++ b/google-beta/resource_sql_user.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" sqladmin "google.golang.org/api/sqladmin/v1beta4" ) @@ -65,6 +66,15 @@ func resourceSqlUser() *schema.Resource { ForceNew: true, Description: `The ID of the project in which the resource belongs. If it is not provided, the provider project is used.`, }, + + "deletion_policy": { + Type: schema.TypeString, + Optional: true, + Description: `The deletion policy for the user. Setting ABANDON allows the resource + to be abandoned rather than deleted. This is useful for Postgres, where users cannot be deleted from the API if they + have been granted SQL roles. Possible values are: "ABANDON".`, + ValidateFunc: validation.StringInSlice([]string{"ABANDON", ""}, false), + }, }, } } @@ -236,6 +246,13 @@ func resourceSqlUserUpdate(d *schema.ResourceData, meta interface{}) error { func resourceSqlUserDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) + + if deletionPolicy := d.Get("deletion_policy"); deletionPolicy == "ABANDON" { + // Allows for user to be abandoned without deletion to avoid deletion failing + // for Postgres users in some circumstances due to existing SQL roles + return nil + } + userAgent, err := generateUserAgentString(d, config.userAgent) if err != nil { return err diff --git a/google-beta/resource_sql_user_test.go b/google-beta/resource_sql_user_test.go index 641f3048fe..35cb14e912 100644 --- a/google-beta/resource_sql_user_test.go +++ b/google-beta/resource_sql_user_test.go @@ -78,6 +78,40 @@ func TestAccSqlUser_postgres(t *testing.T) { }) } +func TestAccSqlUser_postgresAbandon(t *testing.T) { + t.Parallel() + + instance := fmt.Sprintf("i-%d", randInt(t)) + userName := "admin" + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlUserDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlUser_postgresAbandon(instance, userName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_sql_user.user"), + ), + }, + { + ResourceName: "google_sql_user.user", + ImportStateId: fmt.Sprintf("%s/%s/admin", getTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password", "deletion_policy"}, + }, + { + // Abandon user + Config: testGoogleSqlUser_postgresNoUser(instance), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExistsWithName(t, instance, userName), + ), + }, + }, + }) +} + func testAccCheckGoogleSqlUserExists(t *testing.T, n string) resource.TestCheckFunc { return func(s *terraform.State) error { config := googleProviderConfig(t) @@ -106,6 +140,27 @@ func testAccCheckGoogleSqlUserExists(t *testing.T, n string) resource.TestCheckF } } +func testAccCheckGoogleSqlUserExistsWithName(t *testing.T, instance, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := googleProviderConfig(t) + + users, err := config.NewSqlAdminClient(config.userAgent).Users.List(config.Project, + instance).Do() + + if err != nil { + return err + } + + for _, user := range users.Items { + if user.Name == name { + return nil + } + } + + return fmt.Errorf("Not found: User: %s in instance: %s: %s", name, instance, err) + } +} + func testAccSqlUserDestroyProducer(t *testing.T) func(s *terraform.State) error { return func(s *terraform.State) error { for _, rs := range s.RootModule().Resources { @@ -180,3 +235,40 @@ resource "google_sql_user" "user" { } `, instance, password) } + +func testGoogleSqlUser_postgresAbandon(instance, name string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "POSTGRES_9_6" + deletion_protection = false + + settings { + tier = "db-f1-micro" + } +} + +resource "google_sql_user" "user" { + name = "%s" + instance = google_sql_database_instance.instance.name + password = "password" + deletion_policy = "ABANDON" +} +`, instance, name) +} + +func testGoogleSqlUser_postgresNoUser(instance string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "POSTGRES_9_6" + deletion_protection = false + + settings { + tier = "db-f1-micro" + } +} +`, instance) +} diff --git a/website/docs/r/sql_user.html.markdown b/website/docs/r/sql_user.html.markdown index 2aa161fada..234b584693 100644 --- a/website/docs/r/sql_user.html.markdown +++ b/website/docs/r/sql_user.html.markdown @@ -53,6 +53,12 @@ The following arguments are supported: * `password` - (Optional) The password for the user. Can be updated. For Postgres instances this is a Required field. +* `deletion_policy` - (Optional) The deletion policy for the user. + Setting `ABANDON` allows the resource to be abandoned rather than deleted. This is useful + for Postgres, where users cannot be deleted from the API if they have been granted SQL roles. + + Possible values are: `ABANDON`. + - - - * `host` - (Optional) The host the user can connect from. This is only supported