Skip to content

Commit d71b7f7

Browse files
authored
Merge pull request #8492 from sagemathinc/api-admin-project-quotas
next/api/v2: add set-admin-quotas endpoint
2 parents 0a31bf5 + cc947d5 commit d71b7f7

File tree

5 files changed

+165
-9
lines changed

5 files changed

+165
-9
lines changed

src/CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ CoCalc is organized as a monorepo with key packages:
8888
- **Event Emitters**: Inter-service communication within backend
8989
- **REST-like APIs**: Some HTTP endpoints for specific operations
9090
- **API Schema**: API endpoints in `packages/next/pages/api/v2/` use Zod schemas in `packages/next/lib/api/schema/` for validation. These schemas must be kept in harmony with the TypeScript types sent from frontend applications using `apiPost` (in `packages/next/lib/api/post.ts`) or `api` (in `packages/frontend/client/api.ts`). When adding new fields to API requests, both the frontend types and the API schema validation must be updated.
91+
- **Conat Frontend → Hub Communication**: CoCalc uses a custom distributed messaging system called "Conat" for frontend-to-hub communication:
92+
1. **Frontend ConatClient** (`packages/frontend/conat/client.ts`): Manages WebSocket connection to hub, handles authentication, reconnection, and provides API interfaces
93+
2. **Core Protocol** (`packages/conat/core/client.ts`): NATS-like pub/sub/request/response messaging with automatic chunking, multiple encoding formats (MsgPack, JSON), and delivery confirmation
94+
3. **Hub API Structure** (`packages/conat/hub/api/`): Typed interfaces for different services (system, projects, db, purchases, jupyter) that map function calls to conat subjects
95+
4. **Message Flow**: Frontend calls like `hub.projects.setQuotas()` → ConatClient.callHub() → conat request to subject `hub.account.{account_id}.api` → Hub API dispatcher → actual service implementation
96+
5. **Authentication**: Each conat request includes account_id and is subject to permission checks at the hub level
97+
6. **Subjects**: Messages are routed using hierarchical subjects like `hub.account.{uuid}.{service}` or `project.{uuid}.{compute_server_id}.{service}`
9198

9299
### Key Technologies
93100

