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

Follow-on for dismissed deployment support PR #2524

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,
dotNomad marked this conversation as resolved.
Show resolved Hide resolved
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
4 changes: 4 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 Down Expand Up @@ -105,6 +107,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