Skip to content

Commit

Permalink
feat: add project unpause command
Browse files Browse the repository at this point in the history
Closes: #2715
  • Loading branch information
avallete committed Sep 30, 2024
1 parent b64a8c8 commit a842f7b
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 11 deletions.
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

0 comments on commit a842f7b

Please sign in to comment.