From 536215bbf84443d1174cf1a58f2875ff8def97e1 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 11:41:40 -0800 Subject: [PATCH 1/8] Add cannot backup file error code --- internal/types/error.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/types/error.go b/internal/types/error.go index 736e35bfa..d1a7f2104 100644 --- a/internal/types/error.go +++ b/internal/types/error.go @@ -20,6 +20,7 @@ const ( ErrorInvalidConfigFiles ErrorCode = "invalidConfigFiles" ErrorCredentialServiceUnavailable ErrorCode = "credentialsServiceUnavailable" ErrorCredentialsCorrupted ErrorCode = "credentialsCorrupted" + ErrorCredentialsCannotBackupFile ErrorCode = "credentialsCannotBackupFile" ErrorCertificateVerification ErrorCode = "errorCertificateVerification" ErrorRenvPackageVersionMismatch ErrorCode = "renvPackageVersionMismatch" ErrorRenvPackageSourceMissing ErrorCode = "renvPackageSourceMissing" From c77bfe7762c984a90c4e00d56347715b3408cfc5 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 11:42:02 -0800 Subject: [PATCH 2/8] Add credential backup file agent error helper --- internal/credentials/errors.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/credentials/errors.go b/internal/credentials/errors.go index 8e0536923..628f4f3f9 100644 --- a/internal/credentials/errors.go +++ b/internal/credentials/errors.go @@ -2,7 +2,11 @@ package credentials -import "fmt" +import ( + "fmt" + + "github.com/posit-dev/publisher/internal/types" +) type CorruptedError struct { GUID string @@ -99,3 +103,11 @@ func NewIncompleteCredentialError() *IncompleteCredentialError { func (e *IncompleteCredentialError) Error() string { return "New credentials require non-empty Name, URL and Api Key fields" } + +func NewBackupFileAgentError(filename string, err error) *types.AgentError { + details := types.ErrorCredentialsCannotBackupFileDetails{ + Filename: filename, + Message: fmt.Sprintf("failed to backup credentials to %s: %v", filename, err.Error()), + } + return types.NewAgentError(types.ErrorCredentialsCannotBackupFile, err, details) +} From 93f7a5df09fe12448f076519b0f02eae837494af Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 11:42:22 -0800 Subject: [PATCH 3/8] Send up BackupFileAgentError when backupFile fails --- internal/credentials/file.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/credentials/file.go b/internal/credentials/file.go index 253548e27..0cf428be5 100644 --- a/internal/credentials/file.go +++ b/internal/credentials/file.go @@ -231,20 +231,20 @@ func (c *fileCredentialsService) backupFile() (string, error) { if os.IsNotExist(err) { file, err := credsCopyPath.Create() if err != nil { - return "", err + return "", NewBackupFileAgentError(credsCopyPath.String(), err) } file.Close() } credsCopyFile, err := credsCopyPath.OpenFile(os.O_TRUNC|os.O_RDWR, 0644) if err != nil { - return "", err + return "", NewBackupFileAgentError(credsCopyPath.String(), err) } defer credsCopyFile.Close() credsFile, err := c.credsFilepath.Open() if err != nil { - return "", err + return "", NewBackupFileAgentError(credsCopyPath.String(), err) } defer credsFile.Close() From ae373df4582e0211d83d3d8e0fbe160f36ba1afa Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 11:44:27 -0800 Subject: [PATCH 4/8] Respond with credential backup file error in API --- internal/services/api/reset_credentials.go | 11 +++++++++ .../services/api/reset_credentials_test.go | 16 +++++++++++++ internal/types/api_errors.go | 24 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/internal/services/api/reset_credentials.go b/internal/services/api/reset_credentials.go index bd46ebb71..877ae88bd 100644 --- a/internal/services/api/reset_credentials.go +++ b/internal/services/api/reset_credentials.go @@ -23,6 +23,12 @@ func unavailableCredsRes(w http.ResponseWriter, err error) { apiErr.JSONResponse(w) } +func cannotBackupFileRes(w http.ResponseWriter, err error) { + agentErr := types.AsAgentError(err) + apiErr := types.APIErrorCredentialsBackupFileFromAgentError(*agentErr) + apiErr.JSONResponse(w) +} + func ResetCredentialsHandlerFunc(log logging.Logger, credserviceFactory credentials.CredServiceFactory) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { result := struct { @@ -37,6 +43,11 @@ func ResetCredentialsHandlerFunc(log logging.Logger, credserviceFactory credenti backupFile, err := cs.Reset() if err != nil { + var agentErr *types.AgentError + if errors.As(err, &agentErr) && agentErr.Code == types.ErrorCredentialsCannotBackupFile { + cannotBackupFileRes(w, err) + return + } unavailableCredsRes(w, err) return } diff --git a/internal/services/api/reset_credentials_test.go b/internal/services/api/reset_credentials_test.go index 71ad2098e..471d8809a 100644 --- a/internal/services/api/reset_credentials_test.go +++ b/internal/services/api/reset_credentials_test.go @@ -89,6 +89,22 @@ func (s *ResetCredsSuite) TestReset_EvenWithCorruptedError() { s.Equal(http.StatusOK, rec.Result().StatusCode) } +func (s *ResetCredsSuite) TestReset_BackupFileError() { + path := "http://example.com/api/credentials" + req, err := http.NewRequest("DELETE", path, nil) + s.NoError(err) + + s.credservice.On("Reset").Return("", credentials.NewBackupFileAgentError("~/.connect-creds", errors.New("do not have write permissions"))) + + rec := httptest.NewRecorder() + h := ResetCredentialsHandlerFunc(s.log, s.credsFactory) + h(rec, req) + + bodyRes := rec.Body.String() + s.Equal(http.StatusInternalServerError, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"credentialsCannotBackupFile","details":{"filename":"~/.connect-creds","message":"failed to backup credentials to ~/.connect-creds: do not have write permissions"}}`) +} + func (s *ResetCredsSuite) TestReset_UnknownError() { path := "http://example.com/api/credentials" req, err := http.NewRequest("DELETE", path, nil) diff --git a/internal/types/api_errors.go b/internal/types/api_errors.go index 57c40f1e3..5a40712c9 100644 --- a/internal/types/api_errors.go +++ b/internal/types/api_errors.go @@ -176,6 +176,30 @@ func APIErrorCredentialsCorruptedFromAgentError(aerr AgentError) APIErrorCredent } } +type ErrorCredentialsCannotBackupFileDetails struct { + Filename string `json:"filename"` + Message string `json:"message"` +} + +type APIErrorCredentialsBackupFileError struct { + Code ErrorCode `json:"code"` + Details ErrorCredentialsCannotBackupFileDetails `json:"details"` +} + +func (apierr *APIErrorCredentialsBackupFileError) JSONResponse(w http.ResponseWriter) { + jsonResult(w, http.StatusInternalServerError, apierr) +} + +func APIErrorCredentialsBackupFileFromAgentError(aerr AgentError) APIErrorCredentialsBackupFileError { + return APIErrorCredentialsBackupFileError{ + Code: ErrorCredentialsCannotBackupFile, + Details: ErrorCredentialsCannotBackupFileDetails{ + Filename: aerr.Data["Filename"].(string), + Message: aerr.Data["Message"].(string), + }, + } +} + type APIErrorPythonExecNotFound struct { Code ErrorCode `json:"code"` } From a08557b2216fc523d53318ee8af00bfab8a84adf Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 13:01:17 -0800 Subject: [PATCH 5/8] Show backup failure error msg in extension --- .../vscode/src/api/resources/Credentials.ts | 1 + extensions/vscode/src/state.ts | 6 ++++++ extensions/vscode/src/utils/errorTypes.ts | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/extensions/vscode/src/api/resources/Credentials.ts b/extensions/vscode/src/api/resources/Credentials.ts index b5086e2d8..363b9ef03 100644 --- a/extensions/vscode/src/api/resources/Credentials.ts +++ b/extensions/vscode/src/api/resources/Credentials.ts @@ -48,6 +48,7 @@ export class Credentials { // Returns: // 204 - success (no response) + // 500 - internal server error cannot backup file // 503 - credentials service unavailable reset() { return this.client.delete<{ backupFile: string }>(`credentials`); diff --git a/extensions/vscode/src/state.ts b/extensions/vscode/src/state.ts index 06754a4e2..0280ff576 100644 --- a/extensions/vscode/src/state.ts +++ b/extensions/vscode/src/state.ts @@ -22,6 +22,8 @@ import { import { isErrCredentialsCorrupted, errCredentialsCorruptedMessage, + isErrCannotBackupCredentialsFile, + errCannotBackupCredentialsFileMessage, } from "src/utils/errorTypes"; import { DeploymentSelector, SelectionState } from "src/types/shared"; import { LocalState, Views } from "./constants"; @@ -308,6 +310,10 @@ export class PublisherState implements Disposable { const listResponse = await api.credentials.list(); this.credentials = listResponse.data; } catch (err: unknown) { + if (isErrCannotBackupCredentialsFile(err)) { + window.showErrorMessage(errCannotBackupCredentialsFileMessage(err)); + return; + } const summary = getSummaryStringFromError("resetCredentials", err); window.showErrorMessage(summary); } diff --git a/extensions/vscode/src/utils/errorTypes.ts b/extensions/vscode/src/utils/errorTypes.ts index a39580215..013f59567 100644 --- a/extensions/vscode/src/utils/errorTypes.ts +++ b/extensions/vscode/src/utils/errorTypes.ts @@ -18,6 +18,7 @@ export type ErrorCode = | "tomlValidationError" | "tomlUnknownError" | "pythonExecNotFound" + | "credentialsCannotBackupFile" | "credentialsCorrupted"; export type axiosErrorWithJson = @@ -176,6 +177,21 @@ export const errCredentialsCorruptedMessage = (backupFile: string) => { return msg; }; +// Unable to backup credentials file +export type ErrCannotBackupCredentialsFile = MkErrorDataType< + "credentialsCannotBackupFile", + { filename: string; message: string } +>; +export const isErrCannotBackupCredentialsFile = + mkErrorTypeGuard( + "credentialsCannotBackupFile", + ); +export const errCannotBackupCredentialsFileMessage = ( + err: axiosErrorWithJson, +) => { + return err.response.data.details.message; +}; + // Tries to match an Axios error that comes with an identifiable Json structured data // defaulting to be ErrUnknown message when export function resolveAgentJsonErrorMsg(err: axiosErrorWithJson) { From 28166a163c37eb6e9fb658cd82a3347ac060b8f4 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 13:01:42 -0800 Subject: [PATCH 6/8] Use bad request status to avoid decoding error --- internal/services/api/reset_credentials_test.go | 4 ++-- internal/types/api_errors.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/services/api/reset_credentials_test.go b/internal/services/api/reset_credentials_test.go index 471d8809a..aa8206f1f 100644 --- a/internal/services/api/reset_credentials_test.go +++ b/internal/services/api/reset_credentials_test.go @@ -101,8 +101,8 @@ func (s *ResetCredsSuite) TestReset_BackupFileError() { h(rec, req) bodyRes := rec.Body.String() - s.Equal(http.StatusInternalServerError, rec.Result().StatusCode) - s.Contains(bodyRes, `{"code":"credentialsCannotBackupFile","details":{"filename":"~/.connect-creds","message":"failed to backup credentials to ~/.connect-creds: do not have write permissions"}}`) + s.Equal(http.StatusBadRequest, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"credentialsCannotBackupFile","details":{"filename":"~/.connect-creds","message":"Failed to backup credentials to ~/.connect-creds: do not have write permissions"}}`) } func (s *ResetCredsSuite) TestReset_UnknownError() { diff --git a/internal/types/api_errors.go b/internal/types/api_errors.go index 5a40712c9..b771460b7 100644 --- a/internal/types/api_errors.go +++ b/internal/types/api_errors.go @@ -187,7 +187,7 @@ type APIErrorCredentialsBackupFileError struct { } func (apierr *APIErrorCredentialsBackupFileError) JSONResponse(w http.ResponseWriter) { - jsonResult(w, http.StatusInternalServerError, apierr) + jsonResult(w, http.StatusBadRequest, apierr) } func APIErrorCredentialsBackupFileFromAgentError(aerr AgentError) APIErrorCredentialsBackupFileError { From a42e931a2fe62935139b49a45d8257d7d52809a7 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Wed, 8 Jan 2025 13:01:58 -0800 Subject: [PATCH 7/8] Captilize backup file agent error message --- internal/credentials/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/credentials/errors.go b/internal/credentials/errors.go index 628f4f3f9..53dba7014 100644 --- a/internal/credentials/errors.go +++ b/internal/credentials/errors.go @@ -107,7 +107,7 @@ func (e *IncompleteCredentialError) Error() string { func NewBackupFileAgentError(filename string, err error) *types.AgentError { details := types.ErrorCredentialsCannotBackupFileDetails{ Filename: filename, - Message: fmt.Sprintf("failed to backup credentials to %s: %v", filename, err.Error()), + Message: fmt.Sprintf("Failed to backup credentials to %s: %v", filename, err.Error()), } return types.NewAgentError(types.ErrorCredentialsCannotBackupFile, err, details) } From 4b04e4c7a9e44c95471488997beb9b321f759276 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Thu, 9 Jan 2025 11:19:36 -0800 Subject: [PATCH 8/8] Preface cred backup error with explanation --- extensions/vscode/src/utils/errorTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode/src/utils/errorTypes.ts b/extensions/vscode/src/utils/errorTypes.ts index 013f59567..a19727b4f 100644 --- a/extensions/vscode/src/utils/errorTypes.ts +++ b/extensions/vscode/src/utils/errorTypes.ts @@ -189,7 +189,7 @@ export const isErrCannotBackupCredentialsFile = export const errCannotBackupCredentialsFileMessage = ( err: axiosErrorWithJson, ) => { - return err.response.data.details.message; + return `Unrecognizable credentials for Posit Publisher were found. ${err.response.data.details.message}`; }; // Tries to match an Axios error that comes with an identifiable Json structured data