Skip to content

Commit

Permalink
Update to publisher log display, unit tests on cancel API (with small…
Browse files Browse the repository at this point in the history
… fix) and spelling change of "cancelling" to "canceling"
  • Loading branch information
sagerb committed Jan 10, 2025
1 parent 905c2f6 commit 21c0b1e
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 26 deletions.
2 changes: 1 addition & 1 deletion extensions/vscode/src/api/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ export interface PublishFailure extends EventStreamMessage {
data: {
dashboardUrl: string;
url: string;
canceled?: string; // not defined if not user cancelled. Value of "true" if true.
canceled?: string; // not defined if not user canceled. Value of "true" if true.
// and other non-defined attributes
};
error: string; // translated internally
Expand Down
11 changes: 7 additions & 4 deletions extensions/vscode/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export function displayEventStreamMessage(msg: EventStreamMessage): string {
if (msg.data.dashboardUrl) {
return `Deployment failed, click to view Connect logs: ${msg.data.dashboardUrl}`;
}
if (msg.data.canceled === "true") {
return "Deployment canceled";
}
return "Deployment failed";
}
if (msg.error !== undefined) {
Expand All @@ -95,8 +98,8 @@ export class EventStream extends Readable implements Disposable {
private messages: EventStreamMessage[] = [];
// Map to store event callbacks
private callbacks: Map<string, EventStreamRegistration[]> = new Map();
// Cancelled Event Streams - Suppressed when received
private cancelledLocalIDs: string[] = [];
// Canceled Event Streams - Suppressed when received
private canceledLocalIDs: string[] = [];

/**
* Creates a new instance of the EventStream class.
Expand Down Expand Up @@ -170,12 +173,12 @@ export class EventStream extends Readable implements Disposable {
* @returns undefined
*/
public suppressMessages(localId: string) {
this.cancelledLocalIDs.push(localId);
this.canceledLocalIDs.push(localId);
}

private processMessage(msg: EventStreamMessage) {
const localId = msg.data.localId;
if (localId && this.cancelledLocalIDs.includes(localId)) {
if (localId && this.canceledLocalIDs.includes(localId)) {
// suppress and ignore
return;
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/multiStepInputs/newCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function newCredential(
totalSteps: -1,
data: {
// each attribute is initialized to undefined
// to be returned when it has not been cancelled
// to be returned when it has not been canceled
url: startingServerUrl, // eventual type is string
apiKey: <string | undefined>undefined, // eventual type is string
name: <string | undefined>undefined, // eventual type is string
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/multiStepInputs/newDeployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export async function newDeployment(
title: "Select Entrypoint File (main file for your project)",
});
if (!fileUris || !fileUris[0]) {
// cancelled.
// canceled.
continue;
}
const fileUri = fileUris[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export async function selectNewOrExistingConfig(
totalSteps: -1,
data: {
// each attribute is initialized to undefined
// to be returned when it has not been cancelled to assist type guards
// to be returned when it has not been canceled to assist type guards
// Note: We can't initialize existingConfigurationName to a specific initial
// config, as we then wouldn't be able to detect if the user hit ESC to exit
// the selection. :-(
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/views/deployProgress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function deployProject(
url: "",
// and other non-defined attributes
localId: localID,
cancelled: "true",
canceled: "true",
message:
"Deployment has been dismissed (but will continue to be processed on the Connect Server).",
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/views/homeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
activeConfig.configuration.environment,
);
if (name === undefined) {
// Cancelled by the user
// Canceled by the user
return;
}

Expand Down
23 changes: 18 additions & 5 deletions extensions/vscode/src/views/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ enum LogStageStatus {
inProgress,
completed,
failed,
canceled,
}

type LogStage = {
Expand Down Expand Up @@ -182,14 +183,17 @@ export class LogsTreeDataProvider implements TreeDataProvider<LogsTreeItem> {
});

this.stream.register("publish/failure", async (msg: EventStreamMessage) => {
this.publishingStage.status = LogStageStatus.failed;
const failedOrCanceledStatus = msg.data.canceled
? LogStageStatus.canceled
: LogStageStatus.failed;
this.publishingStage.status = failedOrCanceledStatus;
this.publishingStage.events.push(msg);

this.stages.forEach((stage) => {
if (stage.status === LogStageStatus.notStarted) {
stage.status = LogStageStatus.neverStarted;
} else if (stage.status === LogStageStatus.inProgress) {
stage.status = LogStageStatus.failed;
stage.status = failedOrCanceledStatus;
}
});

Expand All @@ -204,8 +208,8 @@ export class LogsTreeDataProvider implements TreeDataProvider<LogsTreeItem> {
errorMessage = handleEventCodedError(msg);
} else {
errorMessage =
msg.data.cancelled === "true"
? `Deployment cancelled: ${msg.data.message}`
msg.data.canceled === "true"
? `Deployment canceled: ${msg.data.message}`
: `Deployment failed: ${msg.data.message}`;
}
const selection = await window.showErrorMessage(errorMessage, ...options);
Expand Down Expand Up @@ -259,7 +263,11 @@ export class LogsTreeDataProvider implements TreeDataProvider<LogsTreeItem> {
(msg: EventStreamMessage) => {
const stage = this.stages.get(stageName);
if (stage) {
stage.status = LogStageStatus.failed;
if (msg.data.canceled === "true") {
stage.status = LogStageStatus.canceled;
} else {
stage.status = LogStageStatus.failed;
}
stage.events.push(msg);
}
this.refresh();
Expand Down Expand Up @@ -413,6 +421,11 @@ export class LogsTreeStageItem extends TreeItem {
this.iconPath = new ThemeIcon("error");
this.collapsibleState = TreeItemCollapsibleState.Expanded;
break;
case LogStageStatus.canceled:
this.label = this.stage.inactiveLabel;
this.iconPath = new ThemeIcon("circle-slash");
this.collapsibleState = TreeItemCollapsibleState.Expanded;
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ const lastStatusDescription = computed(() => {
: "Not Yet Deployed";
}
if (isAbortedContentRecord.value) {
return "Last Deployment Cancelled";
return "Last Deployment Canceled";
}
return "Last Deployment Successful";
});
Expand Down
2 changes: 1 addition & 1 deletion internal/deployment/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (d *Deployment) WriteFile(
return existingDeployment, nil
}
if existingDeployment.AbortedAt != "" {
log.Debug("Skipping deployment record update since deployment has been cancelled")
log.Debug("Skipping deployment record update since deployment has been canceled")
return existingDeployment, nil
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/publish/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ func (p *defaultPublisher) writeDeploymentRecord(forceUpdate bool) (*deployment.

func CancelDeployment(
deploymentPath util.AbsolutePath,
localID state.LocalDeploymentID,
localID string,
log logging.Logger,
) (*deployment.Deployment, error) {
// This function only marks the deployment record as being cancelled.
// This function only marks the deployment record as being canceled.
// It does not cancel the anonymous function which is publishing to the server
// This is because the server API does not support cancellation at this time.

Expand All @@ -240,7 +240,7 @@ func CancelDeployment(
target.AbortedAt = time.Now().Format(time.RFC3339)

// Possibly update the deployment file
d, err := target.WriteFile(deploymentPath, target.LocalID, false, log)
d, err := target.WriteFile(deploymentPath, localID, false, log)
return d, err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/services/api/api_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func RouterHandlerFunc(base util.AbsolutePath, lister accounts.AccountList, log
Methods(http.MethodPost)

// POST /api/deployments/$NAME/cancel/$LOCALID cancels a deployment
r.Handle(ToPath("deployments", "{name}", "cancel", "{localid}"), PostCancelDeploymentHandlerFunc(base, log)).
r.Handle(ToPath("deployments", "{name}", "cancel", "{localid}"), PostDeploymentCancelHandlerFunc(base, log)).
Methods(http.MethodPost)

// DELETE /api/deployments/$NAME
Expand Down
8 changes: 8 additions & 0 deletions internal/services/api/deployment_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type preDeploymentDTO struct {
ServerURL string `json:"serverUrl"`
SaveName string `json:"saveName"`
CreatedAt string `json:"createdAt"`
LocalID string `toml:"local_id,omitempty" json:"localId"`
AbortedAt string `toml:"aborted_at,omitempty" json:"abortedAt"`
ConfigName string `json:"configurationName,omitempty"`
ConfigPath string `json:"configurationPath,omitempty"`
Error *types.AgentError `json:"deploymentError,omitempty"`
Expand All @@ -46,6 +48,8 @@ type fullDeploymentDTO struct {
deployment.Deployment
ConfigPath string `json:"configurationPath"`
SaveName string `json:"saveName"`
LocalID string `toml:"local_id,omitempty" json:"localId"`
AbortedAt string `toml:"aborted_at,omitempty" json:"abortedAt"`
}

type deploymentErrorDTO struct {
Expand Down Expand Up @@ -88,6 +92,8 @@ func deploymentAsDTO(d *deployment.Deployment, err error, projectDir util.Absolu
Deployment: *d,
ConfigPath: configPath,
SaveName: saveName, // TODO: remove this duplicate (remove frontend references first)
AbortedAt: d.AbortedAt,
LocalID: d.LocalID,
}
} else {
if d.ConfigName != "" {
Expand All @@ -105,6 +111,8 @@ func deploymentAsDTO(d *deployment.Deployment, err error, projectDir util.Absolu
ServerURL: d.ServerURL,
SaveName: saveName, // TODO: remove this duplicate (remove frontend references first)
CreatedAt: d.CreatedAt,
AbortedAt: d.AbortedAt,
LocalID: d.LocalID,
ConfigName: d.ConfigName,
ConfigPath: configPath,
Error: d.Error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import (
"github.com/posit-dev/publisher/internal/deployment"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/publish"
"github.com/posit-dev/publisher/internal/state"
"github.com/posit-dev/publisher/internal/util"
)

func PostCancelDeploymentHandlerFunc(base util.AbsolutePath, log logging.Logger) http.HandlerFunc {
func PostDeploymentCancelHandlerFunc(base util.AbsolutePath, log logging.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
name := mux.Vars(req)["name"]
localId := mux.Vars(req)["localid"]
Expand All @@ -26,7 +25,7 @@ func PostCancelDeploymentHandlerFunc(base util.AbsolutePath, log logging.Logger)
return
}
path := deployment.GetDeploymentPath(projectDir, name)
latest, err := publish.CancelDeployment(path, state.LocalDeploymentID(localId), log)
latest, err := publish.CancelDeployment(path, localId, log)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
http.NotFound(w, req)
Expand Down
124 changes: 124 additions & 0 deletions internal/services/api/post_deployment_cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (C) 2024 by Posit Software, PBC.

package api

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
"github.com/posit-dev/publisher/internal/deployment"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/util"
"github.com/posit-dev/publisher/internal/util/utiltest"
"github.com/spf13/afero"
"github.com/stretchr/testify/suite"
)

type PostDeploymentCancelTestSuite struct {
utiltest.Suite
log logging.Logger
fs afero.Fs
cwd util.AbsolutePath
}

func TestPostDeploymentCancelTestSuite(t *testing.T) {
suite.Run(t, new(PostDeploymentCancelTestSuite))
}

func (s *PostDeploymentCancelTestSuite) SetupSuite() {
s.log = logging.New()
}

func (s *PostDeploymentCancelTestSuite) SetupTest() {
s.fs = afero.NewMemMapFs()
cwd, err := util.Getwd(s.fs)
s.Nil(err)
s.cwd = cwd
s.cwd.MkdirAll(0700)
}

func (s *PostDeploymentCancelTestSuite) Test200WithLocalIDMatch() {
deploymentName := "newDeployment"
localId := "abc"

d := deployment.New()
d.LocalID = localId

_, err := d.WriteFile(deployment.GetDeploymentPath(s.cwd, deploymentName), "abc", true, s.log)
s.NoError(err)

h := PostDeploymentCancelHandlerFunc(s.cwd, s.log)

rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/deployments/"+deploymentName+"/cancel/"+localId, nil)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{
"name": deploymentName,
"localid": localId,
})
h(rec, req)

s.Equal(http.StatusOK, rec.Result().StatusCode)
s.Equal("application/json", rec.Header().Get("content-type"))

res := preDeploymentDTO{}
dec := json.NewDecoder(rec.Body)
dec.DisallowUnknownFields()
s.NoError(dec.Decode(&res))

s.NotEmpty(res.AbortedAt)
}

func (s *PostDeploymentCancelTestSuite) Test200WithoutLocalIDMatch() {
deploymentName := "newDeployment"

d := deployment.New()
d.LocalID = "abc"

_, err := d.WriteFile(deployment.GetDeploymentPath(s.cwd, deploymentName), "abc", true, s.log)
s.NoError(err)

h := PostDeploymentCancelHandlerFunc(s.cwd, s.log)

rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/deployments/"+deploymentName+"/cancel/xyz", nil)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{
"name": deploymentName,
"localid": "xyz", // not current localID in file
})
h(rec, req)

s.Equal(http.StatusOK, rec.Result().StatusCode)
s.Equal("application/json", rec.Header().Get("content-type"))

res := preDeploymentDTO{}
dec := json.NewDecoder(rec.Body)
dec.DisallowUnknownFields()
s.NoError(dec.Decode(&res))

// request was successful but not applied
s.Empty(res.AbortedAt)
}

func (s *PostDeploymentCancelTestSuite) Test404() {
deploymentName := "newDeployment"
localId := "abc"

h := PostDeploymentCancelHandlerFunc(s.cwd, s.log)

rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/deployments/"+deploymentName+"/cancel/"+localId, nil)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{
"name": deploymentName,
"localid": localId,
})
h(rec, req)

s.Equal(http.StatusNotFound, rec.Result().StatusCode)
s.Equal("text/plain; charset=utf-8", rec.Header().Get("content-type"))
}
Loading

0 comments on commit 21c0b1e

Please sign in to comment.