Skip to content

Commit

Permalink
feat: support remote config overrides (#2704)
Browse files Browse the repository at this point in the history
* feat: setup basics for branch config override

* fix: use pointers for falsy values to determine emptyness

* chore: refactor turn Auth.EnableSignup into pointer

* wip: attemps non pointer approach

* fix: use direct toml parsing to distinguish undefined values

* chore: restore gomod

* chore: refactor to a single function leverage mergo

* chore: fix lint

* fix: add branch override logic to LoadConfigFs

* fix: inverted logic

* chore: add env branch override test

* chore: add test for slices merging

* chore: remote config overrides

* chore: validate project id

* Apply suggestions from code review

Co-authored-by: Andrew Valleteau <[email protected]>

---------

Co-authored-by: avallete <[email protected]>
Co-authored-by: Andrew Valleteau <[email protected]>
  • Loading branch information
3 people committed Sep 24, 2024
1 parent 5701583 commit 01256a1
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 7 deletions.
57 changes: 50 additions & 7 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/fs"
"maps"
"net"
"net/http"
"net/url"
Expand Down Expand Up @@ -119,7 +120,8 @@ func (c CustomClaims) NewToken() *jwt.Token {
//
// Default values for internal configs should be added to `var Config` initializer.
type (
config struct {
// Common config fields between our "base" config and any "remote" branch specific
baseConfig struct {
ProjectId string `toml:"project_id"`
Hostname string `toml:"-"`
Api api `toml:"api"`
Expand All @@ -135,6 +137,12 @@ type (
Experimental experimental `toml:"experimental" mapstructure:"-"`
}

config struct {
baseConfig
Overrides map[string]interface{} `toml:"remotes"`
Remotes map[string]baseConfig `toml:"-"`
}

api struct {
Enabled bool `toml:"enabled"`
Image string `toml:"-"`
Expand Down Expand Up @@ -438,6 +446,16 @@ type (
}
)

func (c *baseConfig) Clone() baseConfig {
copy := *c
copy.Storage.Buckets = maps.Clone(c.Storage.Buckets)
copy.Functions = maps.Clone(c.Functions)
copy.Auth.External = maps.Clone(c.Auth.External)
copy.Auth.Email.Template = maps.Clone(c.Auth.Email.Template)
copy.Auth.Sms.TestOTP = maps.Clone(c.Auth.Sms.TestOTP)
return copy
}

type ConfigEditor func(*config)

func WithHostname(hostname string) ConfigEditor {
Expand All @@ -447,7 +465,7 @@ func WithHostname(hostname string) ConfigEditor {
}

func NewConfig(editors ...ConfigEditor) config {
initial := config{
initial := config{baseConfig: baseConfig{
Hostname: "127.0.0.1",
Api: api{
Image: postgrestImage,
Expand Down Expand Up @@ -543,7 +561,7 @@ func NewConfig(editors ...ConfigEditor) config {
EdgeRuntime: edgeRuntime{
Image: edgeRuntimeImage,
},
}
}}
for _, apply := range editors {
apply(&initial)
}
Expand Down Expand Up @@ -587,15 +605,18 @@ func (c *config) Load(path string, fsys fs.FS) error {
if _, err := dec.Decode(c); err != nil {
return errors.Errorf("failed to decode config template: %w", err)
}
// Load user defined config
if metadata, err := toml.DecodeFS(fsys, builder.ConfigPath, c); err != nil {
cwd, osErr := os.Getwd()
if osErr != nil {
cwd = "current directory"
}
return errors.Errorf("cannot read config in %s: %w", cwd, err)
} else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
for _, key := range undecoded {
if key[0] != "remotes" {
fmt.Fprintf(os.Stderr, "Unknown config field: [%s]\n", key)
}
}
}
// Load secrets from .env file
if err := loadDefaultEnv(); err != nil {
Expand Down Expand Up @@ -685,10 +706,32 @@ func (c *config) Load(path string, fsys fs.FS) error {
}
c.Functions[slug] = function
}
return c.Validate()
if err := c.baseConfig.Validate(); err != nil {
return err
}
c.Remotes = make(map[string]baseConfig, len(c.Overrides))
for name, remote := range c.Overrides {
base := c.baseConfig.Clone()
// Encode a toml file with only config overrides
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(remote); err != nil {
return errors.Errorf("failed to encode map to TOML: %w", err)
}
// Decode overrides using base config as defaults
if metadata, err := toml.NewDecoder(&buf).Decode(&base); err != nil {
return errors.Errorf("failed to decode remote config: %w", err)
} else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
}
if err := base.Validate(); err != nil {
return err
}
c.Remotes[name] = base
}
return nil
}

func (c *config) Validate() error {
func (c *baseConfig) Validate() error {
if c.ProjectId == "" {
return errors.New("Missing required field in config: project_id")
} else if sanitized := sanitizeProjectId(c.ProjectId); sanitized != c.ProjectId {
Expand Down
35 changes: 35 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ func TestConfigParsing(t *testing.T) {
// Run test
assert.Error(t, config.Load("", fsys))
})

t.Run("config file with remotes", func(t *testing.T) {
config := NewConfig()
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
"supabase/templates/invite.html": &fs.MapFile{},
}
// Run test
t.Setenv("TWILIO_AUTH_TOKEN", "token")
t.Setenv("AZURE_CLIENT_ID", "hello")
t.Setenv("AZURE_SECRET", "this is cool")
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
t.Setenv("SENDGRID_API_KEY", "sendgrid")
assert.NoError(t, config.Load("", fsys))
// Check the default value in the config
assert.Equal(t, "http://127.0.0.1:3000", config.Auth.SiteUrl)
assert.Equal(t, true, config.Auth.EnableSignup)
assert.Equal(t, true, config.Auth.External["azure"].Enabled)
assert.Equal(t, []string{"image/png", "image/jpeg"}, config.Storage.Buckets["images"].AllowedMimeTypes)
// Check the values for remotes override
production, ok := config.Remotes["production"]
assert.True(t, ok)
staging, ok := config.Remotes["staging"]
assert.True(t, ok)
// Check the values for production override
assert.Equal(t, config.ProjectId, production.ProjectId)
assert.Equal(t, "http://feature-auth-branch.com/", production.Auth.SiteUrl)
assert.Equal(t, false, production.Auth.EnableSignup)
assert.Equal(t, false, production.Auth.External["azure"].Enabled)
assert.Equal(t, "nope", production.Auth.External["azure"].ClientId)
// Check the values for the staging override
assert.Equal(t, "staging-project", staging.ProjectId)
assert.Equal(t, []string{"image/png"}, staging.Storage.Buckets["images"].AllowedMimeTypes)
})
}

func TestFileSizeLimitConfigParsing(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions pkg/config/testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,17 @@ s3_region = "ap-southeast-1"
s3_access_key = ""
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = ""

[remotes.production.auth]
site_url = "http://feature-auth-branch.com/"
enable_signup = false

[remotes.production.auth.external.azure]
enabled = false
client_id = "nope"

[remotes.staging]
project_id = "staging-project"

[remotes.staging.storage.buckets.images]
allowed_mime_types = ["image/png"]

0 comments on commit 01256a1

Please sign in to comment.