wip: swarm

This commit is contained in:
Andras Bacsai 2023-12-18 14:01:25 +01:00
parent 27c36bec83
commit 62c38c9859
24 changed files with 387 additions and 114 deletions

View File

@ -56,16 +56,20 @@ private function check_resources($schedule)
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false);
} else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false);
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
}
}
private function instance_auto_update($schedule)
{

View File

@ -217,19 +217,8 @@ public function handle(): void
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage' && !$this->application->destination->server->isSwarm()) {
$this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
[
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
]
);
}
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
@ -301,6 +290,9 @@ private function push_to_docker_registry()
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
if ($this->application->destination->server->isSwarm()) {
throw $e;
}
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
@ -604,7 +596,14 @@ private function deploy_static_buildpack()
private function rolling_update()
{
if ($this->server->isSwarm()) {
// Skip this.
$this->push_to_docker_registry();
$this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}")
],
);
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
@ -703,10 +702,20 @@ private function deploy_pull_request()
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
if ($this->application->destination->server->isSwarm()) {
ray("{$this->workdir}{$this->docker_compose_location}");
$this->push_to_docker_registry();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}-{$this->pull_request_id}")
],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
}
private function create_workdir()
{
@ -970,13 +979,8 @@ private function generate_compose_file()
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'replicas' => data_get($this->application, 'swarm_replicas', 1),
'update_config' => [
'order' => 'start-first'
],
@ -995,6 +999,16 @@ private function generate_compose_file()
]
]
];
if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) {
$docker_compose['services'][$this->container_name]['deploy']['placement'] = [
'constraints' => [
'node.role == worker'
]
];
}
if ($this->pull_request_id !== 0) {
$docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1;
}
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}

View File

