Skip to content

Commit

Permalink
fix: unmarshal remote override into base config
Browse files Browse the repository at this point in the history
  • Loading branch information
sweatybridge committed Jan 10, 2025
1 parent 9898451 commit 9eff642
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 69 deletions.
92 changes: 29 additions & 63 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ type (

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

realtime struct {
Expand Down Expand Up @@ -360,6 +359,7 @@ var (

invalidProjectId = regexp.MustCompile("[^a-zA-Z0-9_.-]+")
envPattern = regexp.MustCompile(`^env\((.*)\)$`)
refPattern = regexp.MustCompile(`^[a-z]{20}$`)
)

func (c *config) Eject(w io.Writer) error {
Expand Down Expand Up @@ -406,6 +406,19 @@ func (c *config) loadFromReader(v *viper.Viper, r io.Reader) error {
if err := v.MergeConfig(r); err != nil {
return errors.Errorf("failed to merge config: %w", err)
}
// Find [remotes.*] block to override base config
for name, remote := range v.GetStringMap("remotes") {
if m, ok := remote.(map[string]any); ok && m["project_id"] == c.ProjectId {
fmt.Fprintln(os.Stderr, "Loading remote override:", name)
// On remotes branches set seed as disabled by default
v.Set("db.seed.enabled", false)
// TODO: warn duplicate project_id in remotes
delete(m, "project_id")
if err := v.MergeConfigMap(m); err != nil {
return err
}
}
}
// Manually parse [functions.*] to empty struct for backwards compatibility
for key, value := range v.GetStringMap("functions") {
if m, ok := value.(map[string]any); ok && len(m) == 0 {
Expand Down Expand Up @@ -532,50 +545,11 @@ func (c *config) Load(path string, fsys fs.FS) error {
if version, err := fs.ReadFile(fsys, builder.PgmetaVersionPath); err == nil && len(version) > 0 {
c.Studio.PgmetaImage = replaceImageTag(pgmetaImage, string(version))
}
// Resolve remote config, then base config
idToName := map[string]string{}
c.Remotes = make(map[string]baseConfig, len(c.Overrides))
for name, remote := range c.Overrides {
base := c.baseConfig.Clone()
// On remotes branches set seed as disabled by default
base.Db.Seed.Enabled = false
// 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, "WARN: unknown config fields: %+v\n", undecoded)
}
// Cross validate remote project id
if base.ProjectId == c.baseConfig.ProjectId {
fmt.Fprintf(os.Stderr, "WARN: project_id is missing for [remotes.%s]\n", name)
} else if other, exists := idToName[base.ProjectId]; exists {
return errors.Errorf("duplicate project_id for [remotes.%s] and [remotes.%s]", other, name)
} else {
idToName[base.ProjectId] = name
}
if err := base.resolve(builder, fsys); err != nil {
return err
}
c.Remotes[name] = base
}
// TODO: replace derived config resolution with viper decode hooks
if err := c.baseConfig.resolve(builder, fsys); err != nil {
return err
}
// Validate base config, then remote config
if err := c.baseConfig.Validate(fsys); err != nil {
return err
}
for name, base := range c.Remotes {
if err := base.Validate(fsys); err != nil {
return errors.Errorf("invalid config for [remotes.%s]: %w", name, err)
}
}
return nil
return c.Validate(fsys)
}

func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error {
Expand Down Expand Up @@ -624,13 +598,19 @@ func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error {
return c.Db.Seed.loadSeedPaths(builder.SupabaseDirPath, fsys)
}

func (c *baseConfig) Validate(fsys fs.FS) error {
func (c *config) Validate(fsys fs.FS) error {
if c.ProjectId == "" {
return errors.New("Missing required field in config: project_id")
} else if sanitized := sanitizeProjectId(c.ProjectId); sanitized != c.ProjectId {
fmt.Fprintln(os.Stderr, "WARN: project_id field in config is invalid. Auto-fixing to", sanitized)
c.ProjectId = sanitized
}
// Since remote config is merged to base, we only need to validate the project_id field.
for name, remote := range c.Remotes {
if !refPattern.MatchString(remote.ProjectId) {
return errors.Errorf("Invalid config for remotes.%s.project_id. Must be like: abcdefghijklmnopqrst", name)
}
}
// Validate api config
if c.Api.Enabled {
if c.Api.Port == 0 {
Expand All @@ -641,7 +621,7 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
if c.Db.Settings.SessionReplicationRole != nil {
allowedRoles := []SessionReplicationRole{SessionReplicationRoleOrigin, SessionReplicationRoleReplica, SessionReplicationRoleLocal}
if !sliceContains(allowedRoles, *c.Db.Settings.SessionReplicationRole) {
return errors.Errorf("Invalid config for db.session_replication_role: %s. Must be one of: %v", *c.Db.Settings.SessionReplicationRole, allowedRoles)
return errors.Errorf("Invalid config for db.session_replication_role. Must be one of: %v", allowedRoles)
}
}
if c.Db.Port == 0 {
Expand Down Expand Up @@ -1329,24 +1309,10 @@ func (c *baseConfig) GetServiceImages() []string {

// Retrieve the final base config to use taking into account the remotes override
func (c *config) GetRemoteByProjectRef(projectRef string) (baseConfig, error) {
var result []string
// Iterate over all the config.Remotes
for name, remoteConfig := range c.Remotes {
// Check if there is one matching project_id
if remoteConfig.ProjectId == projectRef {
// Check for duplicate project IDs across remotes
result = append(result, name)
}
}
// If no matching remote config is found, return the base config
if len(result) == 0 {
return c.baseConfig, errors.Errorf("no remote found for project_id: %s", projectRef)
}
remote := c.Remotes[result[0]]
if len(result) > 1 {
return remote, errors.Errorf("multiple remotes %v have the same project_id: %s", result, projectRef)
}
return remote, nil
// Config must be loaded after setting config.ProjectID = "ref"
base := c.baseConfig.Clone()
base.ProjectId = projectRef
return base, nil
}

func ToTomlBytes(config any) ([]byte, error) {
Expand Down
10 changes: 5 additions & 5 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ func TestConfigParsing(t *testing.T) {
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, "vpefcjyosynxeiebfscx", 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 seed should be disabled by default for remote configs
assert.Equal(t, false, production.Db.Seed.Enabled)
// Check the values for the staging override
assert.Equal(t, "staging-project", staging.ProjectId)
assert.Equal(t, "bvikqvbczudanvggcord", staging.ProjectId)
assert.Equal(t, []string{"image/png"}, staging.Storage.Buckets["images"].AllowedMimeTypes)
assert.Equal(t, true, staging.Db.Seed.Enabled)
})
Expand Down Expand Up @@ -386,7 +386,7 @@ func TestLoadFunctionImportMap(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "test"
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/deno.json": &fs.MapFile{},
Expand All @@ -402,7 +402,7 @@ func TestLoadFunctionImportMap(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "test"
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/deno.jsonc": &fs.MapFile{},
Expand All @@ -418,7 +418,7 @@ func TestLoadFunctionImportMap(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "test"
project_id = "bvikqvbczudanvggcord"
[functions]
hello.import_map = "custom_import_map.json"
`)},
Expand Down
5 changes: 4 additions & 1 deletion pkg/config/testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ s3_access_key = ""
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = ""

[remotes.production]
project_id = "vpefcjyosynxeiebfscx"

[remotes.production.auth]
site_url = "http://feature-auth-branch.com/"
enable_signup = false
Expand All @@ -260,7 +263,7 @@ enabled = false
client_id = "nope"

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

[remotes.staging.db.seed]
enabled = true
Expand Down

0 comments on commit 9eff642

Please sign in to comment.