mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 01:10:30 -04:00
feat(deployments): add team policy for build server fallback
Allow teams to choose whether deployments fall back to the deployment server when no usable dedicated build server is available.
This commit is contained in:
@@ -361,21 +361,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Check custom port
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$teamId = data_get($this->application, 'environment.project.team.id');
|
||||
$buildServers = Server::buildServers($teamId)->get();
|
||||
if ($buildServers->count() === 0) {
|
||||
$this->application_deployment_queue->addLogEntry('No suitable build server found. Using the deployment server.');
|
||||
$this->build_server = $this->server;
|
||||
} else {
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->application_deployment_queue->build_server_id = $this->build_server->id;
|
||||
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
} else {
|
||||
$this->build_server = $this->server;
|
||||
}
|
||||
$this->selectBuildServer();
|
||||
$this->detectBuildKitCapabilities();
|
||||
$this->decide_what_to_do();
|
||||
} catch (Exception $e) {
|
||||
@@ -424,6 +410,34 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function selectBuildServer(): void
|
||||
{
|
||||
if (! data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$this->build_server = $this->server;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$team = $this->application->environment->project->team;
|
||||
$buildServers = Server::buildServers($team->id)->get();
|
||||
|
||||
if ($buildServers->isEmpty()) {
|
||||
if (! $team->is_build_server_fallback_enabled) {
|
||||
throw new DeploymentException('No available dedicated build server was found. Enable a usable build server for this team or allow fallback to the deployment server in the team settings.');
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('No suitable build server found. Using the deployment server.');
|
||||
$this->build_server = $this->server;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->application_deployment_queue->build_server_id = $this->build_server->id;
|
||||
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
|
||||
private function detectBuildKitCapabilities(): void
|
||||
{
|
||||
$this->dockerBuildkitSupported = false;
|
||||
|
||||
@@ -23,12 +23,15 @@ class Index extends Component
|
||||
|
||||
public bool $is_mcp_server_enabled = true;
|
||||
|
||||
public bool $is_build_server_fallback_enabled = true;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'is_mcp_server_enabled' => 'boolean',
|
||||
'is_build_server_fallback_enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -59,6 +62,7 @@ class Index extends Component
|
||||
$this->team->name = $this->name;
|
||||
$this->team->description = $this->description;
|
||||
$this->team->is_mcp_server_enabled = $this->is_mcp_server_enabled;
|
||||
$this->team->is_build_server_fallback_enabled = $this->is_build_server_fallback_enabled;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->name = $this->team->name;
|
||||
@@ -66,6 +70,7 @@ class Index extends Component
|
||||
// Null can appear after Team::create() when the DB default is not
|
||||
// hydrated onto the in-memory model stored in session.
|
||||
$this->is_mcp_server_enabled = (bool) ($this->team->is_mcp_server_enabled ?? true);
|
||||
$this->is_build_server_fallback_enabled = (bool) ($this->team->is_build_server_fallback_enabled ?? true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -903,7 +903,12 @@ $siteAddress {
|
||||
|
||||
public static function buildServers($teamId)
|
||||
{
|
||||
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
|
||||
return Server::whereTeamId($teamId)
|
||||
->whereRelation('settings', 'is_reachable', true)
|
||||
->whereRelation('settings', 'is_usable', true)
|
||||
->whereRelation('settings', 'is_swarm_worker', false)
|
||||
->whereRelation('settings', 'is_build_server', true)
|
||||
->whereRelation('settings', 'force_disabled', false);
|
||||
}
|
||||
|
||||
public function isForceDisabled()
|
||||
|
||||
@@ -48,15 +48,18 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
'show_boarding',
|
||||
'custom_server_limit',
|
||||
'is_mcp_server_enabled',
|
||||
'is_build_server_fallback_enabled',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_mcp_server_enabled' => true,
|
||||
'is_build_server_fallback_enabled' => true,
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'personal_team' => 'boolean',
|
||||
'is_mcp_server_enabled' => 'boolean',
|
||||
'is_build_server_fallback_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->boolean('is_build_server_fallback_enabled')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('is_build_server_fallback_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -287,11 +287,14 @@
|
||||
? ['value' => true, 'label' => 'Available build server (auto-select)']
|
||||
: ['value' => true, 'label' => 'No build servers connected', 'disabled' => true],
|
||||
];
|
||||
$buildServerFallbackPolicy = $application->environment->project->team->is_build_server_fallback_enabled
|
||||
? 'If no usable build server is available, Coolify builds on the deployment server.'
|
||||
: 'If no usable build server is available, the deployment fails.';
|
||||
@endphp
|
||||
<div class="grid gap-4 pt-2 sm:grid-cols-2">
|
||||
<x-forms.listbox id="isBuildServerEnabled" label="Builder selection"
|
||||
onChange="instantSave" :options="$buildServerOptions"
|
||||
helper="Build your application on a dedicated build server. If several build servers are connected, Coolify picks an available one automatically. More info in the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
helper="Build your application on a dedicated build server. If several build servers are connected, Coolify picks an available one automatically. {{ $buildServerFallbackPolicy }} More info in the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -25,6 +25,15 @@
|
||||
['value' => true, 'label' => 'Enabled for this team'],
|
||||
]" />
|
||||
</div>
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.listbox canGate="update" :canResource="$team"
|
||||
id="is_build_server_fallback_enabled" label="Build server fallback"
|
||||
helper="Controls what happens when an application is configured to use a dedicated build server, but no usable build server is available."
|
||||
:disabled="! auth()->user()->can('update', $team)" :options="[
|
||||
['value' => true, 'label' => 'Build on the deployment server'],
|
||||
['value' => false, 'label' => 'Fail the deployment'],
|
||||
]" />
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
</form>
|
||||
|
||||
@@ -142,6 +142,18 @@ test('owner can update team MCP setting', function () {
|
||||
expect($this->team->fresh()->is_mcp_server_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
test('owner can update team build server fallback policy', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamIndex::class)
|
||||
->set('is_build_server_fallback_enabled', false)
|
||||
->call('submit')
|
||||
->assertDispatched('success');
|
||||
|
||||
expect($this->team->fresh()->is_build_server_fallback_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
test('team index mounts when is_mcp_server_enabled is null on the session team', function () {
|
||||
$this->actingAs($this->owner);
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
use App\Exceptions\DeploymentException;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
Server::flushIdentityMap();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Server::flushIdentityMap();
|
||||
});
|
||||
|
||||
function makeBuildServerSelectionJob(Team $team, Server $deploymentServer): array
|
||||
{
|
||||
$project = new Project;
|
||||
$project->setRelation('team', $team);
|
||||
|
||||
$environment = new Environment;
|
||||
$environment->setRelation('project', $project);
|
||||
|
||||
$settings = new ApplicationSetting;
|
||||
$settings->is_build_server_enabled = true;
|
||||
|
||||
$application = new Application;
|
||||
$application->setRelation('environment', $environment);
|
||||
$application->setRelation('settings', $settings);
|
||||
|
||||
$deploymentQueue = Mockery::mock(ApplicationDeploymentQueue::class);
|
||||
$job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ([
|
||||
'application' => $application,
|
||||
'application_deployment_queue' => $deploymentQueue,
|
||||
'server' => $deploymentServer,
|
||||
] as $property => $value) {
|
||||
(new ReflectionProperty(ApplicationDeploymentJob::class, $property))->setValue($job, $value);
|
||||
}
|
||||
|
||||
return [$job, $deploymentQueue];
|
||||
}
|
||||
|
||||
function invokeBuildServerSelection(ApplicationDeploymentJob $job): void
|
||||
{
|
||||
(new ReflectionMethod(ApplicationDeploymentJob::class, 'selectBuildServer'))->invoke($job);
|
||||
}
|
||||
|
||||
function selectedBuildServer(ApplicationDeploymentJob $job): Server
|
||||
{
|
||||
return (new ReflectionProperty(ApplicationDeploymentJob::class, 'build_server'))->getValue($job);
|
||||
}
|
||||
|
||||
test('teams allow deployment server fallback by default', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldReceive('addLogEntry')
|
||||
->once()
|
||||
->with('No suitable build server found. Using the deployment server.');
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect($team->fresh()->is_build_server_fallback_enabled)->toBeTrue()
|
||||
->and(selectedBuildServer($job)->is($deploymentServer))->toBeTrue();
|
||||
});
|
||||
|
||||
test('strict teams fail when no dedicated build server is available', function () {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
expect(fn () => invokeBuildServerSelection($job))
|
||||
->toThrow(DeploymentException::class, 'No available dedicated build server was found.');
|
||||
});
|
||||
|
||||
test('strict teams reject ineligible dedicated build servers', function (array $settings) {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
$buildServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
$buildServer->settings()->update(array_merge([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'is_build_server' => true,
|
||||
'is_swarm_worker' => false,
|
||||
'force_disabled' => false,
|
||||
], $settings));
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
expect(fn () => invokeBuildServerSelection($job))
|
||||
->toThrow(DeploymentException::class, 'No available dedicated build server was found.');
|
||||
})->with([
|
||||
'unreachable' => [['is_reachable' => false]],
|
||||
'unusable' => [['is_usable' => false]],
|
||||
'force-disabled' => [['force_disabled' => true]],
|
||||
'swarm worker' => [['is_swarm_worker' => true]],
|
||||
]);
|
||||
|
||||
test('strict teams use an available dedicated build server', function () {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
$buildServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
$buildServer->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'is_build_server' => true,
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldReceive('setAttribute')->with('build_server_id', $buildServer->id)->once()->andReturnSelf();
|
||||
$deploymentQueue->shouldReceive('addLogEntry')
|
||||
->once()
|
||||
->with("Found a suitable build server ({$buildServer->name}).");
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($buildServer))->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user