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  }