Skip to content

Commit 713abdd

Browse files
committed
Adds option to set memory and cpu constraint
- Adds fields in admin UI - Sets HostConfig.Memory and HostConfig.NanoCPUs fields for Docker Socket Proxy - Passes memory and nanoCPU values to HaRP Proxy (requires changes in HaRP repo)
1 parent 4460957 commit 713abdd

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

lib/DeployActions/DockerActions.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,12 @@ public function deployExAppHarp(ExApp $exApp, DaemonConfig $daemonConfig, array
160160
'restart_policy' => $this->appConfig->getValueString(Application::APP_ID, 'container_restart_policy', 'unless-stopped', lazy: true),
161161
'compute_device' => $computeDevice,
162162
'mount_points' => $mountPoints,
163-
'start_container' => true,
163+
'start_container' => true
164164
];
165+
166+
if (isset($params['container_params']['resourceLimits']) && !empty($params['container_params']['resourceLimits'])) {
167+
$createPayload['resource_limits'] = $params['container_params']['resourceLimits'];
168+
}
165169

166170
$this->logger->debug(sprintf('Payload for /docker/exapp/create for %s: %s', $exAppName, json_encode($createPayload)));
167171
try {
@@ -556,6 +560,15 @@ public function createContainer(string $dockerUrl, string $imageId, DaemonConfig
556560
);
557561
}
558562

563+
if (isset($params['resourceLimits'])) {
564+
if (isset($params['resourceLimits']['memory']) && $params['resourceLimits']['memory'] > 0) {
565+
$containerParams['HostConfig']['Memory'] = $params['resourceLimits']['memory'];
566+
}
567+
if (isset($params['resourceLimits']['nanoCPUs']) && $params['resourceLimits']['nanoCPUs'] > 0) {
568+
$containerParams['HostConfig']['NanoCPUs'] = $params['resourceLimits']['nanoCPUs'];
569+
}
570+
}
571+
559572
$url = $this->buildApiUrl($dockerUrl, sprintf('containers/create?name=%s', urlencode($this->buildExAppContainerName($params['name']))));
560573
try {
561574
$options['json'] = $containerParams;
@@ -1107,6 +1120,7 @@ public function buildDeployParams(DaemonConfig $daemonConfig, array $appInfo): a
11071120
'devices' => $devices,
11081121
'deviceRequests' => $deviceRequests,
11091122
'mounts' => $appInfo['external-app']['mounts'] ?? [],
1123+
'resourceLimits' => $deployConfig['resourceLimits'] ?? []
11101124
];
11111125

11121126
return [

src/components/DaemonConfig/DaemonConfigDetailsModal.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
<p v-if="daemon.deploy_config.computeDevice">
4242
<b>{{ t('app_api', 'Compute device') }}:</b> {{ daemon.deploy_config?.computeDevice?.label }}
4343
</p>
44+
<p><b>{{ t('app_api', 'Memory limit') }}:</b> {{ daemon.deploy_config.resourceLimits && daemon.deploy_config.resourceLimits.memory ? formatMemoryLimit(daemon.deploy_config.resourceLimits.memory) : t('app_api', 'Unlimited') }}</p>
45+
46+
<p><b>{{ t('app_api', 'CPU limit') }}:</b> {{ daemon.deploy_config.resourceLimits && daemon.deploy_config.resourceLimits.nanoCPUs ? formatCpuLimit(daemon.deploy_config.resourceLimits.nanoCPUs) : t('app_api', 'Unlimited') }}</p>
4447

4548
<div v-if="daemon.deploy_config.additional_options" class="additional-options">
4649
<h3>{{ t('app_api', 'Additional options') }}</h3>
@@ -129,6 +132,22 @@ export default {
129132
console.debug(err)
130133
})
131134
},
135+
formatMemoryLimit(memory) {
136+
const memoryMB = memory / (1024 * 1024)
137+
if (memoryMB >= 1024) {
138+
const memoryGB = memoryMB / 1024
139+
return t('app_api', '{size} GB', { size: memoryGB.toFixed(1) })
140+
}
141+
return t('app_api', '{size} MB', { size: Math.round(memoryMB) })
142+
},
143+
formatCpuLimit(nanoCpus) {
144+
if (!nanoCpus) return t('app_api', 'Unlimited')
145+
const cpus = nanoCpus / 1000000000
146+
if (cpus === 1) {
147+
return t('app_api', '1 CPU')
148+
}
149+
return t('app_api', '{cpus} CPUs', { cpus: cpus.toFixed(3) })
150+
},
132151
},
133152
}
134153
</script>

