github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/pg/backend.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package pg 7 8 import ( 9 "context" 10 "database/sql" 11 "fmt" 12 "os" 13 "strconv" 14 15 "github.com/lib/pq" 16 "github.com/opentofu/opentofu/internal/backend" 17 "github.com/opentofu/opentofu/internal/encryption" 18 "github.com/opentofu/opentofu/internal/legacy/helper/schema" 19 ) 20 21 const ( 22 statesTableName = "states" 23 statesIndexName = "states_by_name" 24 ) 25 26 func defaultBoolFunc(k string, dv bool) schema.SchemaDefaultFunc { 27 return func() (interface{}, error) { 28 if v := os.Getenv(k); v != "" { 29 return strconv.ParseBool(v) 30 } 31 32 return dv, nil 33 } 34 } 35 36 // New creates a new backend for Postgres remote state. 37 func New(enc encryption.StateEncryption) backend.Backend { 38 s := &schema.Backend{ 39 Schema: map[string]*schema.Schema{ 40 "conn_str": { 41 Type: schema.TypeString, 42 Optional: true, 43 Description: "Postgres connection string; a `postgres://` URL", 44 DefaultFunc: schema.EnvDefaultFunc("PG_CONN_STR", nil), 45 }, 46 47 "schema_name": { 48 Type: schema.TypeString, 49 Optional: true, 50 Description: "Name of the automatically managed Postgres schema to store state", 51 DefaultFunc: schema.EnvDefaultFunc("PG_SCHEMA_NAME", "terraform_remote_state"), 52 }, 53 54 "skip_schema_creation": { 55 Type: schema.TypeBool, 56 Optional: true, 57 Description: "If set to `true`, OpenTofu won't try to create the Postgres schema", 58 DefaultFunc: defaultBoolFunc("PG_SKIP_SCHEMA_CREATION", false), 59 }, 60 61 "skip_table_creation": { 62 Type: schema.TypeBool, 63 Optional: true, 64 Description: "If set to `true`, OpenTofu won't try to create the Postgres table", 65 DefaultFunc: defaultBoolFunc("PG_SKIP_TABLE_CREATION", false), 66 }, 67 68 "skip_index_creation": { 69 Type: schema.TypeBool, 70 Optional: true, 71 Description: "If set to `true`, OpenTofu won't try to create the Postgres index", 72 DefaultFunc: defaultBoolFunc("PG_SKIP_INDEX_CREATION", false), 73 }, 74 }, 75 } 76 77 result := &Backend{Backend: s, encryption: enc} 78 result.Backend.ConfigureFunc = result.configure 79 return result 80 } 81 82 type Backend struct { 83 *schema.Backend 84 encryption encryption.StateEncryption 85 86 // The fields below are set from configure 87 db *sql.DB 88 configData *schema.ResourceData 89 connStr string 90 schemaName string 91 } 92 93 func (b *Backend) configure(ctx context.Context) error { 94 // Grab the resource data 95 b.configData = schema.FromContextBackendConfig(ctx) 96 data := b.configData 97 98 b.connStr = data.Get("conn_str").(string) 99 b.schemaName = pq.QuoteIdentifier(data.Get("schema_name").(string)) 100 101 db, err := sql.Open("postgres", b.connStr) 102 if err != nil { 103 return err 104 } 105 106 // Prepare database schema, tables, & indexes. 107 var query string 108 109 if !data.Get("skip_schema_creation").(bool) { 110 // list all schemas to see if it exists 111 var count int 112 query = `select count(1) from information_schema.schemata where schema_name = $1` 113 if err := db.QueryRow(query, data.Get("schema_name").(string)).Scan(&count); err != nil { 114 return err 115 } 116 117 // skip schema creation if schema already exists 118 // `CREATE SCHEMA IF NOT EXISTS` is to be avoided if ever 119 // a user hasn't been granted the `CREATE SCHEMA` privilege 120 if count < 1 { 121 // tries to create the schema 122 query = `CREATE SCHEMA IF NOT EXISTS %s` 123 if _, err := db.Exec(fmt.Sprintf(query, b.schemaName)); err != nil { 124 return err 125 } 126 } 127 } 128 129 if !data.Get("skip_table_creation").(bool) { 130 if _, err := db.Exec("CREATE SEQUENCE IF NOT EXISTS public.global_states_id_seq AS bigint"); err != nil { 131 return err 132 } 133 134 query = `CREATE TABLE IF NOT EXISTS %s.%s ( 135 id bigint NOT NULL DEFAULT nextval('public.global_states_id_seq') PRIMARY KEY, 136 name text UNIQUE, 137 data text 138 )` 139 if _, err := db.Exec(fmt.Sprintf(query, b.schemaName, statesTableName)); err != nil { 140 return err 141 } 142 } 143 144 if !data.Get("skip_index_creation").(bool) { 145 query = `CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s.%s (name)` 146 if _, err := db.Exec(fmt.Sprintf(query, statesIndexName, b.schemaName, statesTableName)); err != nil { 147 return err 148 } 149 } 150 151 // Assign db after its schema is prepared. 152 b.db = db 153 154 return nil 155 }