Skip to content

Commit

Permalink
feat(config): allow local postgres configuration (#2796)
Browse files Browse the repository at this point in the history
* feat(cli): allow local postgres.conf configuration

Closes: #2611

* chore: test ToPostgresConfig

* chore: add start runtime test

* chore: use less max_connections

* fix: db start tests

* chore: add prefix to postgres config

* fix: test

* chore: serialise postgres conf as toml

---------

Co-authored-by: Qiao Han <[email protected]>
  • Loading branch information
avallete and sweatybridge authored Oct 24, 2024
1 parent 56c2cc4 commit 78bc7f7
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 6 deletions.
18 changes: 12 additions & 6 deletions internal/db/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ func NewContainerConfig() container.Config {
env := []string{
"POSTGRES_PASSWORD=" + utils.Config.Db.Password,
"POSTGRES_HOST=/var/run/postgresql",
"POSTGRES_INITDB_ARGS=--lc-ctype=C.UTF-8",
"JWT_SECRET=" + utils.Config.Auth.JwtSecret,
fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry),
}
Expand All @@ -81,13 +80,18 @@ func NewContainerConfig() container.Config {
Timeout: 2 * time.Second,
Retries: 3,
},
Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/postgresql.schema.sql && cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && docker-entrypoint.sh postgres -D /etc/postgresql
Entrypoint: []string{"sh", "-c", `
cat <<'EOF' > /etc/postgresql.schema.sql && \
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
docker-entrypoint.sh postgres -D /etc/postgresql
` + initialSchema + `
` + _supabaseSchema + `
EOF
` + utils.Config.Db.RootKey + `
EOF
`},
` + utils.Config.Db.Settings.ToPostgresConfig() + `
EOF`},
}
if utils.Config.Db.MajorVersion >= 14 {
config.Cmd = []string{"postgres",
Expand Down Expand Up @@ -124,11 +128,13 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
}
if utils.Config.Db.MajorVersion <= 14 {
config.Entrypoint = []string{"sh", "-c", `
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql && \
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
docker-entrypoint.sh postgres -D /etc/postgresql
` + _supabaseSchema + `
EOF
docker-entrypoint.sh postgres -D /etc/postgresql
`}
` + utils.Config.Db.Settings.ToPostgresConfig() + `
EOF`}
hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
}
// Creating volume will not override existing volume, so we must inspect explicitly
Expand Down
53 changes: 53 additions & 0 deletions internal/db/start/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/supabase/cli/internal/testing/apitest"
"github.com/supabase/cli/internal/testing/fstest"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/pgtest"
)

Expand Down Expand Up @@ -308,3 +309,55 @@ func TestSetupDatabase(t *testing.T) {
assert.Empty(t, apitest.ListUnmatchedRequests())
})
}
func TestStartDatabaseWithCustomSettings(t *testing.T) {
t.Run("starts database with custom MaxConnections", func(t *testing.T) {
// Setup
utils.Config.Db.MajorVersion = 15
utils.DbId = "supabase_db_test"
utils.ConfigId = "supabase_config_test"
utils.Config.Db.Port = 5432
utils.Config.Db.Settings.MaxConnections = cast.Ptr(uint(50))

// Setup in-memory fs
fsys := afero.NewMemMapFs()

// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
defer gock.OffAll()
gock.New(utils.Docker.DaemonHost()).
Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
Reply(http.StatusNotFound).
JSON(volume.Volume{})
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId)
gock.New(utils.Docker.DaemonHost()).
Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
Reply(http.StatusOK).
JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{
Running: true,
Health: &types.Health{Status: types.Healthy},
},
}})

apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)

// Run test
err := StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)

// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())

// Check if the custom MaxConnections setting was applied
config := NewContainerConfig()
assert.Contains(t, config.Entrypoint[2], "max_connections = 50")
})
}
13 changes: 13 additions & 0 deletions pkg/config/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"bytes"

"github.com/google/go-cmp/cmp"
v1API "github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
Expand Down Expand Up @@ -146,6 +148,17 @@ func (a *settings) fromRemoteConfig(remoteConfig v1API.PostgresConfigResponse) s
return result
}

const pgConfHeader = "\n# supabase [db.settings] configuration\n"

// create a valid string to append to /etc/postgresql/postgresql.conf
func (a *settings) ToPostgresConfig() string {
// Assuming postgres settings is always a flat struct, we can serialise
// using toml, then replace double quotes with single.
data, _ := ToTomlBytes(*a)
body := bytes.ReplaceAll(data, []byte{'"'}, []byte{'\''})
return pgConfHeader + string(body)
}

func (a *settings) DiffWithRemote(remoteConfig v1API.PostgresConfigResponse) ([]byte, error) {
// Convert the config values into easily comparable remoteConfig values
currentValue, err := ToTomlBytes(a)
Expand Down
38 changes: 38 additions & 0 deletions pkg/config/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,41 @@ func TestDbSettingsDiffWithRemote(t *testing.T) {
assert.Contains(t, string(diff), "-shared_buffers = \"1GB\"")
})
}

func TestSettingsToPostgresConfig(t *testing.T) {
t.Run("Only set values should appear", func(t *testing.T) {
settings := settings{
MaxConnections: cast.Ptr(uint(100)),
MaxLocksPerTransaction: cast.Ptr(uint(64)),
SharedBuffers: cast.Ptr("128MB"),
WorkMem: cast.Ptr("4MB"),
}
got := settings.ToPostgresConfig()

assert.Contains(t, got, "max_connections = 100")
assert.Contains(t, got, "max_locks_per_transaction = 64")
assert.Contains(t, got, "shared_buffers = '128MB'")
assert.Contains(t, got, "work_mem = '4MB'")

assert.NotContains(t, got, "effective_cache_size")
assert.NotContains(t, got, "maintenance_work_mem")
assert.NotContains(t, got, "max_parallel_workers")
})

t.Run("SessionReplicationRole should be handled correctly", func(t *testing.T) {
settings := settings{
SessionReplicationRole: cast.Ptr(SessionReplicationRoleOrigin),
}
got := settings.ToPostgresConfig()

assert.Contains(t, got, "session_replication_role = 'origin'")
})

t.Run("Empty settings should result in empty string", func(t *testing.T) {
settings := settings{}
got := settings.ToPostgresConfig()

assert.Equal(t, got, "\n# supabase [db.settings] configuration\n")
assert.NotContains(t, got, "=")
})
}

0 comments on commit 78bc7f7

Please sign in to comment.