gitpod/components/gitpod-protocol/src/public-api-converter.ts
Brad Harris df7929ce8a
Adding ConfigurationServiceAPI (#19020)
* adding ConfigurationServiceAPI

* binding config service api to server

* use getConfiguration in dashboard

* adding missing binding

* use ApplicationError's

* add protobuf classes to query client hydration

* fixing pagination param & query

* changing to import statements for consistency and clarity on what the imports are for

* cleanup

* dropping config settings for create for now

* use protobuf field names in error messages

* removing optional on fields

* fixing converters to account for non-optional (undefined) fields

* update test

* adding more tests for findProjectsBySearchTerm

* fixing test to use offset correctly

* convert pagination args correctly
2023-11-08 22:42:46 +02:00

431 lines
16 KiB
TypeScript

/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/
import { Code, ConnectError } from "@connectrpc/connect";
import { Timestamp } from "@bufbuild/protobuf";
import {
AdmissionLevel,
EditorReference,
Workspace,
WorkspaceConditions,
WorkspaceEnvironmentVariable,
WorkspaceGitStatus,
WorkspacePhase,
WorkspacePhase_Phase,
WorkspacePort,
WorkspacePort_Policy,
WorkspacePort_Protocol,
WorkspaceStatus,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import {
Organization,
OrganizationMember,
OrganizationRole,
OrganizationSettings,
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import {
BranchMatchingStrategy,
Configuration,
PrebuildSettings,
WorkspaceSettings,
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error";
import {
CommitContext,
EnvVarWithValue,
WithEnvvarsContext,
WithPrebuild,
WorkspaceContext,
WorkspaceInfo,
Workspace as ProtocolWorkspace,
} from "./protocol";
import {
ConfigurationIdeConfig,
PortProtocol,
WorkspaceInstance,
WorkspaceInstanceConditions,
WorkspaceInstancePort,
} from "./workspace-instance";
import { ContextURL } from "./context-url";
import { TrustedValue } from "./util/scrubbing";
import {
Organization as ProtocolOrganization,
OrgMemberInfo,
OrgMemberRole,
OrganizationSettings as OrganizationSettingsProtocol,
Project,
PrebuildSettings as PrebuildSettingsProtocol,
} from "./teams-projects-protocol";
const applicationErrorCode = "application-error-code";
const applicationErrorData = "application-error-data";
/**
* Converter between gRPC and JSON-RPC types.
*
* Use following conventions:
* - methods converting from JSON-RPC to gRPC is called `to*`
* - methods converting from gRPC to JSON-RPC is called `from*`
*/
export class PublicAPIConverter {
toWorkspace(arg: WorkspaceInfo | WorkspaceInstance, current?: Workspace): Workspace {
const workspace = current ?? new Workspace();
if ("workspace" in arg) {
workspace.id = arg.workspace.id;
workspace.prebuild = arg.workspace.type === "prebuild";
workspace.organizationId = arg.workspace.organizationId;
workspace.name = arg.workspace.description;
workspace.pinned = arg.workspace.pinned ?? false;
const contextUrl = ContextURL.normalize(arg.workspace);
if (contextUrl) {
workspace.contextUrl = contextUrl;
}
if (WithPrebuild.is(arg.workspace.context)) {
workspace.prebuildId = arg.workspace.context.prebuildWorkspaceId;
}
const status = new WorkspaceStatus();
const phase = new WorkspacePhase();
phase.lastTransitionTime = Timestamp.fromDate(new Date(arg.workspace.creationTime));
status.phase = phase;
status.admission = this.toAdmission(arg.workspace.shareable);
status.gitStatus = this.toGitStatus(arg.workspace);
workspace.status = status;
workspace.additionalEnvironmentVariables = this.toEnvironmentVariables(arg.workspace.context);
if (arg.latestInstance) {
return this.toWorkspace(arg.latestInstance, workspace);
}
return workspace;
}
const status = workspace.status ?? new WorkspaceStatus();
workspace.status = status;
const phase = status.phase ?? new WorkspacePhase();
phase.name = this.toPhase(arg);
status.phase = phase;
let lastTransitionTime = new Date(arg.creationTime).getTime();
if (phase.lastTransitionTime) {
lastTransitionTime = Math.max(lastTransitionTime, new Date(phase.lastTransitionTime.toDate()).getTime());
}
if (arg.deployedTime) {
lastTransitionTime = Math.max(lastTransitionTime, new Date(arg.deployedTime).getTime());
}
if (arg.startedTime) {
lastTransitionTime = Math.max(lastTransitionTime, new Date(arg.startedTime).getTime());
}
if (arg.stoppingTime) {
lastTransitionTime = Math.max(lastTransitionTime, new Date(arg.stoppingTime).getTime());
}
if (arg.stoppedTime) {
lastTransitionTime = Math.max(lastTransitionTime, new Date(arg.stoppedTime).getTime());
}
phase.lastTransitionTime = Timestamp.fromDate(new Date(lastTransitionTime));
status.instanceId = arg.id;
status.message = arg.status.message;
status.workspaceUrl = arg.ideUrl;
status.ports = this.toPorts(arg.status.exposedPorts);
status.conditions = this.toWorkspaceConditions(arg.status.conditions);
status.gitStatus = this.toGitStatus(arg, status.gitStatus);
workspace.region = arg.region;
workspace.workspaceClass = arg.workspaceClass;
workspace.editor = this.toEditor(arg.configuration.ideConfig);
return workspace;
}
toWorkspaceConditions(conditions: WorkspaceInstanceConditions): WorkspaceConditions {
const result = new WorkspaceConditions();
result.failed = conditions.failed;
result.timeout = conditions.timeout;
return result;
}
toEditor(ideConfig: ConfigurationIdeConfig | undefined): EditorReference | undefined {
if (!ideConfig?.ide) {
return undefined;
}
const result = new EditorReference();
result.name = ideConfig.ide;
result.version = ideConfig.useLatest ? "latest" : "stable";
return result;
}
toError(reason: unknown): ConnectError {
if (reason instanceof ConnectError) {
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.NOT_FOUND) {
return new ConnectError(reason.message, Code.NotFound, metadata, undefined, reason);
}
if (reason.code === ErrorCodes.NOT_AUTHENTICATED) {
return new ConnectError(reason.message, Code.Unauthenticated, metadata, 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.CONFLICT) {
return new ConnectError(reason.message, Code.AlreadyExists, metadata, undefined, reason);
}
if (reason.code === ErrorCodes.PRECONDITION_FAILED) {
return new ConnectError(reason.message, Code.FailedPrecondition, metadata, 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.InvalidArgument, metadata, undefined, reason);
}
return ConnectError.from(reason, Code.Internal);
}
fromError(reason: ConnectError): Error {
const codeMetadata = reason.metadata?.get(applicationErrorCode);
if (!codeMetadata) {
return reason;
}
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);
}
}
// data is trusted here, since it was scrubbed before on the server
return new ApplicationError(code, reason.message, new TrustedValue(data));
}
toEnvironmentVariables(context: WorkspaceContext): WorkspaceEnvironmentVariable[] {
if (WithEnvvarsContext.is(context)) {
return context.envvars.map((envvar) => this.toEnvironmentVariable(envvar));
}
return [];
}
toEnvironmentVariable(envVar: EnvVarWithValue): WorkspaceEnvironmentVariable {
const result = new WorkspaceEnvironmentVariable();
result.name = envVar.name;
envVar.value = envVar.value;
return result;
}
toAdmission(shareable: boolean | undefined): AdmissionLevel {
if (shareable) {
return AdmissionLevel.EVERYONE;
}
return AdmissionLevel.OWNER_ONLY;
}
toPorts(ports: WorkspaceInstancePort[] | undefined): WorkspacePort[] {
if (!ports) {
return [];
}
return ports.map((port) => this.toPort(port));
}
toPort(port: WorkspaceInstancePort): WorkspacePort {
const result = new WorkspacePort();
result.port = BigInt(port.port);
if (port.url) {
result.url = port.url;
}
result.policy = this.toPortPolicy(port.visibility);
result.protocol = this.toPortProtocol(port.protocol);
return result;
}
toPortProtocol(protocol: PortProtocol | undefined): WorkspacePort_Protocol {
switch (protocol) {
case "https":
return WorkspacePort_Protocol.HTTPS;
default:
return WorkspacePort_Protocol.HTTP;
}
}
toPortPolicy(visibility: string | undefined): WorkspacePort_Policy {
switch (visibility) {
case "public":
return WorkspacePort_Policy.PUBLIC;
default:
return WorkspacePort_Policy.PRIVATE;
}
}
toGitStatus(
arg: WorkspaceInfo | ProtocolWorkspace | WorkspaceInstance,
current?: WorkspaceGitStatus,
): WorkspaceGitStatus {
let result = current ?? new WorkspaceGitStatus();
if ("workspace" in arg) {
result = this.toGitStatus(arg.workspace, result);
if (arg.latestInstance) {
result = this.toGitStatus(arg.latestInstance, result);
}
return result;
}
if ("context" in arg) {
const context = arg.context;
if (CommitContext.is(context)) {
result.cloneUrl = context.repository.cloneUrl;
if (context.ref) {
result.branch = context.ref;
}
result.latestCommit = context.revision;
}
return result;
}
const gitStatus = arg?.gitStatus;
if (gitStatus) {
result.branch = gitStatus.branch ?? result.branch;
result.latestCommit = gitStatus.latestCommit ?? result.latestCommit;
result.uncommitedFiles = gitStatus.uncommitedFiles || [];
result.totalUncommitedFiles = gitStatus.totalUncommitedFiles || 0;
result.untrackedFiles = gitStatus.untrackedFiles || [];
result.totalUntrackedFiles = gitStatus.totalUntrackedFiles || 0;
result.unpushedCommits = gitStatus.unpushedCommits || [];
result.totalUnpushedCommits = gitStatus.totalUnpushedCommits || 0;
}
return result;
}
toPhase(arg: WorkspaceInstance): WorkspacePhase_Phase {
if ("status" in arg) {
switch (arg.status.phase) {
case "unknown":
return WorkspacePhase_Phase.UNSPECIFIED;
case "preparing":
return WorkspacePhase_Phase.PREPARING;
case "building":
return WorkspacePhase_Phase.IMAGEBUILD;
case "pending":
return WorkspacePhase_Phase.PENDING;
case "creating":
return WorkspacePhase_Phase.CREATING;
case "initializing":
return WorkspacePhase_Phase.INITIALIZING;
case "running":
return WorkspacePhase_Phase.RUNNING;
case "interrupted":
return WorkspacePhase_Phase.INTERRUPTED;
case "stopping":
return WorkspacePhase_Phase.STOPPING;
case "stopped":
return WorkspacePhase_Phase.STOPPED;
}
}
return WorkspacePhase_Phase.UNSPECIFIED;
}
toOrganization(org: ProtocolOrganization): Organization {
const result = new Organization();
result.id = org.id;
result.name = org.name;
result.slug = org.slug || "";
result.creationTime = Timestamp.fromDate(new Date(org.creationTime));
return result;
}
toOrganizationMember(member: OrgMemberInfo): OrganizationMember {
const result = new OrganizationMember();
result.userId = member.userId;
result.fullName = member.fullName;
result.email = member.primaryEmail;
result.avatarUrl = member.avatarUrl;
result.role = this.toOrgMemberRole(member.role);
result.memberSince = Timestamp.fromDate(new Date(member.memberSince));
result.ownedByOrganization = member.ownedByOrganization;
return result;
}
toOrgMemberRole(role: OrgMemberRole): OrganizationRole {
switch (role) {
case "owner":
return OrganizationRole.OWNER;
case "member":
return OrganizationRole.MEMBER;
default:
return OrganizationRole.UNSPECIFIED;
}
}
fromOrgMemberRole(role: OrganizationRole): OrgMemberRole {
switch (role) {
case OrganizationRole.OWNER:
return "owner";
case OrganizationRole.MEMBER:
return "member";
default:
throw new Error(`unknown org member role ${role}`);
}
}
toOrganizationSettings(settings: OrganizationSettingsProtocol): OrganizationSettings {
const result = new OrganizationSettings();
result.workspaceSharingDisabled = !!settings.workspaceSharingDisabled;
result.defaultWorkspaceImage = settings.defaultWorkspaceImage || undefined;
return result;
}
toConfiguration(project: Project): Configuration {
const result = new Configuration();
result.id = project.id;
result.organizationId = project.teamId;
result.name = project.name;
result.cloneUrl = project.cloneUrl;
result.workspaceSettings = this.toWorkspaceSettings(project.settings?.workspaceClasses?.regular);
result.prebuildSettings = this.toPrebuildSettings(project.settings?.prebuilds);
return result;
}
toPrebuildSettings(prebuilds?: PrebuildSettingsProtocol): PrebuildSettings {
const result = new PrebuildSettings();
if (prebuilds) {
result.enabled = !!prebuilds.enable;
result.branchMatchingPattern = prebuilds.branchMatchingPattern ?? "";
result.branchStrategy = this.toBranchMatchingStrategy(prebuilds.branchStrategy);
result.prebuildInterval = prebuilds.prebuildInterval ?? 20;
result.workspaceClass = prebuilds.workspaceClass ?? "";
}
return result;
}
toBranchMatchingStrategy(branchStrategy?: PrebuildSettingsProtocol.BranchStrategy): BranchMatchingStrategy {
switch (branchStrategy) {
case "default-branch":
return BranchMatchingStrategy.DEFAULT_BRANCH;
case "all-branches":
return BranchMatchingStrategy.ALL_BRANCHES;
case "matched-branches":
return BranchMatchingStrategy.MATCHED_BRANCHES;
}
return BranchMatchingStrategy.DEFAULT_BRANCH;
}
toWorkspaceSettings(workspaceClass?: string): WorkspaceSettings {
const result = new WorkspaceSettings();
if (workspaceClass) {
result.workspaceClass = workspaceClass;
}
return result;
}
}