Skip to content
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
3 changes: 2 additions & 1 deletion nitronode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ assets:
|----------|-------------|---------|
| `NITRONODE_SIGNER_KEY` | Private key for signing node state updates | (Required) |
| `NITRONODE_DATABASE_DRIVER` | `sqlite` or `postgres` | `sqlite` |
| `NITRONODE_DATABASE_URL` | Connection string or file path | `nitronode.db` |
| `NITRONODE_DATABASE_URL` | Postgres DSN/URL or sqlite file path. When set for `postgres`, used verbatim and overrides the individual host/user/password/sslmode fields | `nitronode.db` |
| `NITRONODE_DATABASE_SSLMODE` | Postgres SSL mode: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` | `require` |
| `NITRONODE_LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` |
| `NITRONODE_BLOCKCHAIN_RPC_<NAME>` | RPC endpoint for a specific blockchain | (Required) |

Expand Down
3 changes: 3 additions & 0 deletions nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ config:
port: 5432
name: nitronode_stress_v1
user: nitronode_stress_v1_admin
# pgbouncer runs in-cluster on a private network, so TLS is not required
# here. Override the chart's `require` default explicitly.
sslmode: disable
envSecret: nitronode-secret-env
extraEnvs:
NITRONODE_DATABASE_MAX_OPEN_CONNS: "{{ $p.database.maxOpenConns }}"
Expand Down
2 changes: 2 additions & 0 deletions nitronode/chart/templates/helpers/_common.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Returns common environment variables
value: "{{ print .port }}"
- name: NITRONODE_DATABASE_USERNAME
value: {{ .user }}
- name: NITRONODE_DATABASE_SSLMODE
value: {{ .sslmode | default "require" | quote }}
{{- end }}
{{- range $key, $value := .Values.config.extraEnvs }}
- name: {{ $key | upper }}
Expand Down
9 changes: 7 additions & 2 deletions nitronode/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ config:
user: changeme
# -- Database password
password: changeme
# -- Database SSL mode (disable, require, verify-ca, verify-full)
sslmode: disable
# -- Database SSL mode (disable, require, verify-ca, verify-full).
# Defaults to `require` so any deployment that exposes Postgres over an
# untrusted network gets TLS without extra configuration. Override to
# `disable` for setups where the database is only reachable on a private
# network (e.g. a cluster-internal pgbouncer / VPC-only Cloud SQL) and TLS
# is not required; use `verify-ca` / `verify-full` for strict cert checking.
sslmode: require
# -- Name of the secret containing GCP SA Credentials (Optional)
gcpSaSecret: ""
# -- Additional environment variables as key-value pairs
Expand Down
33 changes: 31 additions & 2 deletions nitronode/store/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
//
// To connect to sqlite, you just need to specify "sqlite" driver.
// By default it will use in-memory database. You can provide NITRONODE_DATABASE_NAME to use the file.
//
// For Postgresql, NITRONODE_DATABASE_URL takes precedence: when set, it is used verbatim
// and the individual Username/Password/Host/Port/Name/SSLMode fields are ignored.
type DatabaseConfig struct {
URL string `env:"NITRONODE_DATABASE_URL" env-default:""`
Name string `env:"NITRONODE_DATABASE_NAME" env-default:""`
Expand All @@ -28,6 +31,7 @@ type DatabaseConfig struct {
Password string `env:"NITRONODE_DATABASE_PASSWORD" env-default:"your-super-secret-and-long-postgres-password"`
Host string `env:"NITRONODE_DATABASE_HOST" env-default:"localhost"`
Port string `env:"NITRONODE_DATABASE_PORT" env-default:"5432"`
SSLMode string `env:"NITRONODE_DATABASE_SSLMODE" env-default:"require"`
Retries int `env:"NITRONODE_DATABASE_RETRIES" env-default:"5"`

// Connection pool settings
Expand Down Expand Up @@ -127,12 +131,37 @@ func connectToSqlite(cnf DatabaseConfig) (*gorm.DB, error) {
return db, nil
}

// validPostgresSSLModes lists sslmode values accepted by libpq / pgx.
// See https://www.postgresql.org/docs/current/libpq-ssl.html.
var validPostgresSSLModes = map[string]struct{}{
"disable": {},
"allow": {},
"prefer": {},
"require": {},
"verify-ca": {},
"verify-full": {},
}

func postgresqlDbUrl(cnf DatabaseConfig) (string, error) {
switch cnf.Driver {
case "postgres":
// URL, when supplied, is used verbatim. The operator owns sslmode, search_path,
// and any other parameters encoded in it.
if cnf.URL != "" {
return cnf.URL, nil
}

sslMode := cnf.SSLMode
if sslMode == "" {
sslMode = "require"
}
if _, ok := validPostgresSSLModes[sslMode]; !ok {
return "", fmt.Errorf("invalid sslmode %q: must be one of disable, allow, prefer, require, verify-ca, verify-full", sslMode)
}

dsn := fmt.Sprintf(
"user=%s password=%s host=%s port=%s dbname=%s sslmode=disable",
cnf.Username, cnf.Password, cnf.Host, cnf.Port, cnf.Name,
"user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
cnf.Username, cnf.Password, cnf.Host, cnf.Port, cnf.Name, sslMode,
)

if cnf.Schema != "" {
Expand Down
77 changes: 77 additions & 0 deletions nitronode/store/database/database_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package database

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPostgresqlDbUrl(t *testing.T) {
base := DatabaseConfig{
Driver: "postgres",
Username: "user",
Password: "pass",
Host: "db.example.com",
Port: "5432",
Name: "nitronode",
}

t.Run("DefaultsToRequireWhenSSLModeEmpty", func(t *testing.T) {
dsn, err := postgresqlDbUrl(base)
require.NoError(t, err)
assert.Contains(t, dsn, "sslmode=require")
assert.NotContains(t, dsn, "sslmode=disable")
})

t.Run("HonorsExplicitSSLMode", func(t *testing.T) {
cnf := base
cnf.SSLMode = "verify-full"
dsn, err := postgresqlDbUrl(cnf)
require.NoError(t, err)
assert.Contains(t, dsn, "sslmode=verify-full")
})

t.Run("AllowsDisableForLocalDev", func(t *testing.T) {
cnf := base
cnf.SSLMode = "disable"
dsn, err := postgresqlDbUrl(cnf)
require.NoError(t, err)
assert.Contains(t, dsn, "sslmode=disable")
})

t.Run("RejectsInvalidSSLMode", func(t *testing.T) {
cnf := base
cnf.SSLMode = "totally-bogus"
_, err := postgresqlDbUrl(cnf)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid sslmode")
})

t.Run("AppendsSearchPathWhenSchemaSet", func(t *testing.T) {
cnf := base
cnf.Schema = "tenant_a"
dsn, err := postgresqlDbUrl(cnf)
require.NoError(t, err)
assert.Contains(t, dsn, "search_path=tenant_a")
})

t.Run("URLOverridesIndividualFields", func(t *testing.T) {
cnf := base
cnf.URL = "postgres://override:secret@otherhost:6543/otherdb?sslmode=verify-ca"
cnf.SSLMode = "disable" // ignored when URL set
dsn, err := postgresqlDbUrl(cnf)
require.NoError(t, err)
assert.Equal(t, cnf.URL, dsn)
assert.False(t, strings.Contains(dsn, "user=user"), "URL must be returned verbatim")
})

t.Run("RejectsUnsupportedDriver", func(t *testing.T) {
cnf := base
cnf.Driver = "mysql"
_, err := postgresqlDbUrl(cnf)
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported driver")
})
}