Skip to content

Commit 93ec6ab

Browse files
akosyakovgeropl
andauthored
[fga] extract WorkspaceService.start (#18467)
* [server] WorkspaceService.startWorkspace * [server] Start redis if not running * [server] Move regionCode handling into WorkspaceService.startWorkspace * [server] move "not-deleted" check into WorkspaceService.startWorkspace * fix sshkey tests --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent 513ca76 commit 93ec6ab

16 files changed

+482
-214
lines changed

components/server/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
"test:leeway": "yarn build && yarn test",
2020
"test": "yarn test:unit && yarn test:db",
2121
"test:unit": "mocha --opts mocha.opts './**/*.spec.js' --exclude './node_modules/**'",
22-
"test:db": "cleanup() { echo 'Cleanup started'; yarn stop-spicedb; }; trap cleanup EXIT; . $(leeway run components/gitpod-db:db-test-env) && yarn start-spicedb && mocha --opts mocha.opts './**/*.spec.db.js' --exclude './node_modules/**'",
22+
"test:db": "cleanup() { echo 'Cleanup started'; yarn stop-services; }; trap cleanup EXIT; . $(leeway run components/gitpod-db:db-test-env) && yarn start-services && mocha --opts mocha.opts './**/*.spec.db.js' --exclude './node_modules/**'",
2323
"start-services": "yarn start-testdb && yarn start-redis && yarn start-spicedb",
2424
"stop-services": "yarn stop-redis && yarn stop-spicedb",
25-
"start-testdb": "if netstat -tuln | grep ':23306 '; then echo 'Mysql is already running.'; else leeway run components/gitpod-db:init-testdb; fi",
25+
"start-testdb": "leeway run components/gitpod-db:init-testdb",
2626
"start-spicedb": "leeway run components/spicedb:start-spicedb",
2727
"stop-spicedb": "leeway run components/spicedb:stop-spicedb",
28-
"start-redis": "if netstat -tuln | grep ':6379 '; then echo 'Redis is already running.'; else docker run --rm --name test-redis -p 6379:6379 -d redis; fi",
28+
"start-redis": "if redis-cli -h ${REDIS_HOST:-0.0.0.0} -e ping; then echo 'Redis is already running.'; else docker run --rm --name test-redis -p 6379:6379 -d redis; fi",
2929
"stop-redis": "docker stop test-redis || true",
3030
"telepresence": "telepresence --swap-deployment server --method inject-tcp --run yarn start-inspect"
3131
},

components/server/src/api/teams.spec.db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { createConnectTransport } from "@bufbuild/connect-node";
1313
import { Code, ConnectError, PromiseClient, createPromiseClient } from "@bufbuild/connect";
1414
import { AddressInfo } from "net";
1515
import { TeamsService as TeamsServiceDefinition } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb";
16-
import { WorkspaceStarter } from "../workspace/workspace-starter";
1716
import { UserAuthentication } from "../user/user-authentication";
1817
import { APITeamsService } from "./teams";
1918
import { v4 as uuidv4 } from "uuid";
@@ -24,6 +23,7 @@ import { Connection } from "typeorm";
2423
import { Timestamp } from "@bufbuild/protobuf";
2524
import { APIWorkspacesService } from "./workspaces";
2625
import { APIStatsService } from "./stats";
26+
import { WorkspaceService } from "../workspace/workspace-service";
2727

2828
const expect = chai.expect;
2929