@ -46,7 +46,7 @@ public function handle()
};
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
@ -54,15 +54,15 @@ public function handle()
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
$containerReplicates = null;
}
if (is_null($containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicase) {
$containerReplicase = format_docker_command_output_to_json($containerReplicase);
foreach ($containerReplicase as $containerReplica) {
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {

View File

@ -113,7 +113,7 @@ public function mount()
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels();
$this->customLabels = $this->application->parseContainerLabels();
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}

View File

@ -73,6 +73,10 @@ public function deploy(bool $force_rebuild = false)
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'Please set a Docker image name first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,

View File

@ -72,10 +72,14 @@ protected function setDeploymentUuid()
public function stop(int $pull_request_id)
{
try {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh();

View File

@ -0,0 +1,51 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Swarm extends Component
{
public Application $application;
public string $swarm_placement_constraints = '';
protected $rules = [
'application.swarm_replicas' => 'required',
'application.swarm_placement_constraints' => 'nullable',
'application.settings.is_swarm_only_worker_nodes' => 'required',
];
public function mount() {
if ($this->application->swarm_placement_constraints) {
$this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints);
}
}
public function instantSave() {
try {
$this->validate();
$this->application->settings->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() {
try {
$this->validate();
if ($this->swarm_placement_constraints) {
$this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints);
} else {
$this->application->swarm_placement_constraints = null;
}
$this->application->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.application.swarm');
}
}

View File

@ -15,12 +15,15 @@ class Select extends Component
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $allServers = [];
public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public Collection|array $allServices = [];
public bool $isDatabase = false;
public bool $includeSwarm = true;
public bool $loadingServices = true;
public bool $loading = false;
@ -96,19 +99,43 @@ public function loadServices()
$this->loadingServices = false;
}
}
public function instantSave()
{
if ($this->includeSwarm) {
$this->servers = $this->allServers;
} else {
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
}
}
public function setType(string $type)
{
$this->type = $type;
if ($this->loading) return;
$this->loading = true;
$this->type = $type;
switch ($type) {
case 'postgresql':
case 'mysql':
case 'mariadb':
case 'redis':
case 'mongodb':
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
break;
}
if (str($type)->startsWith('one-click-service')) {
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
}
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->setServer($server);
}
// if (count($this->servers) === 1) {
// $server = $this->servers->first();
// $this->setServer($server);
// }
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server->id)->first();
if ($foundServer) {
@ -142,5 +169,6 @@ public function setDestination(string $destination_uuid)
public function loadServers()
{
$this->servers = Server::isUsable()->get();
$this->allServers = $this->servers;
}
}

View File

@ -2,22 +2,26 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\Application;
use Livewire\Component;
class Add extends Component
{
public $uuid;
public $parameters;
public $isSwarm = false;
public string $name;
public string $mount_path;
public string|null $host_path = null;
public ?string $host_path = null;
protected $listeners = ['clearAddStorage' => 'clear'];
protected $rules = [
public $rules = [
'name' => 'required|string',
'mount_path' => 'required|string',
'host_path' => 'string|nullable',
];
protected $listeners = ['clearAddStorage' => 'clear'];
protected $validationAttributes = [
'name' => 'name',
'mount_path' => 'mount',
@ -27,17 +31,31 @@ class Add extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$applicationUuid = $this->parameters['application_uuid'];
$application = Application::where('uuid', $applicationUuid)->first();
if (!$application) {
abort(404);
}
if ($application->destination->server->isSwarm()) {
$this->isSwarm = true;
$this->rules['host_path'] = 'required|string';
}
}
public function submit()
{
$this->validate();
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
try {
$this->validate($this->rules);
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
$this->dispatch('closeStorageModal');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function clear()

View File

@ -25,7 +25,7 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean',
// 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@ -37,7 +37,7 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager',
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_swarm_worker' => 'Swarm Worker',
];
public function mount()

View File

@ -22,6 +22,8 @@ class ByIp extends Component
public string $user = 'root';
public int $port = 22;
public bool $is_swarm_manager = false;
public bool $is_swarm_worker = false;
protected $rules = [
'name' => 'required|string',
@ -30,6 +32,7 @@ class ByIp extends Component
'user' => 'required|string',
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@ -38,6 +41,7 @@ class ByIp extends Component
'user' => 'User',
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker',
];
public function mount()
@ -77,6 +81,7 @@ public function submit()
],
]);
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->is_swarm_worker = $this->is_swarm_worker;
$server->settings->save();
$server->addInitialNetwork();
return $this->redirectRoute('server.show', $server->uuid, navigate: true);

View File

@ -72,7 +72,7 @@ static public function ownedByCurrentTeam(array $select = ['*'])
static public function isUsable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true);
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false);
}
static public function destinationsByServer(string $server_id)
@ -380,6 +380,14 @@ public function isSwarm()
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function isSwarmManager()
{
return data_get($this, 'settings.is_swarm_manager');
}
public function isSwarmWorker()
{
return data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection()
{
$server = Server::find($this->id);

View File

@ -80,14 +80,18 @@ function generate_default_proxy_configuration(Server $server)
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
} else {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
}
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$array_of_networks = collect([]);
$networks->map(function ($network) use ($array_of_networks) {
$array_of_networks[$network] = [

View File

@ -0,0 +1,36 @@
<?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('applications', function (Blueprint $table) {
$table->integer('swarm_replicas')->default(1);
$table->text('swarm_placement_constraints')->nullable();
});
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_swarm_only_worker_nodes')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('swarm_replicas');
$table->dropColumn('swarm_placement_constraints');
});
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_swarm_only_worker_nodes');
});
}
};

View File

@ -19,24 +19,26 @@
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
Swarm Deployments requires a Docker Image in a Registry.
@else
<x-applications.advanced :application="$application" />
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
@ -47,9 +49,13 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
<path d="M20 4v5h-5" />
</g>
</svg>
@if ($application->destination->server->isSwarm())
Update Service
@else
Restart
@endif
</button>
@if (isDev())
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -61,7 +67,7 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
</svg>
Restart (new)
</button>
@endif
@endif --}}
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
@ -83,7 +89,7 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
</svg>
Deploy
</button>
@if (isDev())
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
@ -94,7 +100,7 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
</svg>
Deploy (new)
</button>
@endif
@endif --}}
@endif
@endif
</div>

View File

@ -2,7 +2,7 @@
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxyType() !== 'NONE')
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.status :server="$server" />
@endif
</div>
@ -20,25 +20,30 @@
]) }}">
<button>Private Key</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@if (!$server->isSwarmWorker())
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@endif
<div class="flex-1"></div>
<livewire:server.proxy.deploy :server="$server" />
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.deploy :server="$server" />
@endif
</nav>
</div>

View File

@ -5,6 +5,11 @@
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($application->destination->server->isSwarm())
<a :class="activeTab === 'swarm' && 'text-white'"
@click.prevent="activeTab = 'swarm'; window.location.hash = 'swarm'" href="#">Swarm
Configuration</a>
@endif
<a :class="activeTab === 'advanced' && 'text-white'"
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced</a>
@if ($application->build_pack !== 'static')
@ -13,6 +18,7 @@
href="#">Environment
Variables</a>
@endif
@if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@ -56,6 +62,9 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.application.general :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'swarm'" class="h-full">
<livewire:project.application.swarm :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
<livewire:project.application.advanced :application="$application" />
</div>

