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
13 changes: 13 additions & 0 deletions pkg/api/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@ func (c *DeploymentClient) DeleteDebugService(ctx context.Context, serviceName s
return nil
}

func (c *DeploymentClient) PruneDebugSessions(ctx context.Context) error {
resp, err := c.httpClient.R().
SetContext(ctx).
Delete(c.baseURL + "/debug/services")
if err != nil {
return fmt.Errorf("%w: trace_id = %s", err, traceIDFromHTTPResponse(resp))
}
if resp.IsError() {
return NewErrorFromHTTPResponse(resp)
}
return nil
}

type statusResponse struct {
Ready bool `json:"ready"`
}
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 @@ -403,6 +403,80 @@ func TestDeleteDebugService(t *testing.T) {
}
}

func TestPruneDebugSessions(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-error",
mock: mock{
mockResponse: `{"error": {"code": 2003, "message": "not found", "traceId": "n/a", "containerLogs": ""}}`,
status: http.StatusNotFound,
},
want: want{
err: errors.New("API Error Encountered: ( HTTP status: 404 Error code: 2003 Detailed message: not found Trace ID: n/a )"),
},
},
{
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/debug/services",
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.PruneDebugSessions(t.Context())
if tt.want.err != nil {
require.EqualError(t, err, tt.want.err.Error())
httpmock.Reset()
return
}
require.NoError(t, err)
httpmock.Reset()
})
}
}

func TestGetServiceReadyStatus(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 @@ -43,6 +43,7 @@ type DeploymentInterface interface {
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
PruneDebugSessions(ctx context.Context) error
CreatePackage(ctx context.Context, createPackageArgs api.CreatePackageArgs) (api.CreatePackageResponse, error)
CreateProject(ctx context.Context, projectName string) (api.CreateProjectResponse, error)
DeployInstance(ctx context.Context, deployInstanceArgs api.DeployInstanceArgs) (api.DeployInstanceResponse, 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.

3 changes: 3 additions & 0 deletions vcr/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ func NewCmdDebug(f cmdutil.Factory) *cobra.Command {
cmd.Flags().IntVarP(&opts.DebuggerPort, "debugger-port", "d", defaultDebuggerPort, "Local port for debugger proxy server (default: 3001)")
cmd.Flags().BoolVarP(&opts.PreserveData, "preserve-data", "", false, "Keep debug session data after stopping (useful for debugging state issues)")
cmd.Flags().StringVarP(&opts.ManifestFile, "filename", "f", "", "Path to VCR manifest file (default: vcr.yml in project directory)")

cmd.AddCommand(NewCmdPruneSessions(f))

return cmd
}

Expand Down
46 changes: 46 additions & 0 deletions vcr/debug/prune_sessions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package debug

import (
"context"
"fmt"

"github.com/spf13/cobra"

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

type PruneSessionsOptions struct {
cmdutil.Factory
}

func NewCmdPruneSessions(f cmdutil.Factory) *cobra.Command {
opts := &PruneSessionsOptions{Factory: f}

cmd := &cobra.Command{
Use: "prune-sessions",
Short: "Remove all active debug sessions",
Long: "Remove all active debug sessions for the configured API key.",
RunE: func(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithDeadline(context.Background(), opts.Deadline())
defer cancel()
return runPruneSessions(ctx, opts)
},
}

return cmd
}

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

spinner := cmdutil.DisplaySpinnerMessageWithHandle(" Pruning debug sessions...")
err := opts.DeploymentClient().PruneDebugSessions(ctx)
spinner.Stop()
if err != nil {
return fmt.Errorf("failed to prune debug sessions: %w", err)
}

fmt.Fprintf(io.Out, "%s Debug sessions successfully pruned\n", c.SuccessIcon())
return nil
}
86 changes: 86 additions & 0 deletions vcr/debug/prune_sessions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package debug

import (
"bytes"
"errors"
"io"
"testing"

"github.com/cli/cli/v2/pkg/iostreams"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

"vonage-cloud-runtime-cli/testutil"
"vonage-cloud-runtime-cli/testutil/mocks"
)

func TestPruneSessions(t *testing.T) {
type mock struct {
pruneDebugSessionsTimes int
pruneDebugSessionsReturnErr error
}

type want struct {
errMsg string
stdout string
}

tests := []struct {
name string
mock mock
want want
}{
{
name: "happy-path",
mock: mock{
pruneDebugSessionsTimes: 1,
pruneDebugSessionsReturnErr: nil,
},
want: want{
stdout: "✓ Debug sessions successfully pruned\n",
},
},
{
name: "api-error",
mock: mock{
pruneDebugSessionsTimes: 1,
pruneDebugSessionsReturnErr: errors.New("api error"),
},
want: want{
errMsg: "failed to prune debug sessions: api error",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
deploymentMock := mocks.NewMockDeploymentInterface(ctrl)

deploymentMock.EXPECT().
PruneDebugSessions(gomock.Any()).
Times(tt.mock.pruneDebugSessionsTimes).
Return(tt.mock.pruneDebugSessionsReturnErr)

ios, _, stdout, _ := iostreams.Test()

f := testutil.DefaultFactoryMock(t, ios, nil, nil, nil, deploymentMock, nil, nil)

cmd := NewCmdPruneSessions(f)
cmd.SetArgs([]string{})
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)

err := cmd.Execute()
if tt.want.errMsg != "" {
require.Error(t, err)
require.Equal(t, tt.want.errMsg, err.Error())
return
}

require.NoError(t, err)
require.Equal(t, tt.want.stdout, stdout.String())
})
}
}
Loading