diff --git a/internal/link/link.go b/internal/link/link.go index b81f2a5d7..ea8af20a4 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) @@ -274,7 +274,10 @@ func checkRemoteProjectStatus(ctx context.Context, projectRef string) error { utils.CmdSuggestion = fmt.Sprintf("An admin must unpause it from the Supabase dashboard at %s", utils.Aqua(fmt.Sprintf("%s/project/%s", utils.GetSupabaseDashboardURL(), projectRef))) return errors.New(errProjectPaused) case api.V1ProjectResponseStatusACTIVEHEALTHY: - // Project is in the desired state, do nothing + // Update postgres image version to match remote + if version := resp.JSON200.Database.Version; len(version) > 0 { + return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys) + } default: fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status) } diff --git a/internal/link/link_test.go b/internal/link/link_test.go index c8a867f6b..a1bd02e52 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -54,7 +54,10 @@ 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). @@ -130,7 +133,10 @@ 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). @@ -175,7 +181,10 @@ 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). @@ -215,7 +224,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 +257,15 @@ 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) 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,7 +274,7 @@ 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) assert.Empty(t, apitest.ListUnmatchedRequests())