Skip to content

Commit 81f6c4f

Browse files
authored
Merge pull request #136 from skulidropek/issue-135
feat: cap docker-git container CPU and RAM by default
2 parents 25b269d + 0ad8a04 commit 81f6c4f

30 files changed

+796
-298
lines changed

packages/api/src/api/contracts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export type CreateProjectRequest = {
4242
readonly envProjectPath?: string | undefined
4343
readonly codexAuthPath?: string | undefined
4444
readonly codexHome?: string | undefined
45+
readonly cpuLimit?: string | undefined
46+
readonly ramLimit?: string | undefined
4547
readonly dockerNetworkMode?: string | undefined
4648
readonly dockerSharedNetworkName?: string | undefined
4749
readonly enableMcpPlaywright?: boolean | undefined

packages/api/src/api/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const CreateProjectRequestSchema = Schema.Struct({
1818
envProjectPath: OptionalString,
1919
codexAuthPath: OptionalString,
2020
codexHome: OptionalString,
21+
cpuLimit: OptionalString,
22+
ramLimit: OptionalString,
2123
dockerNetworkMode: OptionalString,
2224
dockerSharedNetworkName: OptionalString,
2325
enableMcpPlaywright: OptionalBoolean,

packages/api/src/services/projects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ export const createProjectFromRequest = (
192192
...(request.envProjectPath === undefined ? {} : { envProjectPath: request.envProjectPath }),
193193
...(request.codexAuthPath === undefined ? {} : { codexAuthPath: request.codexAuthPath }),
194194
...(request.codexHome === undefined ? {} : { codexHome: request.codexHome }),
195+
...(request.cpuLimit === undefined ? {} : { cpuLimit: request.cpuLimit }),
196+
...(request.ramLimit === undefined ? {} : { ramLimit: request.ramLimit }),
195197
...(request.dockerNetworkMode === undefined ? {} : { dockerNetworkMode: request.dockerNetworkMode }),
196198
...(request.dockerSharedNetworkName === undefined ? {} : { dockerSharedNetworkName: request.dockerSharedNetworkName }),
197199
...(request.enableMcpPlaywright === undefined ? {} : { enableMcpPlaywright: request.enableMcpPlaywright }),

packages/api/src/ui.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,16 @@ export const uiHtml = `<!doctype html>
383383
</select>
384384
</div>
385385
</div>
386+
<div class="row" style="margin-top:0.5rem">
387+
<div>
388+
<label for="create-cpu">CPU limit</label>
389+
<input id="create-cpu" type="text" placeholder="30% or 1.5" />
390+
</div>
391+
<div>
392+
<label for="create-ram">RAM limit</label>
393+
<input id="create-ram" type="text" placeholder="30% or 4g" />
394+
</div>
395+
</div>
386396
<div class="checkbox-row" style="margin-top:0.6rem">
387397
<input id="create-up" type="checkbox" checked />
388398
<label for="create-up" style="margin:0">run up</label>
@@ -530,6 +540,8 @@ export const uiScript = `
530540
createRepoRef: byId('create-repo-ref'),
531541
createSshPort: byId('create-ssh-port'),
532542
createNetworkMode: byId('create-network-mode'),
543+
createCpu: byId('create-cpu'),
544+
createRam: byId('create-ram'),
533545
createUp: byId('create-up'),
534546
createForce: byId('create-force'),
535547
createForceEnv: byId('create-force-env')
@@ -818,6 +830,8 @@ export const uiScript = `
818830
repoUrl: views.createRepoUrl.value.trim() || undefined,
819831
repoRef: views.createRepoRef.value.trim() || undefined,
820832
sshPort: views.createSshPort.value.trim() || undefined,
833+
cpuLimit: views.createCpu.value.trim() || undefined,
834+
ramLimit: views.createRam.value.trim() || undefined,
821835
dockerNetworkMode: views.createNetworkMode.value.trim() || undefined,
822836
up: views.createUp.checked,
823837
force: views.createForce.checked,
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Either } from "effect"
22

33
import { type ApplyCommand, type ParseError } from "@effect-template/lib/core/domain"
4+
import { normalizeCpuLimit, normalizeRamLimit } from "@effect-template/lib/core/resource-limits"
45

56
import { parseProjectDirWithOptions } from "./parser-shared.js"
67

@@ -17,12 +18,19 @@ import { parseProjectDirWithOptions } from "./parser-shared.js"
1718
export const parseApply = (
1819
args: ReadonlyArray<string>
1920
): Either.Either<ApplyCommand, ParseError> =>
20-
Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
21-
_tag: "Apply",
22-
projectDir,
23-
runUp: raw.up ?? true,
24-
gitTokenLabel: raw.gitTokenLabel,
25-
codexTokenLabel: raw.codexTokenLabel,
26-
claudeTokenLabel: raw.claudeTokenLabel,
27-
enableMcpPlaywright: raw.enableMcpPlaywright
28-
}))
21+
Either.gen(function*(_) {
22+
const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args))
23+
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit, "--cpu"))
24+
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit, "--ram"))
25+
return {
26+
_tag: "Apply",
27+
projectDir,
28+
runUp: raw.up ?? true,
29+
gitTokenLabel: raw.gitTokenLabel,
30+
codexTokenLabel: raw.codexTokenLabel,
31+
claudeTokenLabel: raw.claudeTokenLabel,
32+
cpuLimit,
33+
ramLimit,
34+
enableMcpPlaywright: raw.enableMcpPlaywright
35+
}
36+
})

packages/app/src/docker-git/cli/parser-options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface ValueOptionSpec {
2020
| "envProjectPath"
2121
| "codexAuthPath"
2222
| "codexHome"
23+
| "cpuLimit"
24+
| "ramLimit"
2325
| "dockerNetworkMode"
2426
| "dockerSharedNetworkName"
2527
| "archivePath"
@@ -54,6 +56,10 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
5456
{ flag: "--env-project", key: "envProjectPath" },
5557
{ flag: "--codex-auth", key: "codexAuthPath" },
5658
{ flag: "--codex-home", key: "codexHome" },
59+
{ flag: "--cpu", key: "cpuLimit" },
60+
{ flag: "--cpus", key: "cpuLimit" },
61+
{ flag: "--ram", key: "ramLimit" },
62+
{ flag: "--memory", key: "ramLimit" },
5763
{ flag: "--network-mode", key: "dockerNetworkMode" },
5864
{ flag: "--shared-network", key: "dockerSharedNetworkName" },
5965
{ flag: "--archive", key: "archivePath" },
@@ -109,6 +115,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
109115
envProjectPath: (raw, value) => ({ ...raw, envProjectPath: value }),
110116
codexAuthPath: (raw, value) => ({ ...raw, codexAuthPath: value }),
111117
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
118+
cpuLimit: (raw, value) => ({ ...raw, cpuLimit: value }),
119+
ramLimit: (raw, value) => ({ ...raw, ramLimit: value }),
112120
dockerNetworkMode: (raw, value) => ({ ...raw, dockerNetworkMode: value }),
113121
dockerSharedNetworkName: (raw, value) => ({ ...raw, dockerSharedNetworkName: value }),
114122
archivePath: (raw, value) => ({ ...raw, archivePath: value }),

packages/app/src/docker-git/cli/usage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Options:
5050
--env-project <path> Host path to project env file (default: ./.orch/env/project.env)
5151
--codex-auth <path> Host path for Codex auth cache (default: <projectsRoot>/.orch/auth/codex)
5252
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
53+
--cpu <value> CPU limit: percent or cores (examples: 30%, 1.5; default: 30%)
54+
--ram <value> RAM limit: percent or size (examples: 30%, 512m, 4g; default: 30%)
5355
--network-mode <mode> Compose network mode: shared|project (default: shared)
5456
--shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
5557
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])

packages/app/src/docker-git/menu-create.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,38 @@ type CreateReturnContext = CreateContext & {
4545
readonly view: Extract<ViewState, { readonly _tag: "Create" }>
4646
}
4747

48+
type OptionalCreateArg = {
49+
readonly value: string
50+
readonly args: readonly [string, string]
51+
}
52+
53+
const optionalCreateArgs = (input: CreateInputs): ReadonlyArray<OptionalCreateArg> => [
54+
{ value: input.repoUrl, args: ["--repo-url", input.repoUrl] },
55+
{ value: input.repoRef, args: ["--repo-ref", input.repoRef] },
56+
{ value: input.outDir, args: ["--out-dir", input.outDir] },
57+
{ value: input.cpuLimit, args: ["--cpu", input.cpuLimit] },
58+
{ value: input.ramLimit, args: ["--ram", input.ramLimit] }
59+
]
60+
61+
const booleanCreateFlags = (input: CreateInputs): ReadonlyArray<string> =>
62+
[
63+
input.runUp ? null : "--no-up",
64+
input.enableMcpPlaywright ? "--mcp-playwright" : null,
65+
input.force ? "--force" : null,
66+
input.forceEnv ? "--force-env" : null
67+
].filter((value): value is string => value !== null)
68+
4869
export const buildCreateArgs = (input: CreateInputs): ReadonlyArray<string> => {
4970
const args: Array<string> = ["create"]
50-
if (input.repoUrl.length > 0) {
51-
args.push("--repo-url", input.repoUrl)
52-
}
53-
if (input.repoRef.length > 0) {
54-
args.push("--repo-ref", input.repoRef)
55-
}
56-
if (input.outDir.length > 0) {
57-
args.push("--out-dir", input.outDir)
58-
}
59-
if (!input.runUp) {
60-
args.push("--no-up")
61-
}
62-
if (input.enableMcpPlaywright) {
63-
args.push("--mcp-playwright")
64-
}
65-
if (input.force) {
66-
args.push("--force")
71+
72+
for (const spec of optionalCreateArgs(input)) {
73+
if (spec.value.length > 0) {
74+
args.push(spec.args[0], spec.args[1])
75+
}
6776
}
68-
if (input.forceEnv) {
69-
args.push("--force-env")
77+
78+
for (const flag of booleanCreateFlags(input)) {
79+
args.push(flag)
7080
}
7181
return args
7282
}
@@ -118,6 +128,8 @@ export const resolveCreateInputs = (
118128
repoUrl,
119129
repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
120130
outDir,
131+
cpuLimit: values.cpuLimit ?? "",
132+
ramLimit: values.ramLimit ?? "",
121133
runUp: values.runUp !== false,
122134
enableMcpPlaywright: values.enableMcpPlaywright === true,
123135
force: values.force === true,
@@ -196,6 +208,14 @@ const applyCreateStep = (input: {
196208
input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir
197209
return true
198210
}),
211+
Match.when("cpuLimit", () => {
212+
input.nextValues.cpuLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.cpuLimit
213+
return true
214+
}),
215+
Match.when("ramLimit", () => {
216+
input.nextValues.ramLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.ramLimit
217+
return true
218+
}),
199219
Match.when("runUp", () => {
200220
input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp)
201221
return true

packages/app/src/docker-git/menu-render.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const renderStepLabel = (step: CreateStep, defaults: CreateInputs): strin
3131
Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"),
3232
Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`),
3333
Match.when("outDir", () => `Output dir [${defaults.outDir}]`),
34+
Match.when("cpuLimit", () => `CPU limit [${defaults.cpuLimit || "30%"}]`),
35+
Match.when("ramLimit", () => `RAM limit [${defaults.ramLimit || "30%"}]`),
3436
Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`),
3537
Match.when(
3638
"mcpPlaywright",

packages/app/src/docker-git/menu-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export type CreateInputs = {
4848
readonly repoUrl: string
4949
readonly repoRef: string
5050
readonly outDir: string
51+
readonly cpuLimit: string
52+
readonly ramLimit: string
5153
readonly runUp: boolean
5254
readonly enableMcpPlaywright: boolean
5355
readonly force: boolean
@@ -58,6 +60,8 @@ export type CreateStep =
5860
| "repoUrl"
5961
| "repoRef"
6062
| "outDir"
63+
| "cpuLimit"
64+
| "ramLimit"
6165
| "runUp"
6266
| "mcpPlaywright"
6367
| "force"
@@ -66,6 +70,8 @@ export const createSteps: ReadonlyArray<CreateStep> = [
6670
"repoUrl",
6771
"repoRef",
6872
"outDir",
73+
"cpuLimit",
74+
"ramLimit",
6975
"runUp",
7076
"mcpPlaywright",
7177
"force"

0 commit comments

Comments
 (0)