diff --git a/lib/DeployActions/DockerActions.php b/lib/DeployActions/DockerActions.php index ae17b8d7..ca21004a 100644 --- a/lib/DeployActions/DockerActions.php +++ b/lib/DeployActions/DockerActions.php @@ -1125,7 +1125,7 @@ public function buildDeployParams(DaemonConfig $daemonConfig, array $appInfo): a 'devices' => $devices, 'deviceRequests' => $deviceRequests, 'mounts' => $appInfo['external-app']['mounts'] ?? [], - 'resourceLimits' => $deployConfig['resourceLimits'] ?? [] + 'resourceLimits' => AppAPICommonService::buildResourceLimits($deployConfig) ]; return [ diff --git a/lib/DeployActions/KubernetesActions.php b/lib/DeployActions/KubernetesActions.php index e278d5ad..e64cd239 100644 --- a/lib/DeployActions/KubernetesActions.php +++ b/lib/DeployActions/KubernetesActions.php @@ -666,7 +666,7 @@ public function buildDeployParams(DaemonConfig $daemonConfig, array $appInfo): a 'net' => 'bridge', 'env' => $envs, 'computeDevice' => $deployConfig['computeDevice'] ?? null, - 'resourceLimits' => $deployConfig['resourceLimits'] ?? [] + 'resourceLimits' => AppAPICommonService::buildResourceLimits($deployConfig) ]; $result = [ diff --git a/lib/Service/AppAPICommonService.php b/lib/Service/AppAPICommonService.php index 77cd4cdd..8614c932 100644 --- a/lib/Service/AppAPICommonService.php +++ b/lib/Service/AppAPICommonService.php @@ -56,4 +56,22 @@ public function buildExAppHost(array $deployConfig): string { } return '127.0.0.1'; # fallback to loop-back adapter } + + /** + * Docker takes the memory and CPU limits as whole numbers, but the admin settings compute them from decimal + * input (`Number(cpus) * 1000000000`), which can store a value such as 1070000000.0000001. + */ + public static function buildResourceLimits(array $deployConfig): array { + $resourceLimits = $deployConfig['resourceLimits'] ?? []; + if (!is_array($resourceLimits)) { + return []; + } + foreach (['memory', 'nanoCPUs'] as $limit) { + $value = $resourceLimits[$limit] ?? null; + if (is_float($value) && abs($value) < PHP_INT_MAX) { + $resourceLimits[$limit] = (int)round($value); + } + } + return $resourceLimits; + } } diff --git a/tests/php/DeployActions/DockerActionsTest.php b/tests/php/DeployActions/DockerActionsTest.php index 1aa3d666..f72d0a6e 100644 --- a/tests/php/DeployActions/DockerActionsTest.php +++ b/tests/php/DeployActions/DockerActionsTest.php @@ -173,4 +173,17 @@ public function testShouldPullImage(array $registries, bool $expected): void { $shouldPullImage = new ReflectionMethod($this->dockerActions, 'shouldPullImage'); self::assertSame($expected, $shouldPullImage->invoke($this->dockerActions, $imageParams, $daemonConfig)); } + + public function testBuildDeployParamsRoundsTheResourceLimits(): void { + // What the admin settings store for a CPU limit of 1.07. + $daemonConfig = new DaemonConfig(['deploy_config' => [ + 'computeDevice' => ['id' => 'cpu'], + 'resourceLimits' => ['nanoCPUs' => 1.07 * 1000000000], + ]]); + $appInfo = ['id' => 'test-deploy', 'name' => 'Test Deploy', 'version' => '1.0.0', 'port' => 23000, 'secret' => 'secret', 'external-app' => []]; + + $params = $this->dockerActions->buildDeployParams($daemonConfig, $appInfo); + + self::assertSame(['nanoCPUs' => 1070000000], $params['container_params']['resourceLimits']); + } } diff --git a/tests/php/DeployActions/KubernetesActionsTest.php b/tests/php/DeployActions/KubernetesActionsTest.php index ed5d5d70..136f6f03 100644 --- a/tests/php/DeployActions/KubernetesActionsTest.php +++ b/tests/php/DeployActions/KubernetesActionsTest.php @@ -133,6 +133,19 @@ public function testDeployExAppSendsTheMappedImageForEveryRole(): void { } } + public function testBuildDeployParamsRoundsTheResourceLimits(): void { + // What the admin settings store for a CPU limit of 1.07. + $daemonConfig = new DaemonConfig([ + 'accepts_deploy_id' => KubernetesActions::DEPLOY_ID, + 'deploy_config' => ['resourceLimits' => ['nanoCPUs' => 1.07 * 1000000000]], + ]); + $appInfo = ['id' => 'test-deploy', 'name' => 'Test Deploy', 'version' => '1.0.0', 'port' => 23000, 'secret' => 'secret', 'external-app' => []]; + + $params = $this->kubernetesActions->buildDeployParams($daemonConfig, $appInfo); + + self::assertSame(['nanoCPUs' => 1070000000], $params['container_params']['resourceLimits']); + } + /** * Runs deployExApp() against queued HaRP responses and returns the payloads of the /exapp/create requests. */ diff --git a/tests/php/Service/AppAPICommonServiceTest.php b/tests/php/Service/AppAPICommonServiceTest.php new file mode 100644 index 00000000..9b511579 --- /dev/null +++ b/tests/php/Service/AppAPICommonServiceTest.php @@ -0,0 +1,42 @@ + [[], []], + 'empty limits' => [['resourceLimits' => []], []], + 'whole numbers stay as they are' => [ + ['resourceLimits' => ['memory' => 536870912, 'nanoCPUs' => 2000000000]], + ['memory' => 536870912, 'nanoCPUs' => 2000000000], + ], + // What the admin settings store for 1.07 and 2.01 CPUs. + 'a fraction above is rounded' => [['resourceLimits' => ['nanoCPUs' => 1070000000.0000001]], ['nanoCPUs' => 1070000000]], + 'a fraction below is rounded' => [['resourceLimits' => ['nanoCPUs' => 2009999999.9999998]], ['nanoCPUs' => 2010000000]], + 'a fractional memory value is rounded' => [['resourceLimits' => ['memory' => 512.3 * 1024 * 1024]], ['memory' => 537185485]], + 'other values are passed on for HaRP to check' => [ + ['resourceLimits' => ['nanoCPUs' => '1.5', 'memory' => 1e30, 'other' => 1.5]], + ['nanoCPUs' => '1.5', 'memory' => 1e30, 'other' => 1.5], + ], + 'not a list of limits' => [['resourceLimits' => 'x'], []], + ]; + } + + #[DataProvider('resourceLimitsProvider')] + public function testBuildResourceLimits(array $deployConfig, array $expected): void { + $this->assertSame($expected, AppAPICommonService::buildResourceLimits($deployConfig)); + } +}