View File

@ -70,8 +70,11 @@
@if ($application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3>
@if ($application->destination->server->isSwarm())
<div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@if ($application->build_pack !== 'dockerimage')
<div>Docker Swarm requires the image to be available in a registry. More info <a
class="underline" href="https://coolify.io/docs/docker-registries"
target="_blank">here</a>.</div>
@endif
@else
@if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline"
@ -135,7 +138,8 @@
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
</div>
<div class="pt-4">The following commands are for advanced use cases. Only modify them if you know what are
<div class="pt-4">The following commands are for advanced use cases. Only modify them if you
know what are
you doing.</div>
<div class="flex gap-2">
<x-forms.input placeholder="docker compose build"
@ -199,8 +203,10 @@
required
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@if (!$application->destination->server->isSwarm())
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@endif
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>

View File

@ -0,0 +1,24 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Swarm Configuration</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
{{-- <div>Advanced Swarm Configuration</div> --}}
<div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.swarm_replicas" label="Replicas" required />
<x-forms.checkbox instantSave helper="If turned off, this resource will start on manager nodes too."
id="application.settings.is_swarm_only_worker_nodes" label="Only Start on Worker nodes" />
</div>
<x-forms.textarea id="swarm_placement_constraints" rows="7"
label="Custom Placement Constraints"
placeholder="placement:
constraints:
- 'node.role == worker'" />
</div>
</form>
</div>

View File

@ -202,6 +202,14 @@ class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-cool
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
@if ($isDatabase)
<div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm"
helper="Swarm clusters are excluded from this list by default. For database (or services with database) to work with Swarm,
you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/swarm#database-requirements' target='_blank'>here</a>."
label="Include Swarm Clusters" />
</div>
@endif
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server)
<div class="box group" wire:click="setServer({{ $server }})">

View File

@ -1,14 +1,28 @@
<dialog id="newStorage" class="modal">
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
<h3 class="text-lg font-bold">Add Storage Volume</h3>
<x-forms.input placeholder="pv-name" id="name" label="Name" required />
<x-forms.input placeholder="/root" id="host_path" label="Source Path" />
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required />
<x-forms.button onclick="newStorage.close()" type="submit">
@if ($isSwarm)
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you would like to use a persistent volumes.</h5>
@endif
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
@if ($isSwarm)
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required helper="Directory on the host system." />
@else
<x-forms.input placeholder="/root" id="host_path" label="Source Path" helper="Directory on the host system." />
@endif
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required helper="Directory inside the container." />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
<script>
document.addEventListener('livewire:initialized', () => {
Livewire.on('closeStorageModal', () => {
document.getElementById('newStorage').close()
})
})
</script>
</dialog>

View File

@ -35,8 +35,10 @@
<div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required />
<x-forms.input id="server.description" label="Description" />
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
@if (!$server->settings->is_swarm_worker)
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
@endif
</div>
<div class="flex flex-col w-full gap-2 lg:flex-row">
@ -53,10 +55,20 @@
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@endif
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
label="Is it a Swarm Manager?" />
{{-- <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
label="Is it a Swarm Worker?" /> --}}
@if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager"
label="Is it a Swarm Manager?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
label="Is it a Swarm Manager?" />
@endif
@if ($server->settings->is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker"
label="Is it a Swarm Worker?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
label="Is it a Swarm Worker?" />
@endif
</div>
</div>

View File

@ -26,8 +26,19 @@
@endforeach
</x-forms.select>
<div class="w-64">
<x-forms.checkbox type="checkbox" id="is_swarm_manager"
label="Is it a Swarm Manager?" />
@if ($is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager"
label="Is it a Swarm Manager?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="is_swarm_manager"
label="Is it a Swarm Manager?" />
@endif
@if ($is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_worker"
label="Is it a Swarm Worker?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="is_swarm_worker" label="Is it a Swarm Worker?" />
@endif
</div>
<x-forms.button type="submit">
Save New Server

View File

@ -3,7 +3,9 @@
<div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="w-full">
<livewire:server.proxy :server="$server" />
@if ($server->proxyType() !== 'NONE' && $server->isFunctional())
<livewire:server.proxy :server="$server" />
@endif
</div>
</div>
</div>