mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(compose): reject scale and replicas greater than one (#12213)
This commit is contained in:
@@ -200,6 +200,26 @@ function forgetComposeBindStorage(Application|ServiceApplication|ServiceDatabase
|
||||
*
|
||||
* @throws Exception If the source is missing or contains shell metacharacters
|
||||
*/
|
||||
/**
|
||||
* Coolify sets container_name on every Compose service, and Docker Compose does not start more
|
||||
* than one container of a service that has a fixed container_name.
|
||||
*
|
||||
* @throws Exception If the service sets scale or deploy.replicas to more than 1
|
||||
*/
|
||||
function validateComposeServiceScale(string $serviceName, mixed $service): void
|
||||
{
|
||||
foreach (['scale', 'deploy.replicas'] as $key) {
|
||||
$value = data_get($service, $key);
|
||||
if (is_numeric($value) && (int) $value > 1) {
|
||||
throw new Exception(
|
||||
"Service \"{$serviceName}\" sets {$key} to {$value}. Coolify sets a container_name for each service, ".
|
||||
'and Docker Compose cannot run more than one container with the same container_name. '.
|
||||
"Remove {$key}, or define each instance as a separate service."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateComposeContentVolumeSource(array $volume): void
|
||||
{
|
||||
if (! array_key_exists('content', $volume) || ($volume['type'] ?? null) !== 'bind') {
|
||||
@@ -1139,6 +1159,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
' Service names must not contain shell metacharacters.'
|
||||
);
|
||||
}
|
||||
validateComposeServiceScale((string) $serviceName, $service);
|
||||
|
||||
$magicEnvironments = collect([]);
|
||||
$image = data_get_str($service, 'image');
|
||||
@@ -2379,6 +2400,7 @@ function serviceParser(Service $resource): Collection
|
||||
' Service names must not contain shell metacharacters.'
|
||||
);
|
||||
}
|
||||
validateComposeServiceScale((string) $serviceName, $service);
|
||||
|
||||
$image = data_get_str($service, 'image');
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Bus::fake();
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
});
|
||||
|
||||
function scaleTestApplication(string $compose): Application
|
||||
{
|
||||
return Application::factory()->create([
|
||||
'build_pack' => 'dockercompose',
|
||||
'docker_compose_raw' => $compose,
|
||||
'environment_id' => test()->environment->id,
|
||||
'destination_id' => test()->destination->id,
|
||||
'destination_type' => test()->destination->getMorphClass(),
|
||||
])->fresh();
|
||||
}
|
||||
|
||||
function scaleTestService(string $compose): Service
|
||||
{
|
||||
return Service::factory()->create([
|
||||
'docker_compose_raw' => $compose,
|
||||
'environment_id' => test()->environment->id,
|
||||
'server_id' => test()->destination->server_id,
|
||||
'destination_id' => test()->destination->id,
|
||||
'destination_type' => test()->destination->getMorphClass(),
|
||||
]);
|
||||
}
|
||||
|
||||
it('rejects a compose application service with more than one replica', function (string $compose, string $setting) {
|
||||
expect(fn () => applicationParser(scaleTestApplication($compose)))
|
||||
->toThrow(Exception::class, "Service \"celery\" sets {$setting} to 2. Coolify sets a container_name for each service");
|
||||
})->with([
|
||||
'scale' => ["services:\n celery:\n image: alpine\n scale: 2\n", 'scale'],
|
||||
'deploy.replicas' => ["services:\n celery:\n image: alpine\n deploy:\n replicas: 2\n", 'deploy.replicas'],
|
||||
]);
|
||||
|
||||
it('rejects a service stack service with more than one replica', function () {
|
||||
expect(fn () => serviceParser(scaleTestService("services:\n celery:\n image: alpine\n scale: '2'\n")))
|
||||
->toThrow(Exception::class, 'Service "celery" sets scale to 2.');
|
||||
});
|
||||
|
||||
it('accepts one replica and a scale set from a variable', function () {
|
||||
$compose = applicationParser(scaleTestApplication(
|
||||
"services:\n web:\n image: alpine\n scale: 1\n worker:\n image: alpine\n deploy:\n replicas: \${REPLICAS:-1}\n"
|
||||
));
|
||||
|
||||
expect(data_get($compose, 'services.web.scale'))->toBe(1)
|
||||
->and(data_get($compose, 'services.worker.container_name'))->not->toBeEmpty();
|
||||
});
|
||||
Reference in New Issue
Block a user