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
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, "=")
})
}