Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add project unpause command #2716

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/beta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,28 @@ paths:
- Auth
security:
- bearer: []
/v1/projects/{ref}/unpause:
post:
operationId: v1-unpause-a-project
summary: Unpause the given project
parameters:
- name: ref
required: true
in: path
description: Project ref
schema:
minLength: 20
maxLength: 20
type: string
responses:
'201':
description: ''
'403':
description: ''
tags:
- Projects
security:
- bearer: []
/v1/projects/{ref}/database/query:
post:
operationId: v1-run-a-query
Expand Down
39 changes: 38 additions & 1 deletion cmd/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ import (
"github.com/supabase/cli/internal/projects/create"
"github.com/supabase/cli/internal/projects/delete"
"github.com/supabase/cli/internal/projects/list"
"github.com/supabase/cli/internal/projects/unpause"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/api"
"golang.org/x/term"
)

// Custom filter to list only projects with status 'INACTIVE'
func filterInactiveProjects(items []api.V1ProjectResponse) []api.V1ProjectResponse {
var inactiveProjects []api.V1ProjectResponse
for _, project := range items {
if project.Status == api.V1ProjectResponseStatusINACTIVE {
inactiveProjects = append(inactiveProjects, project)
}
}
return inactiveProjects
}

var (
projectsCmd = &cobra.Command{
GroupID: groupManagementAPI,
Expand Down Expand Up @@ -113,7 +125,7 @@ var (
ctx := cmd.Context()
if len(args) == 0 {
title := "Which project do you want to delete?"
cobra.CheckErr(flags.PromptProjectRef(ctx, title))
cobra.CheckErr(flags.PromptProjectRef(ctx, title, nil))
} else {
flags.ProjectRef = args[0]
}
Expand All @@ -123,6 +135,30 @@ var (
return delete.Run(ctx, flags.ProjectRef, afero.NewOsFs())
},
}
projectsUnpauseCmd = &cobra.Command{
Use: "unpause <ref>",
Short: "Unpause a Supabase project",
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return cobra.ExactArgs(1)(cmd, args)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if len(args) == 0 {
title := "Which project do you want to unpause?"
cobra.CheckErr(flags.PromptProjectRef(ctx, title, filterInactiveProjects))
} else {
flags.ProjectRef = args[0]
}
if err := unpause.PreRun(ctx, flags.ProjectRef); err != nil {
return err
}
return unpause.Run(ctx, flags.ProjectRef)
},
}
)

