Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve environment variable support for the pg backend #33045

Merged
merged 2 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions internal/backend/remote-state/pg/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"database/sql"
"fmt"
"os"
"strconv"

"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
Expand All @@ -15,41 +17,53 @@ const (
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.
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"conn_str": {
Type: schema.TypeString,
Required: true,
Optional: true,
Description: "Postgres connection string; a `postgres://` URL",
DefaultFunc: schema.EnvDefaultFunc("PGDATABASE", nil),
DefaultFunc: schema.EnvDefaultFunc("PG_CONN_STR", nil),
},

"schema_name": {
Type: schema.TypeString,
Optional: true,
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": {
Type: schema.TypeBool,
Optional: true,
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": {
Type: schema.TypeBool,
Optional: true,
Description: "If set to `true`, Terraform won't try to create the Postgres table",
DefaultFunc: defaultBoolFunc("PG_SKIP_TABLE_CREATION", false),
},

"skip_index_creation": {
Type: schema.TypeBool,
Optional: true,
Description: "If set to `true`, Terraform won't try to create the Postgres index",
DefaultFunc: defaultBoolFunc("PG_SKIP_INDEX_CREATION", false),
},
},
}
Expand Down
98 changes: 83 additions & 15 deletions internal/backend/remote-state/pg/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package pg
import (
"database/sql"
"fmt"
"net/url"
"os"
"strings"
"testing"
Expand All @@ -22,30 +23,38 @@ import (
//
// A running Postgres server identified by env variable
// DATABASE_URL is required for acceptance tests.
func testACC(t *testing.T) {
func testACC(t *testing.T) string {
skip := os.Getenv("TF_ACC") == ""
if skip {
t.Log("pg backend tests require setting TF_ACC")
t.Skip()
}
if os.Getenv("DATABASE_URL") == "" {
os.Setenv("DATABASE_URL", "postgres://localhost/terraform_backend_pg_test?sslmode=disable")
databaseUrl, found := os.LookupEnv("DATABASE_URL")
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) {
var _ backend.Backend = new(Backend)
}

func TestBackendConfig(t *testing.T) {
testACC(t)
databaseName := testACC(t)
connStr := getDatabaseUrl()

testCases := []struct {
Name string
EnvVars map[string]string
Config map[string]interface{}
ExpectError string
Name string
EnvVars map[string]string
Config map[string]interface{}
ExpectConfigurationError string
ExpectConnectionError string
}{
{
Name: "valid-config",
Expand All @@ -55,20 +64,70 @@ 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{}{
"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",
EnvVars: map[string]string{
"PGDATABASE": connStr,
"PG_CONN_STR": connStr,
},
Config: map[string]interface{}{
"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"`,
},
}

Expand Down Expand Up @@ -97,12 +156,12 @@ func TestBackendConfig(t *testing.T) {
newObj, valDiags := b.PrepareConfig(obj)
diags = diags.Append(valDiags.InConfigBody(config, ""))

if tc.ExpectError != "" {
if tc.ExpectConfigurationError != "" {
if !diags.HasErrors() {
t.Fatal("error expected but got none")
}
if !strings.Contains(diags.ErrWithWarnings().Error(), tc.ExpectError) {
t.Fatalf("failed to find %q in %s", tc.ExpectError, diags.ErrWithWarnings())
if !strings.Contains(diags.ErrWithWarnings().Error(), tc.ExpectConfigurationError) {
t.Fatalf("failed to find %q in %s", tc.ExpectConfigurationError, diags.ErrWithWarnings())
}
return
} else if diags.HasErrors() {
Expand All @@ -112,7 +171,16 @@ func TestBackendConfig(t *testing.T) {
obj = newObj

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, "")
t.Fatal(confDiags.ErrWithWarnings())
}
Expand Down
46 changes: 32 additions & 14 deletions website/docs/language/settings/backends/pg.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,44 @@ 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.

We recommend using a
[partial configuration](/terraform/language/settings/backends/configuration#partial-configuration)
for the `conn_str` variable, because it typically contains access credentials that should not be committed to source control:

### Using environment variables

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
terraform {
backend "pg" {}
}
```

Then, set the credentials when initializing the configuration:

```
terraform init -backend-config="conn_str=postgres://user:pass@db.example.com/terraform_backend"
```shellsession
$ export PG_CONN_STR=postgres://user:pass@db.example.com/terraform_backend
$ terraform init
```

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
Expand All @@ -68,11 +86,11 @@ data "terraform_remote_state" "network" {

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.
- `schema_name` - Name of the automatically-managed Postgres schema, default `terraform_remote_state`.
- `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_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_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.
- `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 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. 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. 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. 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

Expand Down