Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 7 additions & 60 deletions components/dashboard/src/service/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import { CallOptions, Code, ConnectError, PromiseClient, createPromiseClient } f
import { createConnectTransport } from "@connectrpc/connect-web";
import { Disposable } from "@gitpod/gitpod-protocol";
import { PublicAPIConverter } from "@gitpod/public-api-common/lib/public-api-converter";
import { Project as ProtocolProject } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connect";
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connect";
import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organization_connect";
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
Expand Down Expand Up @@ -52,10 +49,6 @@ export const converter = new PublicAPIConverter();

export const helloService = createServiceClient(HelloService);
export const personalAccessTokensService = createPromiseClient(TokensService, transport);
/**
* @deprecated use configurationClient instead
*/
export const projectsService = createPromiseClient(ProjectsService, transport);

export const oidcService = createPromiseClient(OIDCService, transport);

Expand All @@ -68,7 +61,7 @@ export const organizationClient = createServiceClient(OrganizationService, {
featureFlagSuffix: "organization",
});

// No jsonrcp client for the configuration service as it's only used in new UI of the dashboard
// No jsonrpc client for the configuration service as it's only used in new UI of the dashboard
export const configurationClient = createServiceClient(ConfigurationService);
export const prebuildClient = createServiceClient(PrebuildService, {
client: new JsonRpcPrebuildClient(),
Expand Down Expand Up @@ -110,56 +103,6 @@ export const installationClient = createServiceClient(InstallationService, {
featureFlagSuffix: "installation",
});

export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
let pagination = {
page: 1,
pageSize: 100,
};

const response = await projectsService.listProjects({
teamId: opts.orgId,
pagination,
});
const results = response.projects;

while (results.length < response.totalResults) {
pagination = {
pageSize: 100,
page: 1 + pagination.page,
};
const response = await projectsService.listProjects({
teamId: opts.orgId,
pagination,
});
results.push(...response.projects);
}

return results.map(projectToProtocol);
}

export function projectToProtocol(project: Project): ProtocolProject {
return {
id: project.id,
name: project.name,
cloneUrl: project.cloneUrl,
creationTime: project.creationTime?.toDate().toISOString() || "",
teamId: project.teamId,
appInstallationId: "undefined",
settings: {
workspaceClasses: {
regular: project.settings?.workspace?.workspaceClass?.regular || "",
},
prebuilds: {
enable: project.settings?.prebuild?.enablePrebuilds,
branchStrategy: project.settings?.prebuild?.branchStrategy as any,
branchMatchingPattern: project.settings?.prebuild?.branchMatchingPattern,
prebuildInterval: project.settings?.prebuild?.prebuildInterval,
workspaceClass: project.settings?.prebuild?.workspaceClass,
},
},
};
}

let user: { id: string; email?: string } | undefined;
export function updateUserForExperiments(newUser?: { id: string; email?: string }) {
user = newUser;
Expand All @@ -176,10 +119,13 @@ function createServiceClient<T extends ServiceType>(
get(grpcClient, prop) {
const experimentsClient = getExperimentsClient();
// TODO(ak) remove after migration
async function resolveClient(): Promise<PromiseClient<T>> {
async function resolveClient(preferJsonRpc?: boolean): Promise<PromiseClient<T>> {
if (!jsonRpcOptions) {
return grpcClient;
}
if (preferJsonRpc) {
return jsonRpcOptions.client;
}
const featureFlags = [`dashboard_public_api_${jsonRpcOptions.featureFlagSuffix}_enabled`];
const resolvedFlags = await Promise.all(
featureFlags.map((ff) =>
Expand Down Expand Up @@ -241,7 +187,8 @@ function createServiceClient<T extends ServiceType>(
}
return (async function* () {
try {
const client = await resolveClient();
// for server streaming, we prefer jsonRPC
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for future reference: the only streaming methods we implement in our public api v1 are:

  • WorkspaceService.WatchWorkspaceStatus
  • PrebuildService.WatchPrebuild

const client = await resolveClient(true);
const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator<any>;
for await (const item of generator) {
yield item;
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/service/service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { sendTrackEvent } from "../Analytics";
export const gitpodHostUrl = new GitpodHostUrl(window.location.toString());

function createGitpodService<C extends GitpodClient, S extends GitpodServer>() {
let host = gitpodHostUrl.asWebsocket().with({ pathname: GitpodServerPath }).withApi();
const host = gitpodHostUrl.asWebsocket().with({ pathname: GitpodServerPath }).withApi();

const connectionProvider = new WebSocketConnectionProvider();
instrumentWebSocketConnection(connectionProvider);
Expand Down
10 changes: 5 additions & 5 deletions components/gitpod-db/src/redis/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export class RedisPublisher {
constructor(@inject(Redis) private readonly redis: Redis) {}

async publishPrebuildUpdate(update: RedisPrebuildUpdate): Promise<void> {
log.debug("[redis] Publish prebuild udpate invoked.");
log.debug("[redis] Publish prebuild update invoked.");

let err: Error | undefined;
try {
const serialized = JSON.stringify(update);
await this.redis.publish(PrebuildUpdatesChannel, serialized);
log.debug("[redis] Succesfully published prebuild update.", update);
log.debug("[redis] Successfully published prebuild update.", update);
} catch (e) {
err = e;
log.error("[redis] Failed to publish prebuild update.", e, update);
Expand All @@ -43,7 +43,7 @@ export class RedisPublisher {
try {
const serialized = JSON.stringify(update);
await this.redis.publish(WorkspaceInstanceUpdatesChannel, serialized);
log.debug("[redis] Succesfully published instance update.", update);
log.debug("[redis] Successfully published instance update.", update);
} catch (e) {
err = e;
log.error("[redis] Failed to publish instance update.", e, update);
Expand All @@ -53,13 +53,13 @@ export class RedisPublisher {
}

async publishHeadlessUpdate(update: RedisHeadlessUpdate): Promise<void> {
log.debug("[redis] Publish headless udpate invoked.");
log.debug("[redis] Publish headless update invoked.");

let err: Error | undefined;
try {
const serialized = JSON.stringify(update);
await this.redis.publish(HeadlessUpdatesChannel, serialized);
log.debug("[redis] Succesfully published headless update.", update);
log.debug("[redis] Successfully published headless update.", update);
} catch (e) {
err = e;
log.error("[redis] Failed to publish headless update.", e, update);
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-protocol/src/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type RedisPrebuildUpdate = {
prebuildID: string;
workspaceID: string;
projectID: string;
organizationID?: string;
};

export type RedisHeadlessUpdate = {
Expand Down
13 changes: 10 additions & 3 deletions components/server/src/messaging/redis-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class RedisSubscriber {
let err: Error | undefined;
try {
await this.onMessage(channel, message);
log.debug("[redis] Succesfully handled update", { channel, message });
log.debug("[redis] Successfully handled update", { channel, message });
} catch (e) {
err = e;
log.error("[redis] Failed to handle message from Pub/Sub", e, { channel, message });
Expand Down Expand Up @@ -132,7 +132,10 @@ export class RedisSubscriber {
return;
}

const listeners = this.prebuildUpdateListeners.get(update.projectID) || [];
const listeners = this.prebuildUpdateListeners.get(update.projectID) ?? [];
if (update.organizationID) {
listeners.push(...(this.prebuildUpdateListeners.get(update.organizationID) ?? []));
}
if (listeners.length === 0) {
return;
}
Expand Down Expand Up @@ -182,10 +185,14 @@ export class RedisSubscriber {
this.disposables.dispose();
}

listenForPrebuildUpdates(projectId: string, listener: PrebuildUpdateListener): Disposable {
listenForProjectPrebuildUpdates(projectId: string, listener: PrebuildUpdateListener): Disposable {
return this.doRegister(projectId, listener, this.prebuildUpdateListeners, "prebuild");
}

listenForOrganizationPrebuildUpdates(organizationId: string, listener: PrebuildUpdateListener): Disposable {
return this.doRegister(organizationId, listener, this.prebuildUpdateListeners, "prebuild");
}

listenForPrebuildUpdatableEvents(listener: HeadlessWorkspaceEventListener): Disposable {
// we're being cheap here in re-using a map where it just needs to be a plain array.
return this.doRegister(UNDEFINED_KEY, listener, this.headlessWorkspaceEventListeners, "prebuild-updatable");
Expand Down
2 changes: 1 addition & 1 deletion components/server/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class PrebuildManager {
await this.auth.checkPermissionOnProject(userId, "read_prebuild", configurationId);
return generateAsyncGenerator<PrebuildWithStatus>((sink) => {
try {
const toDispose = this.subscriber.listenForPrebuildUpdates(configurationId, (_ctx, prebuild) => {
const toDispose = this.subscriber.listenForProjectPrebuildUpdates(configurationId, (_ctx, prebuild) => {
sink.push(prebuild);
});
return () => {
Expand Down
87 changes: 34 additions & 53 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,30 +236,17 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
log.debug({ userId: this.userID }, "initializeClient");

this.listenForWorkspaceInstanceUpdates();
this.listenForPrebuildUpdates().catch((err) => log.error("error registering for prebuild updates", err));
this.listenForPrebuildUpdates(connectionCtx).catch((err) =>
log.error("error registering for prebuild updates", err),
);
}

private async listenForPrebuildUpdates() {
if (!this.client) {
return;
}

// todo(ft) disable registering for all updates from all projects by default and only listen to updates when the client is explicity interested in them
const disableWebsocketPrebuildUpdates = await getExperimentsClientForBackend().getValueAsync(
"disableWebsocketPrebuildUpdates",
false,
{
gitpodHost: this.config.hostUrl.url.host,
},
);
if (disableWebsocketPrebuildUpdates) {
log.info("ws prebuild updates disabled by feature flag");
private async listenForPrebuildUpdates(ctx?: TraceContext) {
const userId = this.userID;
if (!this.client || !userId) {
return;
}

// 'registering for prebuild updates for all projects this user has access to
const projects = await this.getAccessibleProjects();

const handler = (ctx: TraceContext, update: PrebuildWithStatus) =>
TraceContext.withSpan(
"forwardPrebuildUpdateToClient",
Expand All @@ -273,38 +260,30 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
);

if (!this.disposables.disposed) {
for (const project of projects) {
this.disposables.push(this.subscriber.listenForPrebuildUpdates(project.id, handler));
}
}

// TODO(at) we need to keep the list of accessible project up to date
}

private async getAccessibleProjects() {
const userId = this.userID;
if (!userId) {
return [];
await runWithRequestContext(
{
requestKind: "gitpod-server-impl-listener",
requestMethod: "listenForPrebuildUpdates",
signal: new AbortController().signal,
subjectId: SubjectId.fromUserId(userId),
},
async () => {
const organizations = await this.getTeams(ctx ?? {});
for (const organization of organizations) {
const hasPermission = await this.auth.hasPermissionOnOrganization(
userId,
"read_prebuild",
organization.id,
);
if (hasPermission) {
this.disposables.push(
this.subscriber.listenForOrganizationPrebuildUpdates(organization.id, handler),
);
}
}
},
);
}

// update all project this user has access to
// gpl: This call to runWithRequestContext is not nice, but it's only there to please the old impl for a limited time, so it's fine.
return runWithRequestContext(
{
requestKind: "gitpod-server-impl-listener",
requestMethod: "getAccessibleProjects",
signal: new AbortController().signal,
subjectId: SubjectId.fromUserId(userId),
},
async () => {
const allProjects: Project[] = [];
const teams = await this.organizationService.listOrganizationsByMember(userId, userId);
for (const team of teams) {
allProjects.push(...(await this.projectsService.getProjects(userId, team.id)));
}
return allProjects;
},
);
}

private listenForWorkspaceInstanceUpdates(): void {
Expand Down Expand Up @@ -497,9 +476,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

/**
* Returns the descriptions of auth providers. This also controls the visibility of
* auth providers on the dashbard.
* auth providers on the dashboard.
*
* If this call is unauthenticated (i.e. for anonumous users,) it returns only information
* If this call is unauthenticated (i.e. for anonymous users,) it returns only information
* necessary for the Login page.
*
* If there are built-in auth providers configured, only these are returned.
Expand Down Expand Up @@ -1701,7 +1680,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
ctx,
);

this.disposables.pushAll([this.subscriber.listenForPrebuildUpdates(project.id, prebuildUpdateHandler)]);
this.disposables.pushAll([
this.subscriber.listenForProjectPrebuildUpdates(project.id, prebuildUpdateHandler),
]);
}

return project;
Expand Down
1 change: 1 addition & 0 deletions components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ export class WorkspaceStarter {
prebuildID: prebuild.id,
projectID: prebuild.projectId,
workspaceID: workspace.id,
organizationID: workspace.organizationId,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions components/ws-manager-bridge/src/prebuild-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class PrebuildUpdater {
prebuildID: updatedPrebuild.id,
status: updatedPrebuild.state,
workspaceID: workspaceId,
organizationID: info.teamId,
});
}
}
Expand Down Expand Up @@ -127,6 +128,7 @@ export class PrebuildUpdater {
prebuildID: prebuild.id,
status: prebuild.state,
workspaceID: instance.workspaceId,
organizationID: info.teamId,
});
}
}
Expand Down
Loading