src/components/DaemonConfig/ManageDaemonConfigModal.vue

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,36 @@
209209
:aria-label="t('app_api', 'Compute device')"
210210
:options="computeDevices" />
211211
</div>
212+
<div class="external-label" :aria-label="t('app_api', 'Memory limit')">
213+
<label for="memory-limit">
214+
{{ t('app_api', 'Memory limit (in MB)') }}
215+
<InfoTooltip :text="t('app_api', 'Maximum memory that the ExApp container can use in megabytes')" />
216+
</label>
217+
<NcInputField
218+
id="memory-limit"
219+
ref="memory-limit"
220+
class="ex-input-field"
221+
:value.sync="memoryLimit"
222+
:placeholder="t('app_api', 'Memory limit in MB')"
223+
:aria-label="t('app_api', 'Memory limit in MB')"
224+
:error="isMemoryLimitValid === false"
225+
:helper-text="isMemoryLimitValid === false ? t('app_api', 'Must be a positive integer') : ''" />
226+
</div>
227+
<div class="external-label" :aria-label="t('app_api', 'CPU limit')">
228+
<label for="cpu-limit">
229+
{{ t('app_api', 'CPU limit') }}
230+
<InfoTooltip :text="t('app_api', 'Maximum CPU cores that the ExApp container can use (e.g. 0.5 for half a core, 2 for two cores)')" />
231+
</label>
232+
<NcInputField
233+
id="cpu-limit"
234+
ref="cpu-limit"
235+
class="ex-input-field"
236+
:value.sync="cpuLimit"
237+
:placeholder="t('app_api', 'CPU limit as decimal value')"
238+
:aria-label="t('app_api', 'CPU limit')"
239+
:error="isCpuLimitValid === false"
240+
:helper-text="isCpuLimitValid === false ? t('app_api', 'Must be a positive number') : ''" />
241+
</div>
212242
<template v-if="additionalOptions.length > 0">
213243
<div class="row" style="flex-direction: column;">
214244
<div
@@ -413,12 +443,26 @@ export default {
413443
data.defaultDaemon = this.isDefaultDaemon
414444
data.additionalOptions = Object.entries(this.daemon.deploy_config.additional_options ?? {}).map(([key, value]) => ({ key, value }))
415445
data.deployConfigSettingsOpened = true
446+
if (data.deployConfig.resourceLimits) {
447+
if (data.deployConfig.resourceLimits.memory) {
448+
data.deployConfig.resourceLimits.memoryMB = data.deployConfig.resourceLimits.memory / (1024 * 1024)
449+
delete data.deployConfig.resourceLimits.memory
450+
}
451+
if (data.deployConfig.resourceLimits.nanoCPUs) {
452+
data.deployConfig.resourceLimits.cpus = data.deployConfig.resourceLimits.nanoCPUs / 1000000000
453+
delete data.deployConfig.resourceLimits.nanoCPUs
454+
}
455+
}
416456
}
417457
if (!data.deployConfig.harp) {
418458
data.deployConfig.harp = null
419459
data.deployConfigSettingsOpened = false
420460
}
421461
462+
if (!data.deployConfig.resourceLimits) {
463+
data.deployConfig.resourceLimits = { memoryMB: null, cpus: null }
464+
}
465+
422466
return data
423467
},
424468
computed: {
@@ -437,6 +481,32 @@ export default {
437481
daemonProtocol() {
438482
return this.httpsEnabled ? 'https' : 'http'
439483
},
484+
memoryLimit: {
485+
get() {
486+
return this.deployConfig.resourceLimits.memoryMB || ''
487+
},
488+
set(value) {
489+
this.deployConfig.resourceLimits.memoryMB = value === '' ? null : value
490+
},
491+
},
492+
cpuLimit: {
493+
get() {
494+
return this.deployConfig.resourceLimits.cpus || ''
495+
},
496+
set(value) {
497+
this.deployConfig.resourceLimits.cpus = value === '' ? null : value
498+
},
499+
},
500+
isMemoryLimitValid() {
501+
if (this.memoryLimit === '' || this.memoryLimit === null) return true
502+
const str = String(this.memoryLimit).trim()
503+
return /^[1-9]\d*$/.test(str)
504+
},
505+
isCpuLimitValid() {
506+
if (this.cpuLimit === '' || this.cpuLimit === null) return true
507+
const str = String(this.cpuLimit).trim()
508+
return /^\d*\.?\d+$/.test(str)
509+
},
440510
isDaemonNameInvalid() {
441511
return this.daemons.some(daemon => daemon.name === this.name && daemon.name !== this.daemon?.name)
442512
},
@@ -463,7 +533,7 @@ export default {
463533
return t('app_api', 'The docker network that the deployed ex-apps would use.')
464534
},
465535
cannotRegister() {
466-
return this.isDaemonNameInvalid === true || this.isHaProxyPasswordValid === false || (this.isHarp && !this.deployConfig.net)
536+
return this.isDaemonNameInvalid === true || this.isHaProxyPasswordValid === false || (this.isHarp && !this.deployConfig.net) || this.isMemoryLimitValid === false || this.isCpuLimitValid === false
467537
},
468538
isAdditionalOptionValid() {
469539
return this.additionalOption.key.trim() !== '' && this.additionalOption.value.trim() !== ''
@@ -619,6 +689,16 @@ export default {
619689
registries: this.deployConfig.registries || null,
620690
},
621691
}
692+
693+
const resourceLimits = {}
694+
if (this.deployConfig.resourceLimits.memoryMB && this.isMemoryLimitValid) {
695+
resourceLimits.memory = Number(this.deployConfig.resourceLimits.memoryMB) * 1024 * 1024
696+
}
697+
if (this.deployConfig.resourceLimits.cpus && this.isCpuLimitValid) {
698+
resourceLimits.nanoCPUs = Number(this.deployConfig.resourceLimits.cpus) * 1000000000
699+
}
700+
params.deploy_config.resourceLimits = resourceLimits
701+
622702
if (this.additionalOptions.length > 0) {
623703
params.deploy_config.additional_options = this.additionalOptions.reduce((acc, option) => {
624704
acc[option.key] = option.value

src/constants/daemonTemplates.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const DAEMON_TEMPLATES = [
1919
id: 'cpu',
2020
label: 'CPU',
2121
},
22+
resourceLimits: {
23+
memory: null,
24+
nanoCPUs: null,
25+
},
2226
harp: {
2327
frp_address: 'localhost:8782',
2428
docker_socket_port: 24000,
@@ -43,6 +47,10 @@ export const DAEMON_TEMPLATES = [
4347
id: 'cpu',
4448
label: 'CPU',
4549
},
50+
resourceLimits: {
51+
memory: null,
52+
nanoCPUs: null,
53+
},
4654
harp: {
4755
frp_address: 'appapi-harp:8782',
4856
docker_socket_port: 24000,
@@ -67,6 +75,10 @@ export const DAEMON_TEMPLATES = [
6775
id: 'cpu',
6876
label: 'CPU',
6977
},
78+
resourceLimits: {
79+
memory: null,
80+
nanoCPUs: null,
81+
},
7082
harp: {
7183
frp_address: 'nextcloud-aio-harp:8782',
7284
docker_socket_port: 24000,
@@ -91,6 +103,10 @@ export const DAEMON_TEMPLATES = [
91103
id: 'cpu',
92104
label: 'CPU',
93105
},
106+
resourceLimits: {
107+
memory: null,
108+
nanoCPUs: null,
109+
},
94110
harp: {
95111
frp_address: 'localhost:8782',
96112
docker_socket_port: 24000,
@@ -115,6 +131,10 @@ export const DAEMON_TEMPLATES = [
115131
id: 'cpu',
116132
label: 'CPU',
117133
},
134+
resourceLimits: {
135+
memory: null,
136+
nanoCPUs: null,
137+
},
118138
harp: null,
119139
},
120140
deployConfigSettingsOpened: false,
@@ -135,6 +155,10 @@ export const DAEMON_TEMPLATES = [
135155
id: 'cpu',
136156
label: 'CPU',
137157
},
158+
resourceLimits: {
159+
memory: null,
160+
nanoCPUs: null,
161+
},
138162
harp: null,
139163
},
140164
deployConfigSettingsOpened: false,
@@ -155,6 +179,10 @@ export const DAEMON_TEMPLATES = [
155179
id: 'cpu',
156180
label: 'CPU',
157181
},
182+
resourceLimits: {
183+
memory: null,
184+
nanoCPUs: null,
185+
},
158186
harp: null,
159187
},
160188
deployConfigSettingsOpened: false,
@@ -175,6 +203,10 @@ export const DAEMON_TEMPLATES = [
175203
id: 'cpu',
176204
label: 'CPU',
177205
},
206+
resourceLimits: {
207+
memory: null,
208+
nanoCPUs: null,
209+
},
178210
harp: null,
179211
},
180212
deployConfigSettingsOpened: false,

0 commit comments

Comments
 (0)