mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
* 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
431 lines
16 KiB
TypeScript
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;
|
|
}
|
|
}
|