Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ lefthook-local.yml

# Node modules (for commitlint)
node_modules/

# Agent working files
docs/superpowers/
17 changes: 17 additions & 0 deletions pkg/api/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"

Expand Down Expand Up @@ -106,6 +107,22 @@ func (c *DeploymentClient) GenerateVonageApplicationKeys(ctx context.Context, ap
return nil
}

func (c *DeploymentClient) DeleteVonageApplication(ctx context.Context, appID string) error {
resp, err := c.httpClient.R().
SetContext(ctx).
Delete(c.baseURL + "/applications/" + url.PathEscape(appID))
if err != nil {
return fmt.Errorf("%w: trace_id = %s", err, traceIDFromHTTPResponse(resp))
}
Comment thread
gorkarevilla marked this conversation as resolved.
if resp.StatusCode() == http.StatusNotFound {
return ErrNotFound
}
Comment thread
gorkarevilla marked this conversation as resolved.
if resp.IsError() {
return NewErrorFromHTTPResponse(resp)
}
return nil
}
Comment thread
gorkarevilla marked this conversation as resolved.

type deployRequest struct {
Runtime string `json:"runtime"`
Region string `json:"region"`
Expand Down
74 changes: 74 additions & 0 deletions pkg/api/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,80 @@ func TestGenerateVonageApplicationKeys(t *testing.T) {
}
}

