diff --git a/postgresql/provider.go b/postgresql/provider.go index 2096576a..8479890b 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -201,6 +201,7 @@ func Provider() *schema.Provider { "postgresql_server": resourcePostgreSQLServer(), "postgresql_user_mapping": resourcePostgreSQLUserMapping(), "postgresql_alter_role": resourcePostgreSQLAlterRole(), + "postgresql_script": resourcePostgreSQLScript(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/postgresql/resource_postgresql_script.go b/postgresql/resource_postgresql_script.go new file mode 100644 index 00000000..bd4cad99 --- /dev/null +++ b/postgresql/resource_postgresql_script.go @@ -0,0 +1,120 @@ +package postgresql + +import ( + "crypto/sha1" + "encoding/hex" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + scriptCommandsAttr = "commands" + scriptTriesAttr = "tries" + scriptBackoffDelayAttr = "backoff_delay" + scriptShasumAttr = "shasum" +) + +func resourcePostgreSQLScript() *schema.Resource { + return &schema.Resource{ + Create: PGResourceFunc(resourcePostgreSQLScriptCreateOrUpdate), + Read: PGResourceFunc(resourcePostgreSQLScriptRead), + Update: PGResourceFunc(resourcePostgreSQLScriptCreateOrUpdate), + Delete: PGResourceFunc(resourcePostgreSQLScriptDelete), + + Schema: map[string]*schema.Schema{ + scriptCommandsAttr: { + Type: schema.TypeList, + Required: true, + Description: "List of SQL commands to execute", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + scriptTriesAttr: { + Type: schema.TypeInt, + Optional: true, + Default: 1, + Description: "Number of tries for a failing command", + }, + scriptBackoffDelayAttr: { + Type: schema.TypeInt, + Optional: true, + Default: 1, + Description: "Number of seconds between two tries of the batch of commands", + }, + scriptShasumAttr: { + Type: schema.TypeString, + Computed: true, + Description: "Shasum of commands", + }, + }, + } +} + +func resourcePostgreSQLScriptCreateOrUpdate(db *DBConnection, d *schema.ResourceData) error { + commands := d.Get(scriptCommandsAttr).([]any) + tries := d.Get(scriptTriesAttr).(int) + backoffDelay := d.Get(scriptBackoffDelayAttr).(int) + + sum := shasumCommands(commands) + + if err := executeCommands(db, commands, tries, backoffDelay); err != nil { + return err + } + + d.Set(scriptShasumAttr, sum) + d.SetId(sum) + return nil +} + +func resourcePostgreSQLScriptRead(db *DBConnection, d *schema.ResourceData) error { + commands := d.Get(scriptCommandsAttr).([]any) + newSum := shasumCommands(commands) + d.Set(scriptShasumAttr, newSum) + + return nil +} + +func resourcePostgreSQLScriptDelete(db *DBConnection, d *schema.ResourceData) error { + return nil +} + +func executeCommands(db *DBConnection, commands []any, tries int, backoffDelay int) error { + for try := 1; ; try++ { + err := executeBatch(db, commands) + if err == nil { + return nil + } else { + if try >= tries { + return err + } + time.Sleep(time.Duration(backoffDelay) * time.Second) + } + } +} + +func executeBatch(db *DBConnection, commands []any) error { + for _, command := range commands { + log.Printf("[ERROR] Executing %s", command.(string)) + _, err := db.Query(command.(string)) + + if err != nil { + log.Println("[ERROR] Error catched:", err) + if _, rollbackError := db.Query("ROLLBACK"); rollbackError != nil { + log.Println("[ERROR] Rollback raised an error:", rollbackError) + } + return err + } + } + return nil +} + +func shasumCommands(commands []any) string { + sha := sha1.New() + for _, command := range commands { + sha.Write([]byte(command.(string))) + } + return hex.EncodeToString(sha.Sum(nil)) +} diff --git a/postgresql/resource_postgresql_script_test.go b/postgresql/resource_postgresql_script_test.go new file mode 100644 index 00000000..24660f0d --- /dev/null +++ b/postgresql/resource_postgresql_script_test.go @@ -0,0 +1,182 @@ +package postgresql + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccPostgresqlScript_basic(t *testing.T) { + config := ` + resource "postgresql_script" "test" { + commands = [ + "SELECT 1;" + ] + tries = 2 + backoff_delay = 4 + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 1;"), + resource.TestCheckResourceAttr("postgresql_script.test", "tries", "2"), + resource.TestCheckResourceAttr("postgresql_script.test", "backoff_delay", "4"), + ), + }, + }, + }) +} + +func TestAccPostgresqlScript_multiple(t *testing.T) { + config := ` + resource "postgresql_script" "test" { + commands = [ + "SELECT 1;", + "SELECT 2;", + "SELECT 3;" + ] + tries = 2 + backoff_delay = 4 + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 1;"), + resource.TestCheckResourceAttr("postgresql_script.test", "commands.1", "SELECT 2;"), + resource.TestCheckResourceAttr("postgresql_script.test", "commands.2", "SELECT 3;"), + resource.TestCheckResourceAttr("postgresql_script.test", "tries", "2"), + resource.TestCheckResourceAttr("postgresql_script.test", "backoff_delay", "4"), + ), + }, + }, + }) +} + +func TestAccPostgresqlScript_default(t *testing.T) { + config := ` + resource "postgresql_script" "test" { + commands = [ + "SELECT 1;" + ] + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 1;"), + resource.TestCheckResourceAttr("postgresql_script.test", "tries", "1"), + resource.TestCheckResourceAttr("postgresql_script.test", "backoff_delay", "1"), + ), + }, + }, + }) +} + +func TestAccPostgresqlScript_reapply(t *testing.T) { + config := ` + resource "postgresql_script" "test" { + commands = [ + "SELECT 1;" + ] + } + ` + + configChange := ` + resource "postgresql_script" "test" { + commands = [ + "SELECT 2;" + ] + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 1;"), + ), + }, + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 1;"), + ), + }, + { + Config: configChange, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_script.test", "commands.0", "SELECT 2;"), + ), + }, + }, + }) +} + +func TestAccPostgresqlScript_fail(t *testing.T) { + config := ` + resource "postgresql_script" "invalid" { + commands = [ + "SLC FROM nowhere;" + ] + tries = 2 + backoff_delay = 2 + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("syntax error"), + }, + }, + }) +} + +func TestAccPostgresqlScript_failMultiple(t *testing.T) { + config := ` + resource "postgresql_script" "invalid" { + commands = [ + "BEGIN", + "SLC FROM nowhere;", + "COMMIT" + ] + tries = 2 + backoff_delay = 2 + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("syntax error"), + }, + }, + }) +} diff --git a/website/docs/r/postgresql_script.html.markdown b/website/docs/r/postgresql_script.html.markdown new file mode 100644 index 00000000..6691bf74 --- /dev/null +++ b/website/docs/r/postgresql_script.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "postgresql" +page_title: "PostgreSQL: postgresql_script" +sidebar_current: "docs-postgresql-resource-postgresql_script" +description: |- + Execute a SQL script +--- + +# postgresql\_script + +The ``postgresql_script`` execute a script given as parameter. This script will be executed each time it changes. + +If one command of the batch fails, the provider will send a `ROLLBACK` command to the database, and retry, according to the tries / backoff_delay configuration. + +## Usage + +```hcl +resource "postgresql_script" "foo" { + commands = [ + "command 1", + "command 2" + ] + tries = 1 + backoff_delay = 1 +} +``` + +## Argument Reference + +* `commands` - (Required) An array of commands to execute, one by one. +* `tries` - (Optional) Number of tries of a command before raising an error. +* `backoff_delay` - (Optional) In case of failure, time in second to wait before a retry. + +## Examples + +Revoke default accesses for public schema: + +```hcl +resource "postgresql_script" "foo" { + commands = [ + "BEBIN", + "SELECT * FROM table", + "COMMIT" + ] + tries = 3 + backoff_delay = 1 +} +``` diff --git a/website/postgresql.erb b/website/postgresql.erb index 816b158b..e51e57ff 100644 --- a/website/postgresql.erb +++ b/website/postgresql.erb @@ -55,6 +55,9 @@ > postgresql_user_mapping + > + postgresql_script +