From a8e57c53c59bb35bb66f017de0b76389371be2ed Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Sat, 16 Nov 2024 18:12:21 +0800 Subject: [PATCH] fix: update postgres image if linked project is healthy --- internal/link/link.go | 8 ++- internal/link/link_test.go | 108 ++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/internal/link/link.go b/internal/link/link.go index b81f2a5d7..12fa59ca1 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -32,7 +32,7 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func( fmt.Fprintln(utils.GetDebugLogger(), err) } - if err := checkRemoteProjectStatus(ctx, projectRef); err != nil { + if err := checkRemoteProjectStatus(ctx, projectRef, fsys); err != nil { return err } @@ -254,7 +254,7 @@ func updatePoolerConfig(config api.SupavisorConfigResponse) { var errProjectPaused = errors.New("project is paused") -func checkRemoteProjectStatus(ctx context.Context, projectRef string) error { +func checkRemoteProjectStatus(ctx context.Context, projectRef string, fsys afero.Fs) error { resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to retrieve remote project status: %w", err) @@ -279,5 +279,9 @@ func checkRemoteProjectStatus(ctx context.Context, projectRef string) error { fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status) } + // Update postgres image version to match the remote project + if version := resp.JSON200.Database.Version; len(version) > 0 { + return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys) + } return nil } diff --git a/internal/link/link_test.go b/internal/link/link_test.go index c8a867f6b..28282da4f 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -51,19 +51,38 @@ func TestLinkCommand(t *testing.T) { // Flush pending mocks after test execution defer gock.OffAll() // Mock project status + postgres := api.V1DatabaseResponse{ + Host: utils.GetSupabaseDbHost(project), + Version: "15.1.0.117", + } gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project). Reply(200). - JSON(api.V1ProjectResponse{Status: api.V1ProjectResponseStatusACTIVEHEALTHY}) + JSON(api.V1ProjectResponse{ + Status: api.V1ProjectResponseStatusACTIVEHEALTHY, + Database: &postgres, + }) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/api-keys"). Reply(200). JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}}) // Link configs + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/database/postgres"). + Reply(200). + JSON(api.PostgresConfigResponse{}) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/postgrest"). Reply(200). JSON(api.V1PostgrestConfigResponse{}) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/auth"). + Reply(200). + JSON(api.AuthConfigResponse{}) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/storage"). + Reply(200). + JSON(api.StorageConfigResponse{}) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/config/database/pooler"). Reply(200). @@ -83,23 +102,6 @@ func TestLinkCommand(t *testing.T) { Get("/storage/v1/version"). Reply(200). BodyString("0.40.4") - postgres := api.V1DatabaseResponse{ - Host: utils.GetSupabaseDbHost(project), - Version: "15.1.0.117", - } - gock.New(utils.DefaultApiHost). - Get("/v1/projects"). - Reply(200). - JSON([]api.V1ProjectResponse{ - { - Id: project, - Database: &postgres, - OrganizationId: "combined-fuchsia-lion", - Name: "Test Project", - Region: "us-west-1", - CreatedAt: "2022-04-25T02:14:55.906498Z", - }, - }) // Run test err := Run(context.Background(), project, fsys, conn.Intercept) // Check error @@ -130,15 +132,27 @@ func TestLinkCommand(t *testing.T) { gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project). Reply(200). - JSON(api.V1ProjectResponse{Status: api.V1ProjectResponseStatusACTIVEHEALTHY}) + JSON(api.V1ProjectResponse{ + Status: api.V1ProjectResponseStatusACTIVEHEALTHY, + Database: &api.V1DatabaseResponse{}, + }) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/api-keys"). Reply(200). JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}}) // Link configs + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/database/postgres"). + ReplyError(errors.New("network error")) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/postgrest"). ReplyError(errors.New("network error")) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/auth"). + ReplyError(errors.New("network error")) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/storage"). + ReplyError(errors.New("network error")) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/config/database/pooler"). ReplyError(errors.New("network error")) @@ -152,9 +166,6 @@ func TestLinkCommand(t *testing.T) { gock.New("https://" + utils.GetSupabaseHost(project)). Get("/storage/v1/version"). ReplyError(errors.New("network error")) - gock.New(utils.DefaultApiHost). - Get("/v1/projects"). - ReplyError(errors.New("network error")) // Run test err := Run(context.Background(), project, fsys, func(cc *pgx.ConnConfig) { cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { @@ -175,15 +186,27 @@ func TestLinkCommand(t *testing.T) { gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project). Reply(200). - JSON(api.V1ProjectResponse{Status: api.V1ProjectResponseStatusACTIVEHEALTHY}) + JSON(api.V1ProjectResponse{ + Status: api.V1ProjectResponseStatusACTIVEHEALTHY, + Database: &api.V1DatabaseResponse{}, + }) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/api-keys"). Reply(200). JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}}) // Link configs + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/database/postgres"). + ReplyError(errors.New("network error")) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/postgrest"). ReplyError(errors.New("network error")) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/auth"). + ReplyError(errors.New("network error")) + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project + "/config/storage"). + ReplyError(errors.New("network error")) gock.New(utils.DefaultApiHost). Get("/v1/projects/" + project + "/config/database/pooler"). ReplyError(errors.New("network error")) @@ -215,7 +238,32 @@ func TestLinkCommand(t *testing.T) { func TestStatusCheck(t *testing.T) { project := "test-project" + t.Run("updates postgres version when healthy", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Flush pending mocks after test execution + defer gock.OffAll() + // Mock project status + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + project). + Reply(http.StatusOK). + JSON(api.V1ProjectResponse{ + Status: api.V1ProjectResponseStatusACTIVEHEALTHY, + Database: &api.V1DatabaseResponse{Version: "15.6.1.139"}, + }) + // Run test + err := checkRemoteProjectStatus(context.Background(), project, fsys) + // Check error + assert.NoError(t, err) + version, err := afero.ReadFile(fsys, utils.PostgresVersionPath) + assert.NoError(t, err) + assert.Equal(t, "15.6.1.139", string(version)) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + t.Run("ignores project not found", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Flush pending mocks after test execution defer gock.OffAll() // Mock project status @@ -223,13 +271,18 @@ func TestStatusCheck(t *testing.T) { Get("/v1/projects/" + project). Reply(http.StatusNotFound) // Run test - err := checkRemoteProjectStatus(context.Background(), project) + err := checkRemoteProjectStatus(context.Background(), project, fsys) // Check error assert.NoError(t, err) + exists, err := afero.Exists(fsys, utils.PostgresVersionPath) + assert.NoError(t, err) + assert.False(t, exists) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on project inactive", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Flush pending mocks after test execution defer gock.OffAll() // Mock project status @@ -238,9 +291,12 @@ func TestStatusCheck(t *testing.T) { Reply(http.StatusOK). JSON(api.V1ProjectResponse{Status: api.V1ProjectResponseStatusINACTIVE}) // Run test - err := checkRemoteProjectStatus(context.Background(), project) + err := checkRemoteProjectStatus(context.Background(), project, fsys) // Check error assert.ErrorIs(t, err, errProjectPaused) + exists, err := afero.Exists(fsys, utils.PostgresVersionPath) + assert.NoError(t, err) + assert.False(t, exists) assert.Empty(t, apitest.ListUnmatchedRequests()) }) } @@ -309,7 +365,7 @@ func TestLinkPostgrest(t *testing.T) { // Run test err := linkPostgrest(context.Background(), project) // Validate api - assert.ErrorIs(t, err, tenant.ErrAuthToken) + assert.ErrorContains(t, err, `unexpected API config status 500: {"message":"unavailable"}`) assert.Empty(t, apitest.ListUnmatchedRequests()) }) }