func TestDeleteVonageApplication(t *testing.T) {
client := resty.New()
httpmock.ActivateNonDefault(client.GetClient())
defer httpmock.DeactivateAndReset()

type mock struct {
mockResponse string
status int
}

type want struct {
err error
}

tests := []struct {
name string
mock mock
want want
}{
{
name: "204-happy-path",
mock: mock{
mockResponse: "",
status: http.StatusNoContent,
},
want: want{
err: nil,
},
},
{
name: "404-not-found",
mock: mock{
mockResponse: "",
status: http.StatusNotFound,
},
want: want{
err: ErrNotFound,
},
},
{
name: "500-error",
mock: mock{
mockResponse: `{"error": {"code": 1001, "message": "internal server error", "traceId": "n/a", "containerLogs": ""}}`,
status: http.StatusInternalServerError,
},
want: want{
err: errors.New("API Error Encountered: ( HTTP status: 500 Error code: 1001 Detailed message: internal server error Trace ID: n/a )"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

httpmock.RegisterResponder("DELETE", "https://example.com/v0.3/applications/application-id",
func(_ *http.Request) (*http.Response, error) {
resp := httpmock.NewStringResponse(tt.mock.status, tt.mock.mockResponse)
resp.Header.Set("Content-Type", "application/json")
return resp, nil
})

deploymentClient := NewDeploymentClient("https://example.com", "v0.3", client, nil)

err := deploymentClient.DeleteVonageApplication(t.Context(), "application-id")
if tt.want.err != nil {
require.EqualError(t, err, tt.want.err.Error())
httpmock.Reset()
return
}
require.NoError(t, err)
httpmock.Reset()
})
}
}

func TestDeployDebugService(t *testing.T) {
client := resty.New()
httpmock.ActivateNonDefault(client.GetClient())
Expand Down
1 change: 1 addition & 0 deletions pkg/cmdutil/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type DeploymentInterface interface {
CreateVonageApplication(ctx context.Context, name string, enableRTC, enableVoice, enableMessages bool) (api.CreateVonageApplicationOutput, error)
ListVonageApplications(ctx context.Context, filter string) (api.ListVonageApplicationsOutput, error)
GenerateVonageApplicationKeys(ctx context.Context, appID string) error
DeleteVonageApplication(ctx context.Context, appID string) error
DeployDebugService(ctx context.Context, region, applicationID, name string, caps api.Capabilities) (api.DeployResponse, error)
GetServiceReadyStatus(ctx context.Context, serviceName string) (bool, error)
DeleteDebugService(ctx context.Context, serviceName string, preserveData bool) error
Expand Down
42 changes: 28 additions & 14 deletions testutil/mocks/factory.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions vcr/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
createCmd "vonage-cloud-runtime-cli/vcr/app/create"
generatekeysCmd "vonage-cloud-runtime-cli/vcr/app/generatekeys"
listCmd "vonage-cloud-runtime-cli/vcr/app/list"
removeCmd "vonage-cloud-runtime-cli/vcr/app/remove"
)

func NewCmdApp(f cmdutil.Factory) *cobra.Command {
Expand All @@ -27,6 +28,7 @@ func NewCmdApp(f cmdutil.Factory) *cobra.Command {

AVAILABLE COMMANDS
create Create a new Vonage application
remove (rm) Remove a Vonage application
list (ls) List all Vonage applications in your account
generate-keys Generate new key pairs for an existing application

Expand All @@ -39,19 +41,23 @@ func NewCmdApp(f cmdutil.Factory) *cobra.Command {
# Create a new application with Voice and Messages capabilities
$ vcr app create --name my-app --voice --messages

# Remove an application
$ vcr app remove 12345678-1234-1234-1234-123456789abc

# Generate new keys for an existing application
$ vcr app generate-keys --app-id 12345678-1234-1234-1234-123456789abc

# List all applications
$ vcr app list

# List applications filtered by name
$ vcr app list --filter "production"

# Generate new keys for an existing application
$ vcr app generate-keys --app-id 12345678-1234-1234-1234-123456789abc
`),
}

cmd.AddCommand(listCmd.NewCmdAppList(f))
cmd.AddCommand(createCmd.NewCmdAppCreate(f))
cmd.AddCommand(generatekeysCmd.NewCmdAppGenerateKeys(f))
cmd.AddCommand(listCmd.NewCmdAppList(f))
cmd.AddCommand(removeCmd.NewCmdAppRemove(f))
return cmd
}
90 changes: 90 additions & 0 deletions vcr/app/remove/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package remove

import (
"context"
"errors"
"fmt"

"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"

"vonage-cloud-runtime-cli/pkg/api"
"vonage-cloud-runtime-cli/pkg/cmdutil"
)

type Options struct {
cmdutil.Factory

ApplicationID string
SkipPrompts bool
}

func NewCmdAppRemove(f cmdutil.Factory) *cobra.Command {
opts := Options{
Factory: f,
}

cmd := &cobra.Command{
Use: "remove <applicationID>",
Aliases: []string{"rm"},
Short: "Remove a Vonage application",
Long: heredoc.Doc(`Remove a Vonage application from your account.

This command permanently removes a Vonage application and its associated
credentials. Any VCR instances linked to this application will lose their
authentication credentials on next restart.

WARNING: This action is irreversible. Make sure no running instances
depend on this application before removing it.
`),
Example: heredoc.Doc(`
# Remove an application (will prompt for confirmation)
$ vcr app remove 12345678-1234-1234-1234-123456789abc

# Remove without confirmation prompt (useful for CI/CD)
$ vcr app remove 12345678-1234-1234-1234-123456789abc --yes

# Using the short alias
$ vcr app rm 12345678-1234-1234-1234-123456789abc --yes
`),
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
opts.ApplicationID = args[0]

ctx, cancel := context.WithDeadline(context.Background(), opts.Deadline())
defer cancel()

return runRemove(ctx, &opts)
},
}

cmd.Flags().BoolVarP(&opts.SkipPrompts, "yes", "y", false, "Skip confirmation prompt (use with caution)")

return cmd
}

func runRemove(ctx context.Context, opts *Options) error {
io := opts.IOStreams()
c := io.ColorScheme()

if io.CanPrompt() && !opts.SkipPrompts {
if !opts.Survey().AskYesNo(fmt.Sprintf("Are you sure you want to remove application %q?", opts.ApplicationID)) {
fmt.Fprintf(io.ErrOut, "%s Application removal aborted\n", c.WarningIcon())
return nil
}
}

spinner := cmdutil.DisplaySpinnerMessageWithHandle(fmt.Sprintf(" Removing application %q...", opts.ApplicationID))
err := opts.DeploymentClient().DeleteVonageApplication(ctx, opts.ApplicationID)
spinner.Stop()
if err != nil {
if errors.Is(err, api.ErrNotFound) {
return fmt.Errorf("application %q could not be found or may have already been deleted", opts.ApplicationID)
}
return fmt.Errorf("failed to remove application: %w", err)
}

fmt.Fprintf(io.Out, "%s Application %q successfully removed\n", c.SuccessIcon(), opts.ApplicationID)

return nil
}
Loading
Loading