Improve environment variable support for the pg backend (#33045)

* Improve environment variable support for the pg backend

This patch does two things:
  - it adds environment variable support to the parameters that did
    not have it (and uses `PG_CONN_STR` instead of `PGDATABASE` which is
    actually more appropriate to match the behavior of other PostgreSQL
    utilities)
  - better documents how to give the connection parameters as environment
    variables for the ones that were already supported based on the
	recommendation of @bsouth00

I will prepare a backport of the documentation part of this once it is
merged.

Closes https://github.com/hashicorp/terraform/issues/33024

* Remove global variable in test of the PG backend
This commit is contained in:
Rémi Lapeyre 2023-04-21 08:39:19 +02:00 committed by GitHub
parent 7e2e834aff
commit af571b2642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 133 additions and 33 deletions

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"os"
"strconv"
"github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/hashicorp/terraform/internal/legacy/helper/schema"
@ -15,41 +17,53 @@ const (
statesIndexName = "states_by_name" statesIndexName = "states_by_name"
) )
func defaultBoolFunc(k string, dv bool) schema.SchemaDefaultFunc {
return func() (interface{}, error) {
if v := os.Getenv(k); v != "" {
return strconv.ParseBool(v)
}
return dv, nil
}
}
// New creates a new backend for Postgres remote state. // New creates a new backend for Postgres remote state.
func New() backend.Backend { func New() backend.Backend {
s := &schema.Backend{ s := &schema.Backend{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"conn_str": { "conn_str": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
Description: "Postgres connection string; a `postgres://` URL", Description: "Postgres connection string; a `postgres://` URL",
DefaultFunc: schema.EnvDefaultFunc("PGDATABASE", nil), DefaultFunc: schema.EnvDefaultFunc("PG_CONN_STR", nil),
}, },
"schema_name": { "schema_name": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Description: "Name of the automatically managed Postgres schema to store state", Description: "Name of the automatically managed Postgres schema to store state",
Default: "terraform_remote_state", DefaultFunc: schema.EnvDefaultFunc("PG_SCHEMA_NAME", "terraform_remote_state"),
}, },
"skip_schema_creation": { "skip_schema_creation": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Description: "If set to `true`, Terraform won't try to create the Postgres schema", Description: "If set to `true`, Terraform won't try to create the Postgres schema",
Default: false, DefaultFunc: defaultBoolFunc("PG_SKIP_SCHEMA_CREATION", false),
}, },
"skip_table_creation": { "skip_table_creation": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Description: "If set to `true`, Terraform won't try to create the Postgres table", Description: "If set to `true`, Terraform won't try to create the Postgres table",
DefaultFunc: defaultBoolFunc("PG_SKIP_TABLE_CREATION", false),
}, },
"skip_index_creation": { "skip_index_creation": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Description: "If set to `true`, Terraform won't try to create the Postgres index", Description: "If set to `true`, Terraform won't try to create the Postgres index",
DefaultFunc: defaultBoolFunc("PG_SKIP_INDEX_CREATION", false),
}, },
}, },
} }

View File

