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

Show error message if backing up credential file fails #2517

Merged
merged 8 commits into from
Jan 9, 2025
Merged
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 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
Loading