src/packages/frontend/cspell.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"tolerations",
4343
"undelete",
4444
"undeleting",
45-
"revealjs"
45+
"revealjs",
46+
"conat",
47+
"SocketIO"
4648
],
4749
"ignoreWords": [
4850
"antd",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { z } from "../../framework";
2+
3+
import {
4+
FailedAPIOperationSchema,
5+
SuccessfulAPIOperationSchema,
6+
} from "../common";
7+
8+
import { ProjectIdSchema } from "./common";
9+
10+
// OpenAPI spec
11+
//
12+
export const SetAdminQuotasInputSchema = z
13+
.object({
14+
project_id: ProjectIdSchema.describe("Project id to set quotas for."),
15+
memory_limit: z
16+
.number()
17+
.nonnegative()
18+
.optional()
19+
.describe("Memory limit in MB"),
20+
memory_request: z
21+
.number()
22+
.nonnegative()
23+
.optional()
24+
.describe("Memory request in MB"),
25+
cpu_request: z
26+
.number()
27+
.nonnegative()
28+
.optional()
29+
.describe("CPU request (number of cores)"),
30+
cpu_limit: z
31+
.number()
32+
.nonnegative()
33+
.optional()
34+
.describe("CPU limit (number of cores)"),
35+
disk_quota: z
36+
.number()
37+
.nonnegative()
38+
.optional()
39+
.describe("Disk quota in MB"),
40+
idle_timeout: z
41+
.number()
42+
.nonnegative()
43+
.optional()
44+
.describe("Idle timeout in seconds"),
45+
internet: z.boolean().optional().describe("Internet access"),
46+
member_host: z.boolean().optional().describe("Member hosting"),
47+
always_running: z.boolean().optional().describe("Always running"),
48+
})
49+
.describe(
50+
"**Administrators only**. Used to set project quotas as an admin. Important: you have to stop and start the project after any change.",
51+
);
52+
53+
export const SetAdminQuotasOutputSchema = z.union([
54+
FailedAPIOperationSchema,
55+
SuccessfulAPIOperationSchema,
56+
]);
57+
58+
export type SetAdminQuotasInput = z.infer<typeof SetAdminQuotasInputSchema>;
59+
export type SetAdminQuotasOutput = z.infer<typeof SetAdminQuotasOutputSchema>;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
API endpoint to set project quotas as admin.
3+
4+
This requires the user to be an admin.
5+
*/
6+
7+
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
8+
import { setQuotas } from "@cocalc/server/conat/api/projects";
9+
import getAccountId from "lib/account/get-account";
10+
import { apiRoute, apiRouteOperation } from "lib/api";
11+
import getParams from "lib/api/get-params";
12+
import {
13+
SetAdminQuotasInputSchema,
14+
SetAdminQuotasOutputSchema,
15+
} from "lib/api/schema/projects/set-admin-quotas";
16+
import { SuccessStatus } from "lib/api/status";
17+
18+
async function handle(req, res) {
19+
try {
20+
res.json(await get(req));
21+
} catch (err) {
22+
res.json({ error: `${err.message}` });
23+
return;
24+
}
25+
}
26+
27+
async function get(req) {
28+
const account_id = await getAccountId(req);
29+
if (account_id == null) {
30+
throw Error("must be signed in");
31+
}
32+
// This user MUST be an admin:
33+
if (!(await userIsInGroup(account_id, "admin"))) {
34+
throw Error("only admins can set project quotas");
35+
}
36+
37+
const {
38+
project_id,
39+
memory_limit,
40+
memory_request,
41+
cpu_request,
42+
cpu_limit,
43+
disk_quota,
44+
idle_timeout,
45+
internet,
46+
member_host,
47+
always_running,
48+
} = getParams(req);
49+
50+
await setQuotas({
51+
account_id,
52+
project_id,
53+
memory: memory_limit,
54+
memory_request,
55+
cpu_shares:
56+
cpu_request != null ? Math.round(cpu_request * 1024) : undefined,
57+
cores: cpu_limit,
58+
disk_quota,
59+
mintime: idle_timeout,
60+
network: internet != null ? (internet ? 1 : 0) : undefined,
61+
member_host: member_host != null ? (member_host ? 1 : 0) : undefined,
62+
always_running:
63+
always_running != null ? (always_running ? 1 : 0) : undefined,
64+
});
65+
66+
return SuccessStatus;
67+
}
68+
69+
export default apiRoute({
70+
setAdminQuotas: apiRouteOperation({
71+
method: "POST",
72+
openApiOperation: {
73+
tags: ["Projects", "Admin"],
74+
},
75+
})
76+
.input({
77+
contentType: "application/json",
78+
body: SetAdminQuotasInputSchema,
79+
})
80+
.outputs([
81+
{
82+
status: 200,
83+
contentType: "application/json",
84+
body: SetAdminQuotasOutputSchema,
85+
},
86+
])
87+
.handler(handle),
88+
});

src/packages/server/conat/api/projects.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { delay } from "awaiting";
2+
13
import createProject from "@cocalc/server/projects/create";
24
export { createProject };
3-
import { type UserCopyOptions } from "@cocalc/util/db-schema/projects";
4-
import { getProject } from "@cocalc/server/projects/control";
5-
import isCollaborator from "@cocalc/server/projects/is-collaborator";
6-
import { delay } from "awaiting";
5+
6+
import isAdmin from "@cocalc/server/accounts/is-admin";
7+
import { getProject } from "@cocalc/server/projects/control";
8+
import isCollaborator from "@cocalc/server/projects/is-collaborator";
9+
import { type UserCopyOptions } from "@cocalc/util/db-schema/projects";
710
export * from "@cocalc/server/projects/collaborators";
8-
import isAdmin from "@cocalc/server/accounts/is-admin";
911

1012
export async function copyPathBetweenProjects(
1113
opts: UserCopyOptions,
@@ -45,8 +47,8 @@ async function doCopyPathBetweenProjects(opts: UserCopyOptions) {
4547
}
4648
}
4749

48-
import { callback2 } from "@cocalc/util/async-utils";
4950
import { db } from "@cocalc/database";
51+
import { callback2 } from "@cocalc/util/async-utils";
5052

5153
export async function setQuotas(opts: {
5254
account_id: string;
@@ -73,5 +75,3 @@ export async function setQuotas(opts: {
7375
// @ts-ignore
7476
await project?.setAllQuotas();
7577
}
76-
77-

0 commit comments

Comments
 (0)