@ -6,6 +6,7 @@ package pg
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/url"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -22,15 +23,22 @@ import (
// //
// A running Postgres server identified by env variable // A running Postgres server identified by env variable
// DATABASE_URL is required for acceptance tests. // DATABASE_URL is required for acceptance tests.
func testACC(t *testing.T) { func testACC(t *testing.T) string {
skip := os.Getenv("TF_ACC") == "" skip := os.Getenv("TF_ACC") == ""
if skip { if skip {
t.Log("pg backend tests require setting TF_ACC") t.Log("pg backend tests require setting TF_ACC")
t.Skip() t.Skip()
} }
if os.Getenv("DATABASE_URL") == "" { databaseUrl, found := os.LookupEnv("DATABASE_URL")
os.Setenv("DATABASE_URL", "postgres://localhost/terraform_backend_pg_test?sslmode=disable") if !found {
databaseUrl = "postgres://localhost/terraform_backend_pg_test?sslmode=disable"
os.Setenv("DATABASE_URL", databaseUrl)
} }
u, err := url.Parse(databaseUrl)
if err != nil {
t.Fatal(err)
}
return u.Path[1:]
} }
func TestBackend_impl(t *testing.T) { func TestBackend_impl(t *testing.T) {
@ -38,14 +46,15 @@ func TestBackend_impl(t *testing.T) {
} }
func TestBackendConfig(t *testing.T) { func TestBackendConfig(t *testing.T) {
testACC(t) databaseName := testACC(t)
connStr := getDatabaseUrl() connStr := getDatabaseUrl()
testCases := []struct { testCases := []struct {
Name string Name string
EnvVars map[string]string EnvVars map[string]string
Config map[string]interface{} Config map[string]interface{}
ExpectError string ExpectConfigurationError string
ExpectConnectionError string
}{ }{
{ {
Name: "valid-config", Name: "valid-config",
@ -55,21 +64,71 @@ func TestBackendConfig(t *testing.T) {
}, },
}, },
{ {
Name: "missing-conn-str", Name: "missing-conn_str-defaults-to-localhost",
EnvVars: map[string]string{
"PGSSLMODE": "disable",
"PGDATABASE": databaseName,
},
Config: map[string]interface{}{ Config: map[string]interface{}{
"schema_name": fmt.Sprintf("terraform_%s", t.Name()), "schema_name": fmt.Sprintf("terraform_%s", t.Name()),
}, },
ExpectError: `The attribute "conn_str" is required, but no definition was found.`,
}, },
{ {
Name: "conn-str-env-var", Name: "conn-str-env-var",
EnvVars: map[string]string{ EnvVars: map[string]string{
"PGDATABASE": connStr, "PG_CONN_STR": connStr,
}, },
Config: map[string]interface{}{ Config: map[string]interface{}{
"schema_name": fmt.Sprintf("terraform_%s", t.Name()), "schema_name": fmt.Sprintf("terraform_%s", t.Name()),
}, },
}, },
{
Name: "setting-credentials-using-env-vars",
EnvVars: map[string]string{
"PGUSER": "baduser",
"PGPASSWORD": "badpassword",
},
Config: map[string]interface{}{
"conn_str": connStr,
"schema_name": fmt.Sprintf("terraform_%s", t.Name()),
},
ExpectConnectionError: `role "baduser" does not exist`,
},
{
Name: "host-in-env-vars",
EnvVars: map[string]string{
"PGHOST": "hostthatdoesnotexist",
},
Config: map[string]interface{}{
"schema_name": fmt.Sprintf("terraform_%s", t.Name()),
},
ExpectConnectionError: `no such host`,
},
{
Name: "boolean-env-vars",
EnvVars: map[string]string{
"PGSSLMODE": "disable",
"PG_SKIP_SCHEMA_CREATION": "f",
"PG_SKIP_TABLE_CREATION": "f",
"PG_SKIP_INDEX_CREATION": "f",
"PGDATABASE": databaseName,
},
Config: map[string]interface{}{
"schema_name": fmt.Sprintf("terraform_%s", t.Name()),
},
},
{
Name: "wrong-boolean-env-vars",
EnvVars: map[string]string{
"PGSSLMODE": "disable",
"PG_SKIP_SCHEMA_CREATION": "foo",
"PGDATABASE": databaseName,
},
Config: map[string]interface{}{
"schema_name": fmt.Sprintf("terraform_%s", t.Name()),
},
ExpectConfigurationError: `error getting default for "skip_schema_creation"`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -97,12 +156,12 @@ func TestBackendConfig(t *testing.T) {
newObj, valDiags := b.PrepareConfig(obj) newObj, valDiags := b.PrepareConfig(obj)
diags = diags.Append(valDiags.InConfigBody(config, "")) diags = diags.Append(valDiags.InConfigBody(config, ""))
if tc.ExpectError != "" { if tc.ExpectConfigurationError != "" {
if !diags.HasErrors() { if !diags.HasErrors() {
t.Fatal("error expected but got none") t.Fatal("error expected but got none")
} }
if !strings.Contains(diags.ErrWithWarnings().Error(), tc.ExpectError) { if !strings.Contains(diags.ErrWithWarnings().Error(), tc.ExpectConfigurationError) {
t.Fatalf("failed to find %q in %s", tc.ExpectError, diags.ErrWithWarnings()) t.Fatalf("failed to find %q in %s", tc.ExpectConfigurationError, diags.ErrWithWarnings())
} }
return return
} else if diags.HasErrors() { } else if diags.HasErrors() {
@ -112,7 +171,16 @@ func TestBackendConfig(t *testing.T) {
obj = newObj obj = newObj
confDiags := b.Configure(obj) confDiags := b.Configure(obj)
if len(confDiags) != 0 { if tc.ExpectConnectionError != "" {
err := confDiags.InConfigBody(config, "").ErrWithWarnings()
if err == nil {
t.Fatal("error expected but got none")
}
if !strings.Contains(err.Error(), tc.ExpectConnectionError) {
t.Fatalf("failed to find %q in %s", tc.ExpectConnectionError, err)
}
return
} else if len(confDiags) != 0 {
confDiags = confDiags.InConfigBody(config, "") confDiags = confDiags.InConfigBody(config, "")
t.Fatal(confDiags.ErrWithWarnings()) t.Fatal(confDiags.ErrWithWarnings())
} }

View File

@ -27,9 +27,17 @@ createdb terraform_backend
This `createdb` command is found in [Postgres client applications](https://www.postgresql.org/docs/10/reference-client.html) which are installed along with the database server. This `createdb` command is found in [Postgres client applications](https://www.postgresql.org/docs/10/reference-client.html) which are installed along with the database server.
We recommend using a
[partial configuration](/terraform/language/settings/backends/configuration#partial-configuration) ### Using environment variables
for the `conn_str` variable, because it typically contains access credentials that should not be committed to source control:
We recommend using environment variables to configure the `pg` backend in order
not to have sensitive credentials written to disk and committed to source
control.
The `pg` backend supports the standard [`libpq` environment variables](https://www.postgresql.org/docs/current/libpq-envars.html).
The backend can be configured either by giving the whole configuration as an
environment variable:
```hcl ```hcl
terraform { terraform {
@ -37,16 +45,26 @@ terraform {
} }
``` ```
Then, set the credentials when initializing the configuration: ```shellsession
$ export PG_CONN_STR=postgres://user:pass@db.example.com/terraform_backend
``` $ terraform init
terraform init -backend-config="conn_str=postgres://user:pass@db.example.com/terraform_backend"
``` ```
To use a Postgres server running on the same machine as Terraform, configure localhost with SSL disabled: or just the sensitive parameters:
```hcl
terraform {
backend "pg" {
conn_str = "postgres://db.example.com/terraform_backend"
}
}
``` ```
terraform init -backend-config="conn_str=postgres://localhost/terraform_backend?sslmode=disable"
```shellsession
$ export PGUSER=user
$ read -s PGPASSWORD
$ export PGPASSWORD
$ terraform init
``` ```
## Data Source Configuration ## Data Source Configuration
@ -68,11 +86,11 @@ data "terraform_remote_state" "network" {
The following configuration options or environment variables are supported: The following configuration options or environment variables are supported:
- `conn_str` - (Required) Postgres connection string; a `postgres://` URL. `conn_str` can also be set using the `PGDATABASE` environment variable. - `conn_str` - Postgres connection string; a `postgres://` URL. The `PG_CONN_STR` and [standard `libpq`](https://www.postgresql.org/docs/current/libpq-envars.html) environment variables can also be used to indicate how to connect to the PostgreSQL database.
- `schema_name` - Name of the automatically-managed Postgres schema, default `terraform_remote_state`. - `schema_name` - Name of the automatically-managed Postgres schema, default to `terraform_remote_state`. Can also be set using the `PG_SCHEMA_NAME` environment variable.
- `skip_schema_creation` - If set to `true`, the Postgres schema must already exist. Terraform won't try to create the schema, this is useful when it has already been created by a database administrator. - `skip_schema_creation` - If set to `true`, the Postgres schema must already exist. Can also be set using the `PG_SKIP_SCHEMA_CREATION` environment variable. Terraform won't try to create the schema, this is useful when it has already been created by a database administrator.
- `skip_table_creation` - If set to `true`, the Postgres table must already exist. Terraform won't try to create the table, this is useful when it has already been created by a database administrator. - `skip_table_creation` - If set to `true`, the Postgres table must already exist. Can also be set using the `PG_SKIP_TABLE_CREATION` environment variable. Terraform won't try to create the table, this is useful when it has already been created by a database administrator.
- `skip_index_creation` - If set to `true`, the Postgres index must already exist. Terraform won't try to create the index, this is useful when it has already been created by a database administrator. - `skip_index_creation` - If set to `true`, the Postgres index must already exist. Can also be set using the `PG_SKIP_INDEX_CREATION` environment variable. Terraform won't try to create the index, this is useful when it has already been created by a database administrator.
## Technical Design ## Technical Design