Skip to content

Commit

Permalink
Merge pull request #2517 from posit-dev/dotnomad/creds-backup-err
Browse files Browse the repository at this point in the history
Show error message if backing up credential file fails
  • Loading branch information
dotNomad authored Jan 9, 2025
2 parents b7f0023 + 4b04e4c commit 86c10ea
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 4 deletions.
1 change: 1 addition & 0 deletions extensions/vscode/src/api/resources/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
6 changes: 6 additions & 0 deletions extensions/vscode/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
16 changes: 16 additions & 0 deletions extensions/vscode/src/utils/errorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ErrorCode =
| "tomlValidationError"
| "tomlUnknownError"
| "pythonExecNotFound"
| "credentialsCannotBackupFile"
| "credentialsCorrupted";

export type axiosErrorWithJson<T = { code: ErrorCode; details: unknown }> =
Expand Down Expand Up @@ -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<ErrCannotBackupCredentialsFile>(
"credentialsCannotBackupFile",
);
export const errCannotBackupCredentialsFileMessage = (
err: axiosErrorWithJson<ErrCannotBackupCredentialsFile>,
) => {
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
// defaulting to be ErrUnknown message when
export function resolveAgentJsonErrorMsg(err: axiosErrorWithJson) {
Expand Down
14 changes: 13 additions & 1 deletion internal/credentials/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

package credentials

import "fmt"
import (
"fmt"

"github.com/posit-dev/publisher/internal/types"
)

type CorruptedError struct {
GUID string
Expand Down Expand Up @@ -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)
}
6 changes: 3 additions & 3 deletions internal/credentials/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
11 changes: 11 additions & 0 deletions internal/services/api/reset_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
16 changes: 16 additions & 0 deletions internal/services/api/reset_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.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() {
path := "http://example.com/api/credentials"
req, err := http.NewRequest("DELETE", path, nil)
Expand Down
24 changes: 24 additions & 0 deletions internal/types/api_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.StatusBadRequest, 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"`
}
Expand Down
1 change: 1 addition & 0 deletions internal/types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 86c10ea

Please sign in to comment.