Skip to content

Commit

Permalink
feat(config): add api config for branching override (#2761)
Browse files Browse the repository at this point in the history
* feat: add remote utils for api config

* chore: split api and remote api

* Revert "chore: split api and remote api"

This reverts commit 23173ec.

* chore: make api public

* Revert "Revert "chore: split api and remote api""

This reverts commit 65bc6d3.

* chore: handle api enable

* chore: make convert whitespace resilient

* feat: add some errors handling for remotes config

* chore: move diff into own package

* chore: add some diff tests

* chore: fix golint casting lints

* Update internal/utils/cast/cast.go

Co-authored-by: Han Qiao <[email protected]>

* chore: use Errorf remote config error

* chore: move diff and cast to pkg

* chore: minor refactor

* feat: implement remote config updater

* chore: minor style changes

* chore: refactor duplicate project ref check to getter

* chore: update error message for consistency

* chore: validate duplicate remote early

---------

Co-authored-by: Han Qiao <[email protected]>
Co-authored-by: Qiao Han <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent fe0097e commit c7bdbc2
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 38 deletions.
30 changes: 13 additions & 17 deletions internal/link/link.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package link

import (
"bytes"
"context"
"fmt"
"os"
"strconv"
"strings"
"sync"

"github.com/BurntSushi/toml"
"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
Expand All @@ -20,15 +18,20 @@ import (
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/internal/utils/tenant"
"github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
cliConfig "github.com/supabase/cli/pkg/config"
"github.com/supabase/cli/pkg/diff"
"github.com/supabase/cli/pkg/migration"
)

func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
original := toTomlBytes(map[string]interface{}{
original, err := cliConfig.ToTomlBytes(map[string]interface{}{
"api": utils.Config.Api,
"db": utils.Config.Db,
})
if err != nil {
fmt.Fprintln(utils.GetDebugLogger(), err)
}

if err := checkRemoteProjectStatus(ctx, projectRef); err != nil {
return err
Expand Down Expand Up @@ -60,28 +63,21 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(
fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".")

// 4. Suggest config update
updated := toTomlBytes(map[string]interface{}{
updated, err := cliConfig.ToTomlBytes(map[string]interface{}{
"api": utils.Config.Api,
"db": utils.Config.Db,
})
// if lineDiff := cmp.Diff(original, updated); len(lineDiff) > 0 {
if lineDiff := Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 {
if err != nil {
fmt.Fprintln(utils.GetDebugLogger(), err)
}

if lineDiff := diff.Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 {
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local config differs from linked project. Try updating", utils.Bold(utils.ConfigPath))
fmt.Println(string(lineDiff))
}
return nil
}

func toTomlBytes(config any) []byte {
var buf bytes.Buffer
enc := toml.NewEncoder(&buf)
enc.Indent = ""
if err := enc.Encode(config); err != nil {
fmt.Fprintln(utils.GetDebugLogger(), "failed to marshal toml config:", err)
}
return buf.Bytes()
}

func LinkServices(ctx context.Context, projectRef, anonKey string, fsys afero.Fs) {
// Ignore non-fatal errors linking services
var wg sync.WaitGroup
Expand Down Expand Up @@ -147,7 +143,7 @@ func linkPostgrestVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.
}

func updateApiConfig(config api.PostgrestConfigWithJWTSecretResponse) {
utils.Config.Api.MaxRows = uint(config.MaxRows)
utils.Config.Api.MaxRows = cast.IntToUint(config.MaxRows)
utils.Config.Api.ExtraSearchPath = readCsv(config.DbExtraSearchPath)
utils.Config.Api.Schemas = readCsv(config.DbSchema)
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/cast/cast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cast

import "math"

// UintToInt converts a uint to an int, handling potential overflow
func UintToInt(value uint) int {
if value <= math.MaxInt {
result := int(value)
return result
}
maxInt := math.MaxInt
return maxInt
}

// IntToUint converts an int to a uint, handling negative values
func IntToUint(value int) uint {
if value < 0 {
return 0
}
return uint(value)
}

func Ptr[T any](v T) *T {
return &v
}
101 changes: 101 additions & 0 deletions pkg/config/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package config

import (
"strings"

v1API "github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/diff"
)

type (
api struct {
Enabled bool `toml:"enabled"`
Schemas []string `toml:"schemas"`
ExtraSearchPath []string `toml:"extra_search_path"`
MaxRows uint `toml:"max_rows"`
// Local only config
Image string `toml:"-"`
KongImage string `toml:"-"`
Port uint16 `toml:"port"`
Tls tlsKong `toml:"tls"`
// TODO: replace [auth|studio].api_url
ExternalUrl string `toml:"external_url"`
}

tlsKong struct {
Enabled bool `toml:"enabled"`
}
)

func (a *api) ToUpdatePostgrestConfigBody() v1API.UpdatePostgrestConfigBody {
body := v1API.UpdatePostgrestConfigBody{}

// When the api is disabled, remote side it just set the dbSchema to an empty value
if !a.Enabled {
body.DbSchema = cast.Ptr("")
return body
}

// Convert Schemas to a comma-separated string
if len(a.Schemas) > 0 {
schemas := strings.Join(a.Schemas, ",")
body.DbSchema = &schemas
}

// Convert ExtraSearchPath to a comma-separated string
if len(a.ExtraSearchPath) > 0 {
extraSearchPath := strings.Join(a.ExtraSearchPath, ",")
body.DbExtraSearchPath = &extraSearchPath
}

// Convert MaxRows to int pointer
if a.MaxRows > 0 {
body.MaxRows = cast.Ptr(cast.UintToInt(a.MaxRows))
}

// Note: DbPool is not present in the Api struct, so it's not set here
return body
}

func (a *api) fromRemoteApiConfig(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) api {
result := *a
if remoteConfig.DbSchema == "" {
result.Enabled = false
return result
}

result.Enabled = true
// Update Schemas if present in remoteConfig
schemas := strings.Split(remoteConfig.DbSchema, ",")
result.Schemas = make([]string, len(schemas))
// TODO: use slices.Map when upgrade go version
for i, schema := range schemas {
result.Schemas[i] = strings.TrimSpace(schema)
}

// Update ExtraSearchPath if present in remoteConfig
extraSearchPath := strings.Split(remoteConfig.DbExtraSearchPath, ",")
result.ExtraSearchPath = make([]string, len(extraSearchPath))
for i, path := range extraSearchPath {
result.ExtraSearchPath[i] = strings.TrimSpace(path)
}

// Update MaxRows if present in remoteConfig
result.MaxRows = cast.IntToUint(remoteConfig.MaxRows)

return result
}

func (a *api) DiffWithRemote(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) ([]byte, error) {
// Convert the config values into easily comparable remoteConfig values
currentValue, err := ToTomlBytes(a)
if err != nil {
return nil, err
}
remoteCompare, err := ToTomlBytes(a.fromRemoteApiConfig(remoteConfig))
if err != nil {
return nil, err
}
return diff.Diff("remote[api]", remoteCompare, "local[api]", currentValue), nil
}
143 changes: 143 additions & 0 deletions pkg/config/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
v1API "github.com/supabase/cli/pkg/api"
)

func TestApiToUpdatePostgrestConfigBody(t *testing.T) {
t.Run("converts all fields correctly", func(t *testing.T) {
api := &api{
Enabled: true,
Schemas: []string{"public", "private"},
ExtraSearchPath: []string{"extensions", "public"},
MaxRows: 1000,
}

body := api.ToUpdatePostgrestConfigBody()

assert.Equal(t, "public,private", *body.DbSchema)
assert.Equal(t, "extensions,public", *body.DbExtraSearchPath)
assert.Equal(t, 1000, *body.MaxRows)
})

t.Run("handles empty fields", func(t *testing.T) {
api := &api{}

body := api.ToUpdatePostgrestConfigBody()

// remote api will be false by default, leading to an empty schema on api side
assert.Equal(t, "", *body.DbSchema)
})
}

func TestApiDiffWithRemote(t *testing.T) {
t.Run("detects differences", func(t *testing.T) {
api := &api{
Enabled: true,
Schemas: []string{"public", "private"},
ExtraSearchPath: []string{"extensions", "public"},
MaxRows: 1000,
}

remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
DbSchema: "public",
DbExtraSearchPath: "public",
MaxRows: 500,
}

diff, err := api.DiffWithRemote(remoteConfig)
assert.NoError(t, err, string(diff))

assert.Contains(t, string(diff), "-schemas = [\"public\"]")
assert.Contains(t, string(diff), "+schemas = [\"public\", \"private\"]")
assert.Contains(t, string(diff), "-extra_search_path = [\"public\"]")
assert.Contains(t, string(diff), "+extra_search_path = [\"extensions\", \"public\"]")
assert.Contains(t, string(diff), "-max_rows = 500")
assert.Contains(t, string(diff), "+max_rows = 1000")
})

t.Run("handles no differences", func(t *testing.T) {
api := &api{
Enabled: true,
Schemas: []string{"public"},
ExtraSearchPath: []string{"public"},
MaxRows: 500,
}

remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
DbSchema: "public",
DbExtraSearchPath: "public",
MaxRows: 500,
}

diff, err := api.DiffWithRemote(remoteConfig)
assert.NoError(t, err)

assert.Empty(t, diff)
})

t.Run("handles multiple schemas and search paths with spaces", func(t *testing.T) {
api := &api{
Enabled: true,
Schemas: []string{"public", "private"},
ExtraSearchPath: []string{"extensions", "public"},
MaxRows: 500,
}

remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
DbSchema: "public, private",
DbExtraSearchPath: "extensions, public",
MaxRows: 500,
}

diff, err := api.DiffWithRemote(remoteConfig)
assert.NoError(t, err)

assert.Empty(t, diff)
})

t.Run("handles api disabled on remote side", func(t *testing.T) {
api := &api{
Enabled: true,
Schemas: []string{"public", "private"},
ExtraSearchPath: []string{"extensions", "public"},
MaxRows: 500,
}

remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
DbSchema: "",
DbExtraSearchPath: "",
MaxRows: 0,
}

diff, err := api.DiffWithRemote(remoteConfig)
assert.NoError(t, err, string(diff))

assert.Contains(t, string(diff), "-enabled = false")
assert.Contains(t, string(diff), "+enabled = true")
})

t.Run("handles api disabled on local side", func(t *testing.T) {
api := &api{
Enabled: false,
Schemas: []string{"public"},
ExtraSearchPath: []string{"public"},
MaxRows: 500,
}

remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
DbSchema: "public",
DbExtraSearchPath: "public",
MaxRows: 500,
}

diff, err := api.DiffWithRemote(remoteConfig)
assert.NoError(t, err, string(diff))

assert.Contains(t, string(diff), "-enabled = true")
assert.Contains(t, string(diff), "+enabled = false")
})
}
Loading

0 comments on commit c7bdbc2

Please sign in to comment.