@@ -43,7 +43,7 @@ export class APITeamsServiceSpec {
4343
this.container.bind(APIWorkspacesService).toSelf().inSingletonScope();
4444
this.container.bind(APIStatsService).toSelf().inSingletonScope();
4545

46-
this.container.bind(WorkspaceStarter).toConstantValue({} as WorkspaceStarter);
46+
this.container.bind(WorkspaceService).toConstantValue({} as WorkspaceService);
4747
this.container.bind(UserAuthentication).toConstantValue({} as UserAuthentication);
4848

4949
// Clean-up database

components/server/src/api/user.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { suite, test } from "@testdeck/mocha";
88
import { APIUserService } from "./user";
99
import { Container } from "inversify";
1010
import { testContainer } from "@gitpod/gitpod-db/lib";
11-
import { WorkspaceStarter } from "../workspace/workspace-starter";
1211
import { UserAuthentication } from "../user/user-authentication";
1312
import { BlockUserRequest, BlockUserResponse } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_pb";
1413
import { User } from "@gitpod/gitpod-protocol";
@@ -18,24 +17,26 @@ import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
1817
import { v4 as uuidv4 } from "uuid";
1918
import { ConnectError, Code } from "@bufbuild/connect";
2019
import * as chai from "chai";
20+
import { WorkspaceService } from "../workspace/workspace-service";
2121

2222
const expect = chai.expect;
2323

2424
@suite()
2525
export class APIUserServiceSpec {
2626
private container: Container;
27-
private workspaceStarterMock: WorkspaceStarter = {
27+
private workspaceStarterMock: WorkspaceService = {
2828
stopRunningWorkspacesForUser: async (
2929
ctx: TraceContext,
30-
userID: string,
30+
userId: string,
31+
userIdToStop: string,
3132
reason: string,
3233
policy?: StopWorkspacePolicy,
3334
): Promise<Workspace[]> => {
3435
return [];
3536
},
36-
} as WorkspaceStarter;
37+
} as WorkspaceService;
3738
private userServiceMock: UserAuthentication = {
38-
blockUser: async (targetUserId: string, block: boolean): Promise<User> => {
39+
blockUser: async (userId: string, targetUserId: string, block: boolean): Promise<User> => {
3940
return {
4041
id: targetUserId,
4142
} as User;
@@ -45,7 +46,7 @@ export class APIUserServiceSpec {
4546
async before() {
4647
this.container = testContainer.createChild();
4748

48-
this.container.bind(WorkspaceStarter).toConstantValue(this.workspaceStarterMock);
49+
this.container.bind(WorkspaceService).toConstantValue(this.workspaceStarterMock);
4950
this.container.bind(UserAuthentication).toConstantValue(this.userServiceMock);
5051
this.container.bind(APIUserService).toSelf().inSingletonScope();
5152
}

components/server/src/api/user.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ import {
2323
GetGitTokenResponse,
2424
BlockUserResponse,
2525
} from "@gitpod/public-api/lib/gitpod/experimental/v1/user_pb";
26-
import { WorkspaceStarter } from "../workspace/workspace-starter";
2726
import { UserAuthentication } from "../user/user-authentication";
27+
import { WorkspaceService } from "../workspace/workspace-service";
28+
import { SYSTEM_USER } from "../authorization/authorizer";
2829
import { validate } from "uuid";
29-
import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
3030
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
31+
import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
3132

3233
@injectable()
3334
export class APIUserService implements ServiceImpl<typeof UserServiceInterface> {
34-
@inject(WorkspaceStarter) protected readonly workspaceStarter: WorkspaceStarter;
35+
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
3536
@inject(UserAuthentication) protected readonly userService: UserAuthentication;
3637

3738
public async getAuthenticatedUser(req: GetAuthenticatedUserRequest): Promise<GetAuthenticatedUserResponse> {
@@ -73,14 +74,17 @@ export class APIUserService implements ServiceImpl<typeof UserServiceInterface>
7374

7475
// TODO: Once connect-node supports middlewares, lift the tracing into the middleware.
7576
const trace = {};
76-
await this.userService.blockUser(userId, true);
77+
// TODO for now we use SYSTEM_USER, since it is only called by internal componenets like usage
78+
// and not exposed publically, but there should be better way to get an authenticated user
79+
await this.userService.blockUser(SYSTEM_USER, userId, true);
7780
log.info(`Blocked user ${userId}.`, {
7881
userId,
7982
reason,
8083
});
8184

82-
const stoppedWorkspaces = await this.workspaceStarter.stopRunningWorkspacesForUser(
85+
const stoppedWorkspaces = await this.workspaceService.stopRunningWorkspacesForUser(
8386
trace,
87+
SYSTEM_USER,
8488
userId,
8589
reason,
8690
StopWorkspacePolicy.IMMEDIATELY,

components/server/src/authorization/authorizer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class Authorizer {
5959
permission: OrganizationPermission,
6060
orgId: string,
6161
): Promise<boolean> {
62-
if (userId === "SYSTEM_USER") {
62+
if (userId === SYSTEM_USER) {
6363
return true;
6464
}
6565

@@ -89,7 +89,7 @@ export class Authorizer {
8989
}
9090

9191
async hasPermissionOnProject(userId: string, permission: ProjectPermission, projectId: string): Promise<boolean> {
92-
if (userId === "SYSTEM_USER") {
92+
if (userId === SYSTEM_USER) {
9393
return true;
9494
}
9595

@@ -119,7 +119,7 @@ export class Authorizer {
119119
}
120120

121121
async hasPermissionOnUser(userId: string, permission: UserPermission, resourceUserId: string): Promise<boolean> {
122-
if (userId === "SYSTEM_USER") {
122+
if (userId === SYSTEM_USER) {
123123
return true;
124124
}
125125

@@ -152,7 +152,7 @@ export class Authorizer {
152152
permission: WorkspacePermission,
153153
workspaceId: string,
154154
): Promise<boolean> {
155-
if (userId === "SYSTEM_USER") {
155+
if (userId === SYSTEM_USER) {
156156
return true;
157157
}
158158

components/server/src/authorization/definitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export type WorkspaceResourceType = "workspace";
7373

7474
export type WorkspaceRelation = "org" | "owner";
7575

76-
export type WorkspacePermission = "access" | "stop" | "delete" | "read_info";
76+
export type WorkspacePermission = "access" | "start" | "stop" | "delete" | "read_info";
7777

7878
export const rel = {
7979
user(id: string) {

components/server/src/test/service-testing-container-module.ts

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7+
import * as grpc from "@grpc/grpc-js";
78
import { v1 } from "@authzed/authzed-node";
89
import { IAnalyticsWriter, NullAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
9-
import { IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb";
10+
import { IDEServiceClient, IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb";
1011
import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
11-
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
1212
import { ContainerModule } from "inversify";
1313
import { v4 } from "uuid";
1414
import { AuthProviderParams } from "../auth/auth-provider";
15-
import { HostContextProviderFactory } from "../auth/host-context-provider";
15+
import { HostContextProvider, HostContextProviderFactory } from "../auth/host-context-provider";
1616
import { HostContextProviderImpl } from "../auth/host-context-provider-impl";
1717
import { SpiceDBClient } from "../authorization/spicedb";
1818
import { Config } from "../config";
@@ -21,7 +21,22 @@ import { testContainer } from "@gitpod/gitpod-db/lib";
2121
import { productionContainerModule } from "../container-module";
2222
import { createMock } from "./mocks/mock";
2323
import { UsageServiceClientMock } from "./mocks/usage-service-client-mock";
24-
import { env } from "process";
24+
import { env, nextTick } from "process";
25+
import { WorkspaceManagerClientProviderSource } from "@gitpod/ws-manager/lib/client-provider-source";
26+
import { WorkspaceClusterWoTLS } from "@gitpod/gitpod-protocol/lib/workspace-cluster";
27+
import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider";
28+
import {
29+
BuildInfo,
30+
BuildResponse,
31+
BuildStatus,
32+
IImageBuilderClient,
33+
LogInfo,
34+
ResolveWorkspaceImageResponse,
35+
} from "@gitpod/image-builder/lib";
36+
import { IWorkspaceManagerClient, StartWorkspaceResponse } from "@gitpod/ws-manager/lib";
37+
import { TokenProvider } from "../user/token-provider";
38+
import { GitHubScope } from "../github/scopes";
39+
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
2540

2641
/**
2742
* Expects a fully configured production container and
@@ -30,19 +45,141 @@ import { env } from "process";
3045
* - replaces the analytics writer with a null analytics writer
3146
*/
3247
const mockApplyingContainerModule = new ContainerModule((bind, unbound, isbound, rebind) => {
48+
rebind(HostContextProvider).toConstantValue({
49+
get: () => {
50+
const authProviderId = "Public-GitHub";
51+
return {
52+
authProvider: {
53+
authProviderId,
54+
},
55+
};
56+
},
57+
});
58+
rebind(TokenProvider).toConstantValue(<TokenProvider>{
59+
getTokenForHost: async () => {
60+
return {
61+
value: "test",
62+
scopes: [GitHubScope.EMAIL, GitHubScope.PUBLIC, GitHubScope.PRIVATE],
63+
};
64+
},
65+
});
3366
rebind(UsageServiceDefinition.name).toConstantValue(createMock(new UsageServiceClientMock()));
3467
rebind(StorageClient).toConstantValue(createMock());
35-
rebind(WorkspaceManagerClientProvider).toConstantValue(createMock());
36-
rebind(IDEServiceDefinition.name).toConstantValue(createMock());
68+
rebind(WorkspaceManagerClientProviderSource).toDynamicValue((): WorkspaceManagerClientProviderSource => {
69+
const clusters: WorkspaceClusterWoTLS[] = [
70+
{
71+
name: "eu-central-1",
72+
region: "europe",
73+
url: "https://ws.gitpod.io",
74+
state: "available",
75+
maxScore: 100,
76+
score: 100,
77+
govern: true,
78+
},
79+
];
80+
return <WorkspaceManagerClientProviderSource>{
81+
getAllWorkspaceClusters: async () => {
82+
return clusters;
83+
},
84+
getWorkspaceCluster: async (name: string) => {
85+
return clusters.find((c) => c.name === name);
86+
},
87+
};
88+
});
89+
rebind(WorkspaceManagerClientProvider)
90+
.toSelf()
91+
.onActivation((_, provider) => {
92+
provider["createConnection"] = () => {
93+
const channel = <Partial<grpc.Channel>>{
94+
getConnectivityState() {
95+
return grpc.connectivityState.READY;
96+
},
97+
};
98+
return Object.assign(
99+
<Partial<grpc.Client>>{
100+
getChannel() {
101+
return channel;
102+
},
103+
},
104+
<IImageBuilderClient & IWorkspaceManagerClient>{
105+
resolveWorkspaceImage(request, metadata, options, callback) {
106+
const response = new ResolveWorkspaceImageResponse();
107+
response.setStatus(BuildStatus.DONE_SUCCESS);
108+
callback(null, response);
109+
},
110+
build(request, metadata, options) {
111+
const listeners = new Map<string | symbol, Function>();
112+
nextTick(() => {
113+
const response = new BuildResponse();
114+
response.setStatus(BuildStatus.DONE_SUCCESS);
115+
response.setRef("my-test-build-ref");
116+
const buildInfo = new BuildInfo();
117+
const logInfo = new LogInfo();
118+
logInfo.setUrl("https://ws.gitpod.io/my-test-image-build/logs");
119+
buildInfo.setLogInfo(logInfo);
120+
response.setInfo(buildInfo);
121+
listeners.get("data")!(response);
122+
listeners.get("end")!();
123+
});
124+
return {
125+
on(event, callback) {
126+
listeners.set(event, callback);
127+
},
128+
};
129+
},
130+
startWorkspace(request, metadata, options, callback) {
131+
const workspaceId = request.getServicePrefix();
132+
const response = new StartWorkspaceResponse();
133+
response.setUrl(`https://${workspaceId}.ws.gitpod.io`);
134+
callback(null, response);
135+
},
136+
},
137+
) as any;
138+
};
139+
return provider;
140+
});
141+
rebind(IDEServiceDefinition.name).toConstantValue(
142+
createMock(<Partial<IDEServiceClient>>{
143+
async resolveWorkspaceConfig() {
144+
return {
145+
envvars: [],
146+
supervisorImage: "gitpod/supervisor:latest",
147+
webImage: "gitpod/code:latest",
148+
ideImageLayers: [],
149+
refererIde: "code",
150+
ideSettings: "",
151+
tasks: "",
152+
};
153+
},
154+
}),
155+
);
37156

38157
rebind<Partial<Config>>(Config).toConstantValue({
158+
hostUrl: new GitpodHostUrl("https://gitpod.io"),
39159
blockNewUsers: {
40160
enabled: false,
41161
passlist: [],
42162
},
43163
redis: {
44164
address: (env.REDIS_HOST || "127.0.0.1") + ":" + (env.REDIS_PORT || "6379"),
45165
},
166+
workspaceDefaults: {
167+
workspaceImage: "gitpod/workspace-full",
168+
defaultFeatureFlags: [],
169+
previewFeatureFlags: [],
170+
},
171+
workspaceClasses: [
172+
{
173+
category: "general",
174+
description: "The default workspace class",
175+
displayName: "Default",
176+
id: "default",
177+
isDefault: true,
178+
powerups: 0,
179+
},
180+
],
181+
authProviderConfigs: [],
182+
installationShortname: "gitpod",
46183
});
47184
rebind(IAnalyticsWriter).toConstantValue(NullAnalyticsWriter);
48185
rebind(HostContextProviderFactory)

components/server/src/user/sshkey-service.spec.db.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe("SSHKeyService", async () => {
7474
afterEach(async () => {
7575
// Clean-up database
7676
await resetDB(container.get(TypeORM));
77+
container.unbindAll();
7778
});
7879

7980
it("should add ssh key", async () => {

components/server/src/user/user-authentication.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export class UserAuthentication {
3939
@inject(HostContextProvider) private readonly hostContextProvider: HostContextProvider,
4040
) {}
4141

42-
async blockUser(targetUserId: string, block: boolean): Promise<User> {
43-
const target = await this.userService.findUserById(targetUserId, targetUserId);
42+
async blockUser(userId: string, targetUserId: string, block: boolean): Promise<User> {
43+
const target = await this.userService.findUserById(userId, targetUserId);
4444
if (!target) {
4545
throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found");
4646
}

0 commit comments

Comments
 (0)