diff --git a/components/dashboard/src/components/UsageLimitReachedModal.tsx b/components/dashboard/src/components/UsageLimitReachedModal.tsx
index 35d870a4fee429..5416c48d9a419e 100644
--- a/components/dashboard/src/components/UsageLimitReachedModal.tsx
+++ b/components/dashboard/src/components/UsageLimitReachedModal.tsx
@@ -10,7 +10,7 @@ import Alert from "./Alert";
import Modal from "./Modal";
import { Heading2 } from "./typography/headings";
-export function UsageLimitReachedModal(p: { hints: any; onClose?: () => void }) {
+export function UsageLimitReachedModal(p: { onClose?: () => void }) {
const currentOrg = useCurrentOrg();
const orgName = currentOrg.data?.name;
diff --git a/components/dashboard/src/start/StartPage.tsx b/components/dashboard/src/start/StartPage.tsx
index eaaf8eba6b3f6c..8002c2bf024475 100644
--- a/components/dashboard/src/start/StartPage.tsx
+++ b/components/dashboard/src/start/StartPage.tsx
@@ -111,9 +111,7 @@ export function StartPage(props: StartPageProps) {
)}
{error && error.code === ErrorCodes.NEEDS_VERIFICATION && }
- {error && error.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED && (
-
- )}
+ {error && error.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED && }
{error && }
{props.children}
= ({ error, reset, setS
case ErrorCodes.INVALID_COST_CENTER:
return ;
case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED:
- return ;
+ return ;
case ErrorCodes.NEEDS_VERIFICATION:
return ;
default:
diff --git a/components/gitpod-protocol/src/messaging/error.ts b/components/gitpod-protocol/src/messaging/error.ts
index cd81832898971e..863ffc1cb0e2a9 100644
--- a/components/gitpod-protocol/src/messaging/error.ts
+++ b/components/gitpod-protocol/src/messaging/error.ts
@@ -5,9 +5,15 @@
*/
import { scrubber } from "../util/scrubbing";
+import { PlainMessage } from "@bufbuild/protobuf";
+import {
+ InvalidGitpodYMLError as InvalidGitpodYMLErrorData,
+ RepositoryNotFoundError as RepositoryNotFoundErrorData,
+ RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData,
+} from "@gitpod/public-api/lib/gitpod/v1/error_pb";
export class ApplicationError extends Error {
- constructor(public readonly code: ErrorCode, message: string, public readonly data?: any) {
+ constructor(readonly code: ErrorCode, readonly message: string, readonly data?: any) {
super(message);
this.data = scrubber.scrub(this.data, true);
}
@@ -21,6 +27,25 @@ export class ApplicationError extends Error {
}
}
+export class RepositoryNotFoundError extends ApplicationError {
+ constructor(readonly info: PlainMessage) {
+ // on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
+ super(ErrorCodes.NOT_FOUND, "Repository not found.", info);
+ }
+}
+export class UnauthorizedRepositoryAccessError extends ApplicationError {
+ constructor(readonly info: PlainMessage) {
+ // on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
+ super(ErrorCodes.NOT_AUTHENTICATED, "Repository unauthorized.", info);
+ }
+}
+export class InvalidGitpodYMLError extends ApplicationError {
+ constructor(readonly info: PlainMessage) {
+ // on gRPC we remap to PRECONDITION_FAILED, all error code for backwards compatibility with the dashboard
+ super(ErrorCodes.INVALID_GITPOD_YML, "Invalid gitpod.yml: " + info.violations.join(","), info);
+ }
+}
+
export namespace ApplicationError {
export function hasErrorCode(e: any): e is Error & { code: ErrorCode; data?: any } {
return ErrorCode.is(e["code"]);
diff --git a/components/gitpod-protocol/src/public-api-converter.spec.ts b/components/gitpod-protocol/src/public-api-converter.spec.ts
index 06e983411f99f1..087b610f74c72b 100644
--- a/components/gitpod-protocol/src/public-api-converter.spec.ts
+++ b/components/gitpod-protocol/src/public-api-converter.spec.ts
@@ -5,7 +5,7 @@
* See License.AGPL.txt in the project root for license information.
*/
-import { Timestamp } from "@bufbuild/protobuf";
+import { Timestamp, toPlainMessage } from "@bufbuild/protobuf";
import {
AdmissionLevel,
WorkspaceEnvironmentVariable,
@@ -41,6 +41,27 @@ import {
EnvironmentVariableAdmission,
UserEnvironmentVariable,
} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
+import {
+ ApplicationError,
+ ErrorCodes,
+ InvalidGitpodYMLError,
+ RepositoryNotFoundError,
+ UnauthorizedRepositoryAccessError,
+} from "./messaging/error";
+import { Code, ConnectError } from "@connectrpc/connect";
+import {
+ FailedPreconditionDetails,
+ NeedsVerificationError,
+ PermissionDeniedDetails,
+ UserBlockedError,
+ InvalidGitpodYMLError as InvalidGitpodYMLErrorData,
+ RepositoryNotFoundError as RepositoryNotFoundErrorData,
+ RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData,
+ PaymentSpendingLimitReachedError,
+ InvalidCostCenterError,
+ ImageBuildLogsNotYetAvailableError,
+ TooManyRunningWorkspacesError,
+} from "@gitpod/public-api/lib/gitpod/v1/error_pb";
describe("PublicAPIConverter", () => {
const converter = new PublicAPIConverter();
@@ -998,4 +1019,331 @@ describe("PublicAPIConverter", () => {
});
});
});
+
+ describe("errors", () => {
+ it("USER_BLOCKED", () => {
+ const connectError = converter.toError(new ApplicationError(ErrorCodes.USER_BLOCKED, "user blocked"));
+ expect(connectError.code).to.equal(Code.PermissionDenied);
+ expect(connectError.rawMessage).to.equal("user blocked");
+
+ const details = connectError.findDetails(PermissionDeniedDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("userBlocked");
+ expect(details?.reason?.value).to.be.instanceOf(UserBlockedError);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.USER_BLOCKED);
+ expect(appError.message).to.equal("user blocked");
+ });
+
+ it("NEEDS_VERIFICATION", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.NEEDS_VERIFICATION, "needs verification"),
+ );
+ expect(connectError.code).to.equal(Code.PermissionDenied);
+ expect(connectError.rawMessage).to.equal("needs verification");
+
+ const details = connectError.findDetails(PermissionDeniedDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("needsVerification");
+ expect(details?.reason?.value).to.be.instanceOf(NeedsVerificationError);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.NEEDS_VERIFICATION);
+ expect(appError.message).to.equal("needs verification");
+ });
+
+ it("INVALID_GITPOD_YML", () => {
+ const connectError = converter.toError(
+ new InvalidGitpodYMLError({
+ violations: ['Invalid value: "": must not be empty'],
+ }),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal('Invalid gitpod.yml: Invalid value: "": must not be empty');
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("invalidGitpodYml");
+ expect(details?.reason?.value).to.be.instanceOf(InvalidGitpodYMLErrorData);
+
+ let violations = (details?.reason?.value as InvalidGitpodYMLErrorData).violations;
+ expect(violations).to.deep.equal(['Invalid value: "": must not be empty']);
+
+ const appError = converter.fromError(connectError);
+ expect(appError).to.be.instanceOf(InvalidGitpodYMLError);
+ expect(appError.code).to.equal(ErrorCodes.INVALID_GITPOD_YML);
+ expect(appError.message).to.equal('Invalid gitpod.yml: Invalid value: "": must not be empty');
+
+ violations = (appError as InvalidGitpodYMLError).info.violations;
+ expect(violations).to.deep.equal(['Invalid value: "": must not be empty']);
+ });
+
+ it("RepositoryNotFoundError", () => {
+ const connectError = converter.toError(
+ new RepositoryNotFoundError({
+ host: "github.com",
+ lastUpdate: "2021-06-28T10:48:28Z",
+ owner: "akosyakov",
+ userIsOwner: true,
+ userScopes: ["repo"],
+ }),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("Repository not found.");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("repositoryNotFound");
+ expect(details?.reason?.value).to.be.instanceOf(RepositoryNotFoundErrorData);
+
+ let data = toPlainMessage(details?.reason?.value as RepositoryNotFoundErrorData);
+ expect(data.host).to.equal("github.com");
+ expect(data.lastUpdate).to.equal("2021-06-28T10:48:28Z");
+ expect(data.owner).to.equal("akosyakov");
+ expect(data.userIsOwner).to.equal(true);
+ expect(data.userScopes).to.deep.equal(["repo"]);
+
+ const appError = converter.fromError(connectError);
+ expect(appError).to.be.instanceOf(RepositoryNotFoundError);
+ expect(appError.code).to.equal(ErrorCodes.NOT_FOUND);
+ expect(appError.message).to.equal("Repository not found.");
+
+ data = (appError as RepositoryNotFoundError).info;
+ expect(data.host).to.equal("github.com");
+ expect(data.lastUpdate).to.equal("2021-06-28T10:48:28Z");
+ expect(data.owner).to.equal("akosyakov");
+ expect(data.userIsOwner).to.equal(true);
+ expect(data.userScopes).to.deep.equal(["repo"]);
+ });
+
+ it("UnauthorizedRepositoryAccessError", () => {
+ const connectError = converter.toError(
+ new UnauthorizedRepositoryAccessError({
+ host: "github.com",
+ scopes: ["repo"],
+ }),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("Repository unauthorized.");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("repositoryUnauthorized");
+ expect(details?.reason?.value).to.be.instanceOf(RepositoryUnauthorizedErrorData);
+
+ let data = toPlainMessage(details?.reason?.value as RepositoryUnauthorizedErrorData);
+ expect(data.host).to.equal("github.com");
+ expect(data.scopes).to.deep.equal(["repo"]);
+
+ const appError = converter.fromError(connectError);
+ expect(appError).to.be.instanceOf(UnauthorizedRepositoryAccessError);
+ expect(appError.code).to.equal(ErrorCodes.NOT_AUTHENTICATED);
+ expect(appError.message).to.equal("Repository unauthorized.");
+
+ data = (appError as UnauthorizedRepositoryAccessError).info;
+ expect(data.host).to.equal("github.com");
+ expect(data.scopes).to.deep.equal(["repo"]);
+ });
+
+ it("PAYMENT_SPENDING_LIMIT_REACHED", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, "payment spending limit reached"),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("payment spending limit reached");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("paymentSpendingLimitReached");
+ expect(details?.reason?.value).to.be.instanceOf(PaymentSpendingLimitReachedError);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED);
+ expect(appError.message).to.equal("payment spending limit reached");
+ });
+
+ it("INVALID_COST_CENTER", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.INVALID_COST_CENTER, "invalid cost center", {
+ attributionId: 12345,
+ }),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("invalid cost center");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("invalidCostCenter");
+ expect(details?.reason?.value).to.be.instanceOf(InvalidCostCenterError);
+
+ let data = toPlainMessage(details?.reason?.value as InvalidCostCenterError);
+ expect(data.attributionId).to.equal(12345);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.INVALID_COST_CENTER);
+ expect(appError.message).to.equal("invalid cost center");
+
+ data = appError.data;
+ expect(data.attributionId).to.equal(12345);
+ });
+
+ it("HEADLESS_LOG_NOT_YET_AVAILABLE", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, "image build log not yet available"),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("image build log not yet available");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("imageBuildLogsNotYetAvailable");
+ expect(details?.reason?.value).to.be.instanceOf(ImageBuildLogsNotYetAvailableError);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE);
+ expect(appError.message).to.equal("image build log not yet available");
+ });
+
+ it("TOO_MANY_RUNNING_WORKSPACES", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.TOO_MANY_RUNNING_WORKSPACES, "too many running workspaces"),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("too many running workspaces");
+
+ const details = connectError.findDetails(FailedPreconditionDetails)[0];
+ expect(details).to.not.be.undefined;
+ expect(details?.reason?.case).to.equal("tooManyRunningWorkspaces");
+ expect(details?.reason?.value).to.be.instanceOf(TooManyRunningWorkspacesError);
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.TOO_MANY_RUNNING_WORKSPACES);
+ expect(appError.message).to.equal("too many running workspaces");
+ });
+
+ it("NOT_FOUND", () => {
+ const connectError = converter.toError(new ApplicationError(ErrorCodes.NOT_FOUND, "not found"));
+ expect(connectError.code).to.equal(Code.NotFound);
+ expect(connectError.rawMessage).to.equal("not found");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.NOT_FOUND);
+ expect(appError.message).to.equal("not found");
+ });
+
+ it("NOT_AUTHENTICATED", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "not authenticated"),
+ );
+ expect(connectError.code).to.equal(Code.Unauthenticated);
+ expect(connectError.rawMessage).to.equal("not authenticated");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.NOT_AUTHENTICATED);
+ expect(appError.message).to.equal("not authenticated");
+ });
+
+ it("PERMISSION_DENIED", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.PERMISSION_DENIED, "permission denied"),
+ );
+ expect(connectError.code).to.equal(Code.PermissionDenied);
+ expect(connectError.rawMessage).to.equal("permission denied");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.PERMISSION_DENIED);
+ expect(appError.message).to.equal("permission denied");
+ });
+
+ it("CONFLICT", () => {
+ const connectError = converter.toError(new ApplicationError(ErrorCodes.CONFLICT, "conflict"));
+ expect(connectError.code).to.equal(Code.AlreadyExists);
+ expect(connectError.rawMessage).to.equal("conflict");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.CONFLICT);
+ expect(appError.message).to.equal("conflict");
+ });
+
+ it("PRECONDITION_FAILED", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.PRECONDITION_FAILED, "precondition failed"),
+ );
+ expect(connectError.code).to.equal(Code.FailedPrecondition);
+ expect(connectError.rawMessage).to.equal("precondition failed");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.PRECONDITION_FAILED);
+ expect(appError.message).to.equal("precondition failed");
+ });
+
+ it("TOO_MANY_REQUESTS", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.TOO_MANY_REQUESTS, "too many requests"),
+ );
+ expect(connectError.code).to.equal(Code.ResourceExhausted);
+ expect(connectError.rawMessage).to.equal("too many requests");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.TOO_MANY_REQUESTS);
+ expect(appError.message).to.equal("too many requests");
+ });
+
+ it("CANCELLED", () => {
+ const connectError = converter.toError(new ApplicationError(ErrorCodes.CANCELLED, "cancelled"));
+ expect(connectError.code).to.equal(Code.Canceled);
+ expect(connectError.rawMessage).to.equal("cancelled");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.CANCELLED);
+ expect(appError.message).to.equal("cancelled");
+ });
+
+ it("INTERNAL_SERVER_ERROR", () => {
+ const connectError = converter.toError(
+ new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "internal server error"),
+ );
+ expect(connectError.code).to.equal(Code.Internal);
+ expect(connectError.rawMessage).to.equal("internal server error");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.INTERNAL_SERVER_ERROR);
+ expect(appError.message).to.equal("internal server error");
+ });
+
+ it("UNKNOWN", () => {
+ // some errors are not really used by clients we turn them to unknown on API interface
+ // and then to internal on the dashboard
+ // we monitor such occurences via tests and observability and replace them with stanard codes or get rid of them
+ const connectError = converter.toError(new ApplicationError(ErrorCodes.EE_FEATURE, "unknown"));
+ expect(connectError.code).to.equal(Code.Unknown);
+ expect(connectError.rawMessage).to.equal("unknown");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.INTERNAL_SERVER_ERROR);
+ expect(appError.message).to.equal("unknown");
+ });
+
+ it("ConnectError", () => {
+ const connectError = new ConnectError("already exists", Code.AlreadyExists);
+ const error = converter.toError(connectError);
+ expect(error).to.equal(connectError, "preserved on API");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.CONFLICT, "app error on dashboard");
+ expect(appError.message).to.equal("already exists");
+ });
+
+ it("Any other error is internal", () => {
+ const error = new Error("unknown");
+ const connectError = converter.toError(error);
+ expect(connectError.code).to.equal(Code.Internal);
+ expect(connectError.rawMessage).to.equal("unknown");
+
+ const appError = converter.fromError(connectError);
+ expect(appError.code).to.equal(ErrorCodes.INTERNAL_SERVER_ERROR);
+ expect(appError.message).to.equal("unknown");
+ });
+ });
});
diff --git a/components/gitpod-protocol/src/public-api-converter.ts b/components/gitpod-protocol/src/public-api-converter.ts
index b36c0bc0e6319c..b27b7730ad43a7 100644
--- a/components/gitpod-protocol/src/public-api-converter.ts
+++ b/components/gitpod-protocol/src/public-api-converter.ts
@@ -4,8 +4,21 @@
* See License.AGPL.txt in the project root for license information.
*/
-import { Timestamp } from "@bufbuild/protobuf";
+import { Timestamp, toPlainMessage } from "@bufbuild/protobuf";
import { Code, ConnectError } from "@connectrpc/connect";
+import {
+ FailedPreconditionDetails,
+ ImageBuildLogsNotYetAvailableError,
+ InvalidCostCenterError as InvalidCostCenterErrorData,
+ InvalidGitpodYMLError as InvalidGitpodYMLErrorData,
+ NeedsVerificationError,
+ PaymentSpendingLimitReachedError,
+ PermissionDeniedDetails,
+ RepositoryNotFoundError as RepositoryNotFoundErrorData,
+ RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData,
+ TooManyRunningWorkspacesError,
+ UserBlockedError,
+} from "@gitpod/public-api/lib/gitpod/v1/error_pb";
import {
AuthProvider,
AuthProviderDescription,
@@ -51,7 +64,13 @@ import {
PrebuildPhase,
PrebuildPhase_Phase,
} from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
-import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error";
+import {
+ ApplicationError,
+ ErrorCodes,
+ InvalidGitpodYMLError,
+ RepositoryNotFoundError,
+ UnauthorizedRepositoryAccessError,
+} from "./messaging/error";
import {
AuthProviderEntry as AuthProviderProtocol,
AuthProviderInfo,
@@ -79,7 +98,6 @@ import {
Project,
Organization as ProtocolOrganization,
} from "./teams-projects-protocol";
-import { TrustedValue } from "./util/scrubbing";
import {
ConfigurationIdeConfig,
PortProtocol,
@@ -92,9 +110,6 @@ import type { DeepPartial } from "./util/deep-partial";
export type PartialConfiguration = DeepPartial & Pick;
-const applicationErrorCode = "application-error-code";
-const applicationErrorData = "application-error-data";
-
/**
* Converter between gRPC and JSON-RPC types.
*
@@ -199,57 +214,240 @@ export class PublicAPIConverter {
return reason;
}
if (reason instanceof ApplicationError) {
- const metadata: HeadersInit = {};
- metadata[applicationErrorCode] = String(reason.code);
- if (reason.data) {
- metadata[applicationErrorData] = JSON.stringify(reason.data);
+ if (reason.code === ErrorCodes.USER_BLOCKED) {
+ return new ConnectError(
+ reason.message,
+ Code.PermissionDenied,
+ undefined,
+ [
+ new PermissionDeniedDetails({
+ reason: {
+ case: "userBlocked",
+ value: new UserBlockedError(),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason.code === ErrorCodes.NEEDS_VERIFICATION) {
+ return new ConnectError(
+ reason.message,
+ Code.PermissionDenied,
+ undefined,
+ [
+ new PermissionDeniedDetails({
+ reason: {
+ case: "needsVerification",
+ value: new NeedsVerificationError(),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason instanceof InvalidGitpodYMLError) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "invalidGitpodYml",
+ value: new InvalidGitpodYMLErrorData(reason.info),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason instanceof RepositoryNotFoundError) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "repositoryNotFound",
+ value: new RepositoryNotFoundErrorData(reason.info),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason instanceof UnauthorizedRepositoryAccessError) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "repositoryUnauthorized",
+ value: new RepositoryUnauthorizedErrorData(reason.info),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason.code === ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "paymentSpendingLimitReached",
+ value: new PaymentSpendingLimitReachedError(),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason.code === ErrorCodes.INVALID_COST_CENTER) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "invalidCostCenter",
+ value: new InvalidCostCenterErrorData({
+ attributionId: reason.data.attributionId,
+ }),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason.code === ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "imageBuildLogsNotYetAvailable",
+ value: new ImageBuildLogsNotYetAvailableError(),
+ },
+ }),
+ ],
+ reason,
+ );
+ }
+ if (reason.code === ErrorCodes.TOO_MANY_RUNNING_WORKSPACES) {
+ return new ConnectError(
+ reason.message,
+ Code.FailedPrecondition,
+ undefined,
+ [
+ new FailedPreconditionDetails({
+ reason: {
+ case: "tooManyRunningWorkspaces",
+ value: new TooManyRunningWorkspacesError(),
+ },
+ }),
+ ],
+ reason,
+ );
}
if (reason.code === ErrorCodes.NOT_FOUND) {
- return new ConnectError(reason.message, Code.NotFound, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.NotFound, undefined, undefined, reason);
}
if (reason.code === ErrorCodes.NOT_AUTHENTICATED) {
- return new ConnectError(reason.message, Code.Unauthenticated, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.Unauthenticated, undefined, undefined, reason);
}
- if (reason.code === ErrorCodes.PERMISSION_DENIED || reason.code === ErrorCodes.USER_BLOCKED) {
- return new ConnectError(reason.message, Code.PermissionDenied, metadata, undefined, reason);
+ if (reason.code === ErrorCodes.PERMISSION_DENIED) {
+ return new ConnectError(reason.message, Code.PermissionDenied, undefined, undefined, reason);
}
if (reason.code === ErrorCodes.CONFLICT) {
- return new ConnectError(reason.message, Code.AlreadyExists, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.AlreadyExists, undefined, undefined, reason);
}
if (reason.code === ErrorCodes.PRECONDITION_FAILED) {
- return new ConnectError(reason.message, Code.FailedPrecondition, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.FailedPrecondition, undefined, undefined, reason);
}
if (reason.code === ErrorCodes.TOO_MANY_REQUESTS) {
- return new ConnectError(reason.message, Code.ResourceExhausted, metadata, undefined, reason);
- }
- if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
- return new ConnectError(reason.message, Code.Internal, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.ResourceExhausted, undefined, undefined, reason);
}
if (reason.code === ErrorCodes.CANCELLED) {
- return new ConnectError(reason.message, Code.DeadlineExceeded, metadata, undefined, reason);
+ return new ConnectError(reason.message, Code.Canceled, undefined, undefined, reason);
}
- return new ConnectError(reason.message, Code.InvalidArgument, metadata, undefined, reason);
+ if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
+ return new ConnectError(reason.message, Code.Internal, undefined, undefined, reason);
+ }
+ return new ConnectError(reason.message, Code.Unknown, undefined, undefined, reason);
}
return ConnectError.from(reason, Code.Internal);
}
- fromError(reason: ConnectError): Error {
- const codeMetadata = reason.metadata?.get(applicationErrorCode);
- if (!codeMetadata) {
- return reason;
+ fromError(reason: ConnectError): ApplicationError {
+ if (reason.code === Code.NotFound) {
+ return new ApplicationError(ErrorCodes.NOT_FOUND, reason.rawMessage);
}
- const code = Number(codeMetadata) as ErrorCode;
- const dataMetadata = reason.metadata?.get(applicationErrorData);
- let data = undefined;
- if (dataMetadata) {
- try {
- data = JSON.parse(dataMetadata);
- } catch (e) {
- console.error("failed to parse application error data", e);
+ if (reason.code === Code.Unauthenticated) {
+ return new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, reason.rawMessage);
+ }
+ if (reason.code === Code.PermissionDenied) {
+ const details = reason.findDetails(PermissionDeniedDetails)[0];
+ switch (details?.reason?.case) {
+ case "userBlocked":
+ return new ApplicationError(ErrorCodes.USER_BLOCKED, reason.rawMessage);
+ case "needsVerification":
+ return new ApplicationError(ErrorCodes.NEEDS_VERIFICATION, reason.rawMessage);
+ }
+ return new ApplicationError(ErrorCodes.PERMISSION_DENIED, reason.rawMessage);
+ }
+ if (reason.code === Code.AlreadyExists) {
+ return new ApplicationError(ErrorCodes.CONFLICT, reason.rawMessage);
+ }
+ if (reason.code === Code.FailedPrecondition) {
+ const details = reason.findDetails(FailedPreconditionDetails)[0];
+ switch (details?.reason?.case) {
+ case "invalidGitpodYml":
+ const invalidGitpodYmlInfo = toPlainMessage(details.reason.value);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ return new InvalidGitpodYMLError(invalidGitpodYmlInfo);
+ case "repositoryNotFound":
+ const repositoryNotFoundInfo = toPlainMessage(details.reason.value);
+ return new RepositoryNotFoundError(repositoryNotFoundInfo);
+ case "repositoryUnauthorized":
+ const repositoryUnauthorizedInfo = toPlainMessage(details.reason.value);
+ return new UnauthorizedRepositoryAccessError(repositoryUnauthorizedInfo);
+ case "paymentSpendingLimitReached":
+ return new ApplicationError(ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, reason.rawMessage);
+ case "invalidCostCenter":
+ const invalidCostCenterInfo = toPlainMessage(details.reason.value);
+ return new ApplicationError(
+ ErrorCodes.INVALID_COST_CENTER,
+ reason.rawMessage,
+ invalidCostCenterInfo,
+ );
+ case "imageBuildLogsNotYetAvailable":
+ return new ApplicationError(ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, reason.rawMessage);
+ case "tooManyRunningWorkspaces":
+ return new ApplicationError(ErrorCodes.TOO_MANY_RUNNING_WORKSPACES, reason.rawMessage);
}
+ return new ApplicationError(ErrorCodes.PRECONDITION_FAILED, reason.rawMessage);
+ }
+ if (reason.code === Code.ResourceExhausted) {
+ return new ApplicationError(ErrorCodes.TOO_MANY_REQUESTS, reason.rawMessage);
+ }
+ if (reason.code === Code.Canceled) {
+ return new ApplicationError(ErrorCodes.CANCELLED, reason.rawMessage);
+ }
+ if (reason.code === Code.Internal) {
+ return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, reason.rawMessage);
}
- // data is trusted here, since it was scrubbed before on the server
- return new ApplicationError(code, reason.message, new TrustedValue(data));
+ return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, reason.rawMessage);
}
toWorkspaceEnvironmentVariables(context: WorkspaceContext): WorkspaceEnvironmentVariable[] {
diff --git a/components/server/src/auth/errors.ts b/components/server/src/auth/errors.ts
index f517350e03330a..8017609e35dedf 100644
--- a/components/server/src/auth/errors.ts
+++ b/components/server/src/auth/errors.ts
@@ -4,21 +4,8 @@
* See License.AGPL.txt in the project root for license information.
*/
-import { Identity } from "@gitpod/gitpod-protocol";
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
-export interface TosNotAcceptedYetException extends Error {
- readonly identity: Identity;
-}
-export namespace TosNotAcceptedYetException {
- export function create(identity: Identity) {
- return Object.assign(new Error("TosNotAcceptedYetException"), { identity });
- }
- export function is(error: any): error is TosNotAcceptedYetException {
- return !!error && error.message === "TosNotAcceptedYetException";
- }
-}
-
export interface AuthException extends Error {
readonly payload: any;
readonly authException: string;
diff --git a/components/server/src/errors/index.ts b/components/server/src/errors/index.ts
index 793a15c3a6787b..9d85b7d1edbccc 100644
--- a/components/server/src/errors/index.ts
+++ b/components/server/src/errors/index.ts
@@ -5,62 +5,38 @@
*/
import { User, Token } from "@gitpod/gitpod-protocol";
+import {
+ RepositoryNotFoundError,
+ UnauthorizedRepositoryAccessError,
+} from "@gitpod/gitpod-protocol/lib/messaging/error";
-export interface NotFoundError extends Error {
- readonly data: NotFoundError.Data;
-}
export namespace NotFoundError {
- export interface Data {
- readonly host: string;
- readonly owner: string;
- readonly repoName: string;
- readonly userScopes?: string[];
- readonly lastUpdate?: string;
- }
export async function create(token: Token | undefined, user: User, host: string, owner: string, repoName: string) {
- const lastUpdate = token && token.updateDate;
+ const lastUpdate = (token && token.updateDate) || "";
const userScopes = token ? [...token.scopes] : [];
const userIsOwner = owner == user.name; // TODO: shouldn't this be a comparison with `identity.authName`?
- const data = { host, owner, repoName, userIsOwner, userScopes, lastUpdate };
- const error = Object.assign(new Error("NotFoundError"), { data });
- return error;
+ return new RepositoryNotFoundError({
+ host,
+ owner,
+ userIsOwner,
+ userScopes,
+ lastUpdate,
+ });
}
- export function is(error: any): error is NotFoundError {
- return (
- !!error &&
- !!error.data &&
- !!error.data.host &&
- !!error.data.owner &&
- !!error.data.repoName &&
- error.message === "NotFoundError"
- );
+ export function is(error: any): error is RepositoryNotFoundError {
+ return error instanceof RepositoryNotFoundError;
}
}
-export interface UnauthorizedError extends Error {
- readonly data: UnauthorizedError.Data;
-}
export namespace UnauthorizedError {
- export interface Data {
- readonly host: string;
- readonly scopes: string[];
- readonly messageHint: string;
- }
- const message = "UnauthorizedError";
export function create(host: string, scopes: string[], messageHint?: string) {
- const data = { host, scopes, messageHint: messageHint || "unauthorized" };
- const error = Object.assign(new Error(message), { data });
- return error;
+ return new UnauthorizedRepositoryAccessError({
+ host,
+ scopes,
+ });
}
- export function is(error: any): error is UnauthorizedError {
- return (
- !!error &&
- !!error.data &&
- !!error.data.host &&
- !!error.data.scopes &&
- !!error.data.messageHint &&
- error.message === message
- );
+ export function is(error: any): error is UnauthorizedRepositoryAccessError {
+ return error instanceof UnauthorizedRepositoryAccessError;
}
}
diff --git a/components/server/src/prebuilds/prebuild-manager.ts b/components/server/src/prebuilds/prebuild-manager.ts
index 4bf3de5402e465..563a0d1ad6a497 100644
--- a/components/server/src/prebuilds/prebuild-manager.ts
+++ b/components/server/src/prebuilds/prebuild-manager.ts
@@ -17,7 +17,6 @@ import {
User,
Workspace,
WorkspaceConfig,
- WorkspaceInstance,
} from "@gitpod/gitpod-protocol";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
@@ -42,12 +41,6 @@ import { generateAsyncGenerator } from "@gitpod/gitpod-protocol/lib/generate-asy
import { RedisSubscriber } from "../messaging/redis-subscriber";
import { ctxSignal } from "../util/request-context";
-export class WorkspaceRunningError extends Error {
- constructor(msg: string, public instance: WorkspaceInstance) {
- super(msg);
- }
-}
-
export interface StartPrebuildParams {
user: User;
context: CommitContext;
@@ -397,41 +390,6 @@ export class PrebuildManager {
}
}
- async retriggerPrebuild(
- ctx: TraceContext,
- user: User,
- project: Project | undefined,
- workspaceId: string,
- ): Promise {
- const span = TraceContext.startSpan("retriggerPrebuild", ctx);
- span.setTag("workspaceId", workspaceId);
- try {
- const workspacePromise = this.workspaceDB.trace({ span }).findById(workspaceId);
- const prebuildPromise = this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(workspaceId);
- const runningInstance = await this.workspaceDB.trace({ span }).findRunningInstance(workspaceId);
- if (runningInstance !== undefined) {
- throw new WorkspaceRunningError("Workspace is still runnning", runningInstance);
- }
- span.setTag("starting", true);
- const workspace = await workspacePromise;
- if (!workspace) {
- console.error("Unknown workspace id.", { workspaceId });
- throw new Error("Unknown workspace " + workspaceId);
- }
- const prebuild = await prebuildPromise;
- if (!prebuild) {
- throw new Error("No prebuild found for workspace " + workspaceId);
- }
- await this.workspaceService.startWorkspace({ span }, user, workspaceId, {}, false);
- return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
- } catch (err) {
- TraceContext.setError({ span }, err);
- throw err;
- } finally {
- span.finish();
- }
- }
-
checkPrebuildPrecondition(params: { config: WorkspaceConfig; project: Project; context: CommitContext }): {
shouldRun: boolean;
reason: string;
diff --git a/components/server/src/workspace/config-provider.ts b/components/server/src/workspace/config-provider.ts
index abca87436630fd..141288ec528189 100644
--- a/components/server/src/workspace/config-provider.ts
+++ b/components/server/src/workspace/config-provider.ts
@@ -32,6 +32,7 @@ import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { Config } from "../config";
import { EntitlementService } from "../billing/entitlement-service";
import { TeamDB } from "@gitpod/gitpod-db/lib";
+import { InvalidGitpodYMLError } from "@gitpod/gitpod-protocol/lib/messaging/error";
const POD_PATH_WORKSPACE_BASE = "/workspace";
@@ -149,7 +150,9 @@ export class ConfigProvider {
customConfig = parseResult.config;
customConfig._origin = "additional-content";
if (parseResult.validationErrors) {
- const err = new InvalidGitpodYMLError(parseResult.validationErrors);
+ const err = new InvalidGitpodYMLError({
+ violations: parseResult.validationErrors,
+ });
// this is not a system error but a user misconfiguration
log.info(logContext, err.message, {
repoCloneUrl: commit.repository.cloneUrl,
@@ -186,7 +189,9 @@ export class ConfigProvider {
const parseResult = this.gitpodParser.parse(customConfigString);
customConfig = parseResult.config;
if (parseResult.validationErrors) {
- const err = new InvalidGitpodYMLError(parseResult.validationErrors);
+ const err = new InvalidGitpodYMLError({
+ violations: parseResult.validationErrors,
+ });
// this is not a system error but a user misconfiguration
log.info(logContext, err.message, {
repoCloneUrl: commit.repository.cloneUrl,
@@ -302,17 +307,3 @@ export class ConfigProvider {
return normalizedPath.includes("..") || pathSegments.slice(0, 2).join("/") != POD_PATH_WORKSPACE_BASE;
}
}
-
-export class InvalidGitpodYMLError extends Error {
- public readonly errorType = "invalidGitpodYML";
-
- constructor(public readonly validationErrors: string[]) {
- super("Invalid gitpod.yml: " + validationErrors.join(","));
- }
-}
-
-export namespace InvalidGitpodYMLError {
- export function is(obj: any): obj is InvalidGitpodYMLError {
- return "errorType" in obj && (obj as any).errorType === "invalidGitpodYML" && "validationErrors" in obj;
- }
-}
diff --git a/components/server/src/workspace/context-parser-service.ts b/components/server/src/workspace/context-parser-service.ts
index 8f51c4945643ad..c44aba962094ba 100644
--- a/components/server/src/workspace/context-parser-service.ts
+++ b/components/server/src/workspace/context-parser-service.ts
@@ -9,7 +9,8 @@ import { injectable, multiInject, inject } from "inversify";
import { HostContextProvider } from "../auth/host-context-provider";
import { IPrefixContextParser, IContextParser } from "./context-parser";
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
-import { ConfigProvider, InvalidGitpodYMLError } from "./config-provider";
+import { ConfigProvider } from "./config-provider";
+import { InvalidGitpodYMLError } from "@gitpod/gitpod-protocol/lib/messaging/error";
@injectable()
export class ContextParser {
@@ -128,9 +129,9 @@ export class ContextParser {
config.config.mainConfiguration,
);
if (!CommitContext.is(mainRepoContext)) {
- throw new InvalidGitpodYMLError([
- `Cannot find main repository '${config.config.mainConfiguration}'.`,
- ]);
+ throw new InvalidGitpodYMLError({
+ violations: [`Cannot find main repository '${config.config.mainConfiguration}'.`],
+ });
}
// Note: we only care about repo related stuff in this function.
// Fields like `config.image` will not be exposed, so we don't pass organizationId here
@@ -146,7 +147,9 @@ export class ContextParser {
subRepo.url,
)) as CommitContext;
if (!CommitContext.is(subContext)) {
- throw new InvalidGitpodYMLError([`Cannot find sub-repository '${subRepo.url}'.`]);
+ throw new InvalidGitpodYMLError({
+ violations: [`Cannot find sub-repository '${subRepo.url}'.`],
+ });
}
if (context.repository.cloneUrl === subContext.repository.cloneUrl) {
// if it's the repo from the original context we want to use that commit.
diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts
index 7d129f7f057a43..72eaa6640e7eb3 100644
--- a/components/server/src/workspace/gitpod-server-impl.ts
+++ b/components/server/src/workspace/gitpod-server-impl.ts
@@ -78,7 +78,11 @@ import {
AdminModifyRoleOrPermissionRequest,
WorkspaceAndInstance,
} from "@gitpod/gitpod-protocol/lib/admin-protocol";
-import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
+import {
+ ApplicationError,
+ ErrorCodes,
+ UnauthorizedRepositoryAccessError,
+} from "@gitpod/gitpod-protocol/lib/messaging/error";
import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging";
import {
InterfaceWithTraceContext,
@@ -101,13 +105,12 @@ import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
import { AuthProviderService } from "../auth/auth-provider-service";
import { GuardedResource, ResourceAccessGuard, ResourceAccessOp } from "../auth/resource-access";
import { Config } from "../config";
-import { NotFoundError, UnauthorizedError } from "../errors";
import { AuthorizationService } from "../user/authorization-service";
import { UserAuthentication } from "../user/user-authentication";
import { ContextParser } from "./context-parser-service";
import { isClusterMaintenanceError } from "./workspace-starter";
import { HeadlessLogUrls } from "@gitpod/gitpod-protocol/lib/headless-workspace-log";
-import { ConfigProvider, InvalidGitpodYMLError } from "./config-provider";
+import { ConfigProvider } from "./config-provider";
import { ProjectsService } from "../projects/projects-service";
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
import {
@@ -1089,7 +1092,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
event: "snapshot_access_denied",
properties: { snapshot_id: context.snapshotId, error: String(error) },
}).catch((err) => log.error("cannot track event", err));
- if (UnauthorizedError.is(error)) {
+ if (error instanceof UnauthorizedRepositoryAccessError) {
throw error;
}
throw new ApplicationError(
@@ -1197,20 +1200,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
}
private handleError(error: any, logContext: LogContext, normalizedContextUrl: string) {
- if (NotFoundError.is(error)) {
- throw new ApplicationError(ErrorCodes.NOT_FOUND, "Repository not found.", error.data);
- }
- if (UnauthorizedError.is(error)) {
- throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);
- }
- if (InvalidGitpodYMLError.is(error)) {
- throw new ApplicationError(ErrorCodes.INVALID_GITPOD_YML, error.message);
- }
-
if (ApplicationError.hasErrorCode(error)) {
// specific errors will be handled in create-workspace.tsx
throw error;
}
+ // TODO(ak) not sure about it we shovel all errors in context parsing error
+ // we should rather do internal errors, and categorize at sources
log.debug(logContext, error);
throw new ApplicationError(
ErrorCodes.CONTEXT_PARSE_ERROR,
@@ -1955,21 +1950,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
const user = await this.checkAndBlockUser("getProjectOverview");
await this.guardProjectOperation(user, projectId, "get");
- try {
- const result = await this.projectsService.getProjectOverview(user, projectId);
- if (result) {
- result.isConsideredInactive = await this.projectsService.isProjectConsideredInactive(
- user.id,
- projectId,
- );
- }
- return result;
- } catch (error) {
- if (UnauthorizedError.is(error)) {
- throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);
- }
- throw error;
+ const result = await this.projectsService.getProjectOverview(user, projectId);
+ if (result) {
+ result.isConsideredInactive = await this.projectsService.isProjectConsideredInactive(user.id, projectId);
}
+ return result;
}
async adminFindPrebuilds(ctx: TraceContext, params: FindPrebuildsParams): Promise {