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

feat(config): add api config for branching override #2761

Merged
merged 24 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
15ff758
feat: add remote utils for api config
avallete Oct 11, 2024
23173ec
chore: split api and remote api
avallete Oct 11, 2024
65bc6d3
Revert "chore: split api and remote api"
avallete Oct 11, 2024
21270cf
chore: make api public
avallete Oct 11, 2024
b7ad1e0
Revert "Revert "chore: split api and remote api""
avallete Oct 11, 2024
327a09a
chore: handle api enable
avallete Oct 11, 2024
018ab57
chore: make convert whitespace resilient
avallete Oct 11, 2024
f0b9d57
feat: add some errors handling for remotes config
avallete Oct 11, 2024
935b0f7
chore: move diff into own package
avallete Oct 12, 2024
a3a13f6
Merge branch 'develop' into avallete/feat-add-api-config-remote-tooling
avallete Oct 12, 2024
6d84006
chore: add some diff tests
avallete Oct 12, 2024
bf03ef7
Merge branch 'develop' into avallete/feat-add-api-config-remote-tooling
avallete Oct 14, 2024
cfada5b
chore: fix golint casting lints
avallete Oct 14, 2024
274a438
Update internal/utils/cast/cast.go
avallete Oct 15, 2024
af51f75
chore: use Errorf remote config error
avallete Oct 15, 2024
70519e8
chore: move diff and cast to pkg
avallete Oct 15, 2024
afe73fe
Merge branch 'develop' into avallete/feat-add-api-config-remote-tooling
avallete Oct 15, 2024
3af42eb
chore: minor refactor
sweatybridge Oct 16, 2024
f5f960e
feat: implement remote config updater
sweatybridge Oct 16, 2024
bd4b42d
chore: minor style changes
sweatybridge Oct 16, 2024
1183979
chore: refactor duplicate project ref check to getter
sweatybridge Oct 16, 2024
1a8b72d
chore: update error message for consistency
sweatybridge Oct 16, 2024
6a45ff3
chore: validate duplicate remote early
sweatybridge Oct 16, 2024
0ceeb14
Merge branch 'develop' into avallete/feat-add-api-config-remote-tooling
avallete Oct 16, 2024
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: 6 additions & 16 deletions internal/link/link.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
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"
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/cast"
"github.com/supabase/cli/internal/utils/credentials"
"github.com/supabase/cli/internal/utils/diff"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/internal/utils/tenant"
"github.com/supabase/cli/pkg/api"
Expand All @@ -25,7 +25,7 @@ import (
)

func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
original := toTomlBytes(map[string]interface{}{
original := cliConfig.ToTomlBytes(map[string]interface{}{
"api": utils.Config.Api,
"db": utils.Config.Db,
})
Expand Down Expand Up @@ -60,28 +60,18 @@ 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 := 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 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 +137,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
21 changes: 21 additions & 0 deletions internal/utils/cast/cast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cast

import "math"

// UintToInt converts a uint to an *int, handling potential overflow
avallete marked this conversation as resolved.
Show resolved Hide resolved
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)
}
2 changes: 1 addition & 1 deletion internal/link/diff.go → internal/utils/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package link
package diff
avallete marked this conversation as resolved.
Show resolved Hide resolved

import (
"bytes"
Expand Down
98 changes: 98 additions & 0 deletions pkg/config/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package config

import (
"strings"

"github.com/supabase/cli/internal/utils/cast"
"github.com/supabase/cli/internal/utils/diff"
avallete marked this conversation as resolved.
Show resolved Hide resolved
v1API "github.com/supabase/cli/pkg/api"
)

type (
RemoteApi struct {
Enabled bool `toml:"enabled"`
Schemas []string `toml:"schemas"`
ExtraSearchPath []string `toml:"extra_search_path"`
MaxRows uint `toml:"max_rows"`
}
avallete marked this conversation as resolved.
Show resolved Hide resolved
api struct {
RemoteApi
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"`
}
avallete marked this conversation as resolved.
Show resolved Hide resolved

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

func (a *RemoteApi) 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 {
emptyString := ""
body.DbSchema = &emptyString
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 {
intValue := cast.UintToInt(a.MaxRows)
body.MaxRows = &intValue
}

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

func (a *RemoteApi) fromRemoteApiConfig(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) RemoteApi {
sweatybridge marked this conversation as resolved.
Show resolved Hide resolved
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 *RemoteApi) DiffWithRemote(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) []byte {
// Convert the config values into easily comparable remoteConfig values
currentValue := ToTomlBytes(a)
remoteCompare := ToTomlBytes(a.fromRemoteApiConfig(remoteConfig))
return diff.Diff("remote[api]", remoteCompare, "local[api]", currentValue)
}
140 changes: 140 additions & 0 deletions pkg/config/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package config

import (
"fmt"
"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 := &RemoteApi{
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 := &RemoteApi{}

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 := &RemoteApi{
Enabled: true,
Schemas: []string{"public", "private"},
ExtraSearchPath: []string{"extensions", "public"},
MaxRows: 1000,
}

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

diff := api.DiffWithRemote(remoteConfig)

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 := &RemoteApi{
Enabled: true,
Schemas: []string{"public"},
ExtraSearchPath: []string{"public"},
MaxRows: 500,
}

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

diff := api.DiffWithRemote(remoteConfig)

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

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

diff := api.DiffWithRemote(remoteConfig)

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

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

diff := api.DiffWithRemote(remoteConfig)
d := string(diff)
fmt.Println(d)

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 := &RemoteApi{
Enabled: false,
Schemas: []string{"public"},
ExtraSearchPath: []string{"public"},
MaxRows: 500,
}

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

diff := api.DiffWithRemote(remoteConfig)
d := string(diff)
fmt.Println(d)

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