Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/Actions/Service/DeployServiceApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Actions\Service;

use App\Models\ServiceApplication;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Contracts\Activity;

class DeployServiceApplication
{
use AsAction;

public string $jobQueue = 'high';

public function handle(ServiceApplication $serviceApplication, bool $pullLatestImages = false, bool $forceRebuild = false): Activity
{
$service = $serviceApplication->service;
$composeServiceName = $serviceApplication->name;

$service->parse();
$service->saveComposeConfigs();
$service->isConfigurationChanged(save: true);

$workdir = $service->workdir();
$composeFile = "{$workdir}/docker-compose.yml";
$safeWorkdir = escapeshellarg($workdir);
$safeComposeFile = escapeshellarg($composeFile);
$safeProjectName = escapeshellarg($service->uuid);
$safeComposeServiceName = escapeshellarg($composeServiceName);

$commands = collect([
'echo '.escapeshellarg("Saved configuration files to {$workdir}."),
'touch '.escapeshellarg("{$workdir}/.env"),
]);

if ($pullLatestImages) {
$commands->push('echo Pulling image for service.');
$commands->push("docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} pull {$safeComposeServiceName}");
}

if ($service->networks()->count() > 0) {
$commands->push('echo Creating Docker network.');
$commands->push("docker network inspect {$safeProjectName} >/dev/null 2>&1 || docker network create --attachable {$safeProjectName}");
}

$upCommand = "docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} up -d --no-deps";
if ($forceRebuild) {
$upCommand .= ' --build';
}
$upCommand .= " {$safeComposeServiceName}";
$commands->push('echo Starting service container.');
$commands->push($upCommand);

$commands->push("docker network connect {$safeProjectName} coolify-proxy >/dev/null 2>&1 || true");

if (data_get($service, 'connect_to_docker_network')) {
$network = escapeshellarg($service->destination->network);
$containerName = escapeshellarg("{$composeServiceName}-{$service->uuid}");
$networkAlias = escapeshellarg("{$composeServiceName}-{$service->uuid}");
$commands->push("docker network connect --alias {$networkAlias} {$network} {$containerName} >/dev/null 2>&1 || true");
}

return remote_process($commands->toArray(), $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
}
}
24 changes: 24 additions & 0 deletions app/Actions/Service/RestartServiceApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Actions\Service;

use App\Models\ServiceApplication;
use Lorisleiva\Actions\Concerns\AsAction;

class RestartServiceApplication
{
use AsAction;

public string $jobQueue = 'high';

public function handle(ServiceApplication $serviceApplication): void
{
$service = $serviceApplication->service;
$server = $service->destination->server;
$containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);

instant_remote_process([
"docker restart {$containerName}",
], $server);
}
}
24 changes: 24 additions & 0 deletions app/Actions/Service/StopServiceApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Actions\Service;

use App\Models\ServiceApplication;
use Lorisleiva\Actions\Concerns\AsAction;

class StopServiceApplication
{
use AsAction;

public string $jobQueue = 'high';

public function handle(ServiceApplication $serviceApplication): void
{
$service = $serviceApplication->service;
$server = $service->destination->server;
$containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);

instant_remote_process([
"docker stop {$containerName}",
], $server);
}
}
107 changes: 107 additions & 0 deletions app/Actions/Service/UpdateServiceApplicationFromApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace App\Actions\Service;

use App\Models\ServiceApplication;
use App\Support\ServiceComposeUrl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UpdateServiceApplicationFromApi
{
public function execute(ServiceApplication $serviceApplication, Request $request, string $teamId, array $payload): ?JsonResponse
{
$forceDomainOverride = $request->boolean('force_domain_override');

if (array_key_exists('url', $payload)) {
$urlRaw = $payload['url'];
if ($urlRaw !== null && ! is_string($urlRaw)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['url' => 'The url must be a string.'],
], 422);
}

$parsed = ServiceComposeUrl::validateUrlString(
is_string($urlRaw) ? $urlRaw : null,
$forceDomainOverride
);

if (count($parsed['errors']) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $parsed['errors'],
], 422);
}

if ($parsed['normalized'] !== null) {
$containerUrls = str($parsed['normalized'])
->explode(',')
->map(fn ($url) => str(trim((string) $url))->lower());

$result = checkIfDomainIsAlreadyUsedViaAPI($containerUrls, $teamId, $serviceApplication->uuid);
if (isset($result['error'])) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [$result['error']],
], 422);
}

if ($result['hasConflicts'] && ! $forceDomainOverride) {
return response()->json([
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
'conflicts' => $result['conflicts'],
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
], 409);
}
}

$serviceApplication->fqdn = $parsed['normalized'];
}

if (array_key_exists('human_name', $payload)) {
$serviceApplication->human_name = $payload['human_name'];
}

if (array_key_exists('description', $payload)) {
$serviceApplication->description = $payload['description'];
}

if (array_key_exists('image', $payload)) {
$serviceApplication->image = $payload['image'];
}

if (array_key_exists('exclude_from_status', $payload)) {
$serviceApplication->exclude_from_status = filter_var($payload['exclude_from_status'], FILTER_VALIDATE_BOOLEAN);
}

if (array_key_exists('is_gzip_enabled', $payload)) {
$serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN);
}

if (array_key_exists('is_stripprefix_enabled', $payload)) {
$serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN);
}

if (array_key_exists('is_log_drain_enabled', $payload)) {
$enabled = filter_var($payload['is_log_drain_enabled'], FILTER_VALIDATE_BOOLEAN);
$server = $serviceApplication->service->destination->server;
if ($enabled && ! $server->isLogDrainEnabled()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'is_log_drain_enabled' => ['Log drain is not enabled on the server for this service.'],
],
], 422);
}
$serviceApplication->is_log_drain_enabled = $enabled;
}

$serviceApplication->save();
$serviceApplication->refresh();

updateCompose($serviceApplication);

return null;
}
}
Loading
Loading