func init() {
Expand All @@ -146,6 +182,7 @@ func init() {
projectsCmd.AddCommand(projectsDeleteCmd)
projectsCmd.AddCommand(projectsListCmd)
projectsCmd.AddCommand(projectsApiKeysCmd)
projectsCmd.AddCommand(projectsUnpauseCmd)
rootCmd.AddCommand(projectsCmd)
}

Expand Down
7 changes: 4 additions & 3 deletions internal/projects/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,19 @@ func Run(ctx context.Context, fsys afero.Fs) error {
}

if utils.OutputFormat.Value == utils.OutputPretty {
table := `LINKED|ORG ID|REFERENCE ID|NAME|REGION|CREATED AT (UTC)
|-|-|-|-|-|-|
table := `LINKED|ORG ID|REFERENCE ID|NAME|REGION|CREATED AT (UTC)|STATUS
|-|-|-|-|-|-|-|
`
for _, project := range projects {
table += fmt.Sprintf(
"|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n",
"|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|`%s`|\n",
formatBullet(project.Linked),
project.OrganizationId,
project.Id,
strings.ReplaceAll(project.Name, "|", "\\|"),
formatRegion(project.Region),
utils.FormatTimestamp(project.CreatedAt),
project.Status,
)
}
return list.RenderTable(table)
Expand Down
42 changes: 42 additions & 0 deletions internal/projects/unpause/unpause.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package unpause

import (
"context"
"fmt"
"net/http"

"github.com/go-errors/errors"
"github.com/supabase/cli/internal/utils"
)

func PreRun(ctx context.Context, ref string) error {
if err := utils.AssertProjectRefIsValid(ref); err != nil {
return err
}
title := fmt.Sprintf("Do you want to unpause project %s?", utils.Aqua(ref))
if shouldUnpause, err := utils.NewConsole().PromptYesNo(ctx, title, false); err != nil {
return err
} else if !shouldUnpause {
return errors.New(context.Canceled)
}
return nil
}

func Run(ctx context.Context, ref string) error {
resp, err := utils.GetSupabase().V1UnpauseAProjectWithResponse(ctx, ref)
if err != nil {
return errors.Errorf("failed to unpause project: %w", err)
}

switch resp.StatusCode() {
case http.StatusNotFound:
return errors.New("Project does not exist:" + utils.Aqua(ref))
case http.StatusCreated:
break
default:
return errors.Errorf("Failed to unpause project %s: %s", utils.Aqua(ref), string(resp.Body))
}

fmt.Println("Unpausing project: " + utils.Aqua(ref) + " it should be ready in a few minutes.\nRun: " + utils.Bold("supabase projects list") + " to see your projects status.")
return nil
}
78 changes: 78 additions & 0 deletions internal/projects/unpause/unpause_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package unpause

import (
"context"
"errors"
"net/http"
"testing"

"github.com/h2non/gock"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/supabase/cli/internal/testing/apitest"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/api"
"github.com/zalando/go-keyring"
)

func TestUnpauseCommand(t *testing.T) {
ref := apitest.RandomProjectRef()
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Mock credentials store
keyring.MockInit()

t.Run("unpause project", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(ref), 0644))
// Setup api mock
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + ref + "/unpause").
Reply(http.StatusCreated).
JSON(api.V1UnpauseAProjectResponse{})
// Run test
err := Run(context.Background(), ref)
// Check error
assert.NoError(t, err)
})

t.Run("throws error on network failure", func(t *testing.T) {
// Setup api mock
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + ref + "/unpause").
ReplyError(errors.New("network error"))
// Run test
err := Run(context.Background(), ref)
// Check error
assert.ErrorContains(t, err, "network error")
})

t.Run("throws error on project not found", func(t *testing.T) {
// Setup api mock
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + ref + "/unpause").
Reply(http.StatusNotFound)
// Run test
err := Run(context.Background(), ref)
// Check error
assert.ErrorContains(t, err, "Project does not exist:")
})

t.Run("throws error on service unavailable", func(t *testing.T) {
// Setup api mock
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + ref + "/unpause").
Reply(http.StatusServiceUnavailable)
// Run test
err := Run(context.Background(), ref)
// Check error
assert.ErrorContains(t, err, "Failed to unpause project")
})
}
13 changes: 9 additions & 4 deletions internal/utils/flags/project_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/api"
"golang.org/x/term"
)

Expand All @@ -29,21 +30,25 @@ func ParseProjectRef(ctx context.Context, fsys afero.Fs) error {
}
// Prompt as the last resort
if term.IsTerminal(int(os.Stdin.Fd())) {
return PromptProjectRef(ctx, "Select a project:")
return PromptProjectRef(ctx, "Select a project:", nil)
}
return errors.New(utils.ErrNotLinked)
}

func PromptProjectRef(ctx context.Context, title string) error {
func PromptProjectRef(ctx context.Context, title string, filterFunc func([]api.V1ProjectResponse) []api.V1ProjectResponse) error {
resp, err := utils.GetSupabase().V1ListAllProjectsWithResponse(ctx)
if err != nil {
return errors.Errorf("failed to retrieve projects: %w", err)
}
if resp.JSON200 == nil {
return errors.New("Unexpected error retrieving projects: " + string(resp.Body))
}
items := make([]utils.PromptItem, len(*resp.JSON200))
for i, project := range *resp.JSON200 {
projects := *resp.JSON200
if filterFunc != nil {
projects = filterFunc(projects)
}
items := make([]utils.PromptItem, len(projects))
for i, project := range projects {
items[i] = utils.PromptItem{
Summary: project.Id,
Details: fmt.Sprintf("name: %s, org: %s, region: %s", project.Name, project.OrganizationId, project.Region),
Expand Down
6 changes: 3 additions & 3 deletions internal/utils/flags/project_ref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestProjectPrompt(t *testing.T) {
OrganizationId: "test-org",
}})
// Run test
err := PromptProjectRef(context.Background(), "")
err := PromptProjectRef(context.Background(), "", nil)
// Check error
assert.ErrorContains(t, err, "failed to prompt choice:")
assert.Empty(t, apitest.ListUnmatchedRequests())
Expand All @@ -94,7 +94,7 @@ func TestProjectPrompt(t *testing.T) {
Get("/v1/projects").
ReplyError(errNetwork)
// Run test
err := PromptProjectRef(context.Background(), "")
err := PromptProjectRef(context.Background(), "", nil)
// Check error
assert.ErrorIs(t, err, errNetwork)
})
Expand All @@ -106,7 +106,7 @@ func TestProjectPrompt(t *testing.T) {
Get("/v1/projects").
Reply(http.StatusServiceUnavailable)
// Run test
err := PromptProjectRef(context.Background(), "")
err := PromptProjectRef(context.Background(), "", nil)
// Check error
assert.ErrorContains(t, err, "Unexpected error retrieving projects:")
})
Expand Down
Loading