From 8611ace19bc1fd62f5e4e96154402a208194940f Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Fri, 25 Oct 2024 07:15:10 +0200 Subject: [PATCH] feat(config): experimental config webhooks (#2794) --- pkg/config/config.go | 27 +++++-- pkg/config/updater.go | 16 ++++ pkg/config/updater_test.go | 153 +++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 5 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 7f0acf72c..7fc8942b5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -388,12 +388,17 @@ type ( VectorPort uint16 `toml:"vector_port"` } + webhooks struct { + Enabled bool `toml:"enabled"` + } + experimental struct { - OrioleDBVersion string `toml:"orioledb_version"` - S3Host string `toml:"s3_host"` - S3Region string `toml:"s3_region"` - S3AccessKey string `toml:"s3_access_key"` - S3SecretKey string `toml:"s3_secret_key"` + OrioleDBVersion string `toml:"orioledb_version"` + S3Host string `toml:"s3_host"` + S3Region string `toml:"s3_region"` + S3AccessKey string `toml:"s3_access_key"` + S3SecretKey string `toml:"s3_secret_key"` + Webhooks *webhooks `toml:"webhooks"` } ) @@ -986,6 +991,9 @@ func (c *baseConfig) Validate(fsys fs.FS) error { return errors.Errorf("Invalid config for analytics.backend. Must be one of: %v", allowed) } } + if err := c.Experimental.validateWebhooks(); err != nil { + return err + } return nil } @@ -1351,3 +1359,12 @@ func ToTomlBytes(config any) ([]byte, error) { } return buf.Bytes(), nil } + +func (e *experimental) validateWebhooks() error { + if e.Webhooks != nil { + if !e.Webhooks.Enabled { + return errors.Errorf("Webhooks cannot be deactivated. [experimental.webhooks] enabled can either be true or left undefined") + } + } + return nil +} diff --git a/pkg/config/updater.go b/pkg/config/updater.go index ac97cc63d..c7739eded 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -24,6 +24,9 @@ func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfi if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db); err != nil { return err } + if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental); err != nil { + return err + } return nil } @@ -87,3 +90,16 @@ func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c } return nil } + +func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental) error { + if exp.Webhooks != nil && exp.Webhooks.Enabled { + fmt.Fprintln(os.Stderr, "Enabling webhooks for the project...") + + if resp, err := u.client.V1EnableDatabaseWebhookWithResponse(ctx, projectRef); err != nil { + return errors.Errorf("failed to enable webhooks: %w", err) + } else if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { + return errors.Errorf("unexpected enable webhook status %d: %s", resp.StatusCode(), string(resp.Body)) + } + } + return nil +} diff --git a/pkg/config/updater_test.go b/pkg/config/updater_test.go index 241d612e9..2ad07c998 100644 --- a/pkg/config/updater_test.go +++ b/pkg/config/updater_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1API "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" ) func TestUpdateApi(t *testing.T) { @@ -63,3 +64,155 @@ func TestUpdateApi(t *testing.T) { assert.True(t, gock.IsDone()) }) } + +func TestUpdateDbConfig(t *testing.T) { + server := "http://localhost" + client, err := v1API.NewClientWithResponses(server) + require.NoError(t, err) + + t.Run("updates remote DB config", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Setup mock server + defer gock.Off() + gock.New(server). + Get("/v1/projects/test-project/config/database"). + Reply(http.StatusOK). + JSON(v1API.PostgresConfigResponse{}) + gock.New(server). + Put("/v1/projects/test-project/config/database"). + Reply(http.StatusOK). + JSON(v1API.PostgresConfigResponse{ + MaxConnections: cast.Ptr(cast.UintToInt(100)), + }) + // Run test + err := updater.UpdateDbConfig(context.Background(), "test-project", db{ + Settings: settings{ + MaxConnections: cast.Ptr(cast.IntToUint(100)), + }, + }) + // Check result + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) + + t.Run("skips update if no diff in DB config", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Setup mock server + defer gock.Off() + gock.New(server). + Get("/v1/projects/test-project/config/database"). + Reply(http.StatusOK). + JSON(v1API.PostgresConfigResponse{ + MaxConnections: cast.Ptr(cast.UintToInt(100)), + }) + // Run test + err := updater.UpdateDbConfig(context.Background(), "test-project", db{ + Settings: settings{ + MaxConnections: cast.Ptr(cast.IntToUint(100)), + }, + }) + // Check result + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) +} + +func TestUpdateExperimentalConfig(t *testing.T) { + server := "http://localhost" + client, err := v1API.NewClientWithResponses(server) + require.NoError(t, err) + + t.Run("enables webhooks", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Setup mock server + defer gock.Off() + gock.New(server). + Post("/v1/projects/test-project/database/webhooks/enable"). + Reply(http.StatusOK). + JSON(map[string]interface{}{}) + // Run test + err := updater.UpdateExperimentalConfig(context.Background(), "test-project", experimental{ + Webhooks: &webhooks{ + Enabled: true, + }, + }) + // Check result + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) + + t.Run("skips update if webhooks not enabled", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Run test + err := updater.UpdateExperimentalConfig(context.Background(), "test-project", experimental{ + Webhooks: &webhooks{ + Enabled: false, + }, + }) + // Check result + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) +} + +func TestUpdateRemoteConfig(t *testing.T) { + server := "http://localhost" + client, err := v1API.NewClientWithResponses(server) + require.NoError(t, err) + + t.Run("updates all configs", func(t *testing.T) { + updater := NewConfigUpdater(*client) + // Setup mock server + defer gock.Off() + // API config + gock.New(server). + Get("/v1/projects/test-project/postgrest"). + Reply(http.StatusOK). + JSON(v1API.PostgrestConfigWithJWTSecretResponse{}) + gock.New(server). + Patch("/v1/projects/test-project/postgrest"). + Reply(http.StatusOK). + JSON(v1API.PostgrestConfigWithJWTSecretResponse{ + DbSchema: "public", + MaxRows: 1000, + }) + // DB config + gock.New(server). + Get("/v1/projects/test-project/config/database"). + Reply(http.StatusOK). + JSON(v1API.PostgresConfigResponse{}) + gock.New(server). + Put("/v1/projects/test-project/config/database"). + Reply(http.StatusOK). + JSON(v1API.PostgresConfigResponse{ + MaxConnections: cast.Ptr(cast.UintToInt(100)), + }) + // Experimental config + gock.New(server). + Post("/v1/projects/test-project/database/webhooks/enable"). + Reply(http.StatusOK). + JSON(map[string]interface{}{}) + // Run test + err := updater.UpdateRemoteConfig(context.Background(), baseConfig{ + ProjectId: "test-project", + Api: api{ + Enabled: true, + Schemas: []string{"public", "private"}, + MaxRows: 1000, + }, + Db: db{ + Settings: settings{ + MaxConnections: cast.Ptr(cast.IntToUint(100)), + }, + }, + Experimental: experimental{ + Webhooks: &webhooks{ + Enabled: true, + }, + }, + }) + // Check result + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) +}