mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-14 08:42:38 -04:00
The compose parser queued ServerFilesFromServerJob for every volume on each parse, and pages that only show data parse the Compose file. Each sync also sent two SSH commands for each file. - Queue the sync only for a compose deployment, and for each service resource with file storages when the service starts or deploys. - Read the compose service names without a parse on the Domains and scheduled task pages. - Check all file storages with one remote command, and stop when a symbolic link, a special file, or an unknown state is at a file path. - Keep the application path in a file storage when a pull request preview is parsed. Preview deployments of the current parser write their `-pr-<id>` copies through LocalFileVolume::forPullRequest(). - Remove the unreachable file sync from the service resource page.
754 lines
30 KiB
PHP
754 lines
30 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Service;
|
|
|
|
use App\Actions\Database\StartDatabaseProxy;
|
|
use App\Actions\Database\StopDatabaseProxy;
|
|
use App\Actions\Service\DeleteService;
|
|
use App\Enums\ProxyTypes;
|
|
use App\Models\S3Storage;
|
|
use App\Models\Server;
|
|
use App\Models\ServerSetting;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceApplication;
|
|
use App\Models\ServiceDatabase;
|
|
use App\Services\Dns\ManagedDnsRecordCleanup;
|
|
use App\Support\ValidationPatterns;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Component;
|
|
|
|
class Index extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public ?Service $service = null;
|
|
|
|
public ServiceApplication|ServiceDatabase|null $serviceApplication = null;
|
|
|
|
public ?ServiceDatabase $serviceDatabase = null;
|
|
|
|
public ?string $resourceType = null;
|
|
|
|
public ?string $currentRoute = null;
|
|
|
|
public bool $embedded = false;
|
|
|
|
public array $parameters;
|
|
|
|
public array $query;
|
|
|
|
public Collection $services;
|
|
|
|
public $s3s;
|
|
|
|
public ?Server $server = null;
|
|
|
|
// Database-specific properties
|
|
public ?string $db_url_public = null;
|
|
|
|
public $fileStorages;
|
|
|
|
public ?string $humanName = null;
|
|
|
|
public ?string $description = null;
|
|
|
|
public ?string $image = null;
|
|
|
|
public bool $excludeFromStatus = false;
|
|
|
|
public mixed $publicPort = null;
|
|
|
|
public mixed $publicPortTimeout = 3600;
|
|
|
|
public bool $isPublic = false;
|
|
|
|
public bool $isLogDrainEnabled = false;
|
|
|
|
// Application-specific properties
|
|
public $docker_cleanup = true;
|
|
|
|
public $delete_volumes = true;
|
|
|
|
public $domainConflicts = [];
|
|
|
|
public $showDomainConflictModal = false;
|
|
|
|
public $forceSaveDomains = false;
|
|
|
|
public $showPortWarningModal = false;
|
|
|
|
public $forceRemovePort = false;
|
|
|
|
public $requiredPort = null;
|
|
|
|
public ?string $fqdn = null;
|
|
|
|
public bool $isGzipEnabled = false;
|
|
|
|
public bool $isStripprefixEnabled = false;
|
|
|
|
/**
|
|
* How the Traefik certificate resolver is chosen: `server`, `none`, or `custom`.
|
|
*/
|
|
public string $certificateResolverMode = 'server';
|
|
|
|
public ?string $certificateResolverName = null;
|
|
|
|
public mixed $maxRestartCount = 0;
|
|
|
|
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh', 'refreshFileStorages'];
|
|
|
|
protected $rules = [
|
|
'humanName' => 'nullable',
|
|
'description' => 'nullable',
|
|
'image' => 'required',
|
|
'excludeFromStatus' => 'required|boolean',
|
|
'publicPort' => 'nullable|integer|min:1|max:65535',
|
|
'publicPortTimeout' => 'nullable|integer|min:1',
|
|
'isPublic' => 'required|boolean',
|
|
'isLogDrainEnabled' => 'required|boolean',
|
|
'maxRestartCount' => 'integer|min:0',
|
|
// Application-specific rules
|
|
'fqdn' => 'nullable',
|
|
'isGzipEnabled' => 'nullable|boolean',
|
|
'isStripprefixEnabled' => 'nullable|boolean',
|
|
'certificateResolverMode' => 'required|in:server,none,custom',
|
|
'certificateResolverName' => ['exclude_unless:certificateResolverMode,custom', 'required', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN, 'not_in:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE],
|
|
];
|
|
|
|
protected $validationAttributes = [
|
|
'certificateResolverMode' => 'certificate resolver',
|
|
'certificateResolverName' => 'certificate resolver name',
|
|
];
|
|
|
|
public function mount(
|
|
ServiceApplication|ServiceDatabase|null $serviceApplication = null,
|
|
bool $embedded = false,
|
|
) {
|
|
try {
|
|
$this->embedded = $embedded;
|
|
$this->services = collect([]);
|
|
if ($serviceApplication instanceof ServiceDatabase) {
|
|
$this->service = $serviceApplication->service;
|
|
$this->authorize('view', $this->service);
|
|
$this->parameters = [
|
|
'project_uuid' => $this->service->environment->project->uuid,
|
|
'environment_uuid' => $this->service->environment->uuid,
|
|
'service_uuid' => $this->service->uuid,
|
|
'stack_service_uuid' => $serviceApplication->uuid,
|
|
];
|
|
$this->query = request()->query();
|
|
$this->currentRoute = 'project.service.index';
|
|
$this->serviceDatabase = $serviceApplication;
|
|
$this->serviceApplication = null;
|
|
$this->resourceType = 'database';
|
|
$this->initializeDatabaseProperties();
|
|
$this->s3s = $this->serviceTeamS3Storages();
|
|
|
|
return;
|
|
}
|
|
if ($serviceApplication) {
|
|
$this->service = $serviceApplication->service;
|
|
$this->authorize('view', $this->service);
|
|
$this->parameters = [
|
|
'project_uuid' => $this->service->environment->project->uuid,
|
|
'environment_uuid' => $this->service->environment->uuid,
|
|
'service_uuid' => $this->service->uuid,
|
|
'stack_service_uuid' => $serviceApplication->uuid,
|
|
];
|
|
$this->query = request()->query();
|
|
$this->currentRoute = 'project.service.index';
|
|
$this->serviceApplication = $serviceApplication;
|
|
$this->resourceType = 'application';
|
|
$this->initializeApplicationProperties();
|
|
$this->s3s = $this->serviceTeamS3Storages();
|
|
|
|
return;
|
|
}
|
|
$this->parameters = get_route_parameters();
|
|
$project = currentTeam()
|
|
->projects()
|
|
->select('id', 'uuid', 'team_id')
|
|
->where('uuid', $this->parameters['project_uuid'])
|
|
->firstOrFail();
|
|
$environment = $project->environments()
|
|
->select('id', 'uuid', 'name', 'project_id')
|
|
->where('uuid', $this->parameters['environment_uuid'])
|
|
->firstOrFail();
|
|
$this->service = $environment->services()->whereUuid($this->parameters['service_uuid'])->firstOrFail();
|
|
$this->authorize('view', $this->service);
|
|
|
|
// The settings of a stack service open in a modal on the configuration page.
|
|
return redirect()->route(
|
|
'project.service.configuration',
|
|
collect($this->parameters)->except('stack_service_uuid')->all(),
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
private function serviceTeamS3Storages(): Collection
|
|
{
|
|
return S3Storage::query()
|
|
->where('team_id', $this->service->team()->id)
|
|
->where('is_usable', true)
|
|
->get();
|
|
}
|
|
|
|
private function initializeDatabaseProperties(): void
|
|
{
|
|
$this->server = $this->serviceDatabase->service->destination->server;
|
|
if ($this->serviceDatabase->is_public) {
|
|
$this->db_url_public = $this->serviceDatabase->getServiceDatabaseUrl();
|
|
}
|
|
$this->refreshFileStorages();
|
|
$this->syncDatabaseData(false);
|
|
|
|
}
|
|
|
|
private function syncDatabaseData(bool $toModel = false): void
|
|
{
|
|
if ($toModel) {
|
|
$this->serviceDatabase->human_name = $this->humanName;
|
|
$this->serviceDatabase->description = $this->description;
|
|
$this->serviceDatabase->image = $this->image;
|
|
$this->serviceDatabase->exclude_from_status = $this->excludeFromStatus;
|
|
$this->serviceDatabase->public_port = $this->publicPort ?: null;
|
|
$this->serviceDatabase->public_port_timeout = $this->publicPortTimeout ?: null;
|
|
$this->serviceDatabase->is_public = $this->isPublic;
|
|
$this->serviceDatabase->is_log_drain_enabled = $this->isLogDrainEnabled;
|
|
} else {
|
|
$this->humanName = $this->serviceDatabase->human_name;
|
|
$this->description = $this->serviceDatabase->description;
|
|
$this->image = $this->serviceDatabase->image;
|
|
$this->excludeFromStatus = $this->serviceDatabase->exclude_from_status ?? false;
|
|
$this->publicPort = $this->serviceDatabase->public_port;
|
|
$this->publicPortTimeout = $this->serviceDatabase->public_port_timeout;
|
|
$this->isPublic = $this->serviceDatabase->is_public ?? false;
|
|
$this->isLogDrainEnabled = $this->serviceDatabase->is_log_drain_enabled ?? false;
|
|
}
|
|
}
|
|
|
|
public function generateDockerCompose()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->service);
|
|
$this->service->parse();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
// Database-specific methods
|
|
public function refreshFileStorages()
|
|
{
|
|
if ($this->serviceDatabase) {
|
|
$this->fileStorages = $this->serviceDatabase->fileStorages()->get();
|
|
}
|
|
}
|
|
|
|
public function deleteDatabase($password, $selectedActions = [])
|
|
{
|
|
try {
|
|
$this->authorize('delete', $this->serviceDatabase);
|
|
|
|
if (! verifyPasswordConfirmation($password, $this)) {
|
|
return 'The provided password is incorrect.';
|
|
}
|
|
|
|
$containerRemoved = app(DeleteService::class)->removeSubresourceContainer($this->serviceDatabase);
|
|
$this->serviceDatabase->delete();
|
|
$this->auditServiceSubResource('deleted', $this->serviceDatabase, ['container_removed' => $containerRemoved]);
|
|
$containerRemoved
|
|
? $this->dispatch('success', 'Database deleted.')
|
|
: $this->dispatch('warning', 'Database deleted from Coolify. The server does not respond, so its container is removed when the service starts again.');
|
|
|
|
return redirectRoute($this, 'project.service.configuration', $this->parameters);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function instantSaveExclude()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceDatabase);
|
|
$this->submitDatabase();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function instantSaveLogDrain()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceDatabase);
|
|
if (! $this->serviceDatabase->service->destination->server->isLogDrainEnabled()) {
|
|
$this->isLogDrainEnabled = false;
|
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
|
|
|
return;
|
|
}
|
|
$this->submitDatabase();
|
|
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function convertToApplication()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceDatabase);
|
|
$service = $this->serviceDatabase->service;
|
|
$serviceDatabase = $this->serviceDatabase;
|
|
|
|
// Check if application with same name already exists
|
|
if ($service->applications()->where('name', $serviceDatabase->name)->exists()) {
|
|
throw new \Exception('An application with this name already exists.');
|
|
}
|
|
|
|
// Create new parameters removing database_uuid
|
|
$redirectParams = collect($this->parameters)
|
|
->except('database_uuid')
|
|
->all();
|
|
|
|
$serviceApplication = DB::transaction(function () use ($service, $serviceDatabase) {
|
|
$serviceApplication = $service->applications()->create([
|
|
'name' => $serviceDatabase->name,
|
|
'human_name' => $serviceDatabase->human_name,
|
|
'description' => $serviceDatabase->description,
|
|
'exclude_from_status' => $serviceDatabase->exclude_from_status,
|
|
'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled,
|
|
'image' => $serviceDatabase->image,
|
|
'service_id' => $service->id,
|
|
'is_migrated' => true,
|
|
]);
|
|
$serviceDatabase->delete();
|
|
|
|
return $serviceApplication;
|
|
});
|
|
$this->auditServiceSubResource('converted_to_application', $serviceDatabase, [
|
|
'service_application_uuid' => $serviceApplication->uuid,
|
|
]);
|
|
|
|
return redirectRoute($this, 'project.service.configuration', $redirectParams);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function enablePublicAccess(): void
|
|
{
|
|
$this->isPublic = true;
|
|
$this->instantSave();
|
|
}
|
|
|
|
public function disablePublicAccess(): void
|
|
{
|
|
$this->isPublic = false;
|
|
$this->instantSave();
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
$this->authorize('update', $this->serviceDatabase);
|
|
try {
|
|
if ($this->isPublic) {
|
|
if (! $this->publicPort) {
|
|
$this->dispatch('error', 'Public port is required.');
|
|
$this->isPublic = false;
|
|
|
|
return;
|
|
}
|
|
if (! str($this->serviceDatabase->status)->startsWith('running')) {
|
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
|
$this->isPublic = false;
|
|
|
|
return;
|
|
}
|
|
$this->persistPublicAccess();
|
|
StartDatabaseProxy::run($this->serviceDatabase);
|
|
$this->db_url_public = $this->serviceDatabase->getServiceDatabaseUrl();
|
|
$this->dispatch('success', 'Database is now publicly accessible.');
|
|
} else {
|
|
$this->persistPublicAccess();
|
|
StopDatabaseProxy::run($this->serviceDatabase);
|
|
$this->db_url_public = null;
|
|
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->isPublic = ! $this->isPublic;
|
|
$this->persistPublicAccess();
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
private function persistPublicAccess(): void
|
|
{
|
|
$this->serviceDatabase->fill([
|
|
'is_public' => $this->isPublic,
|
|
'public_port' => $this->publicPort ?: null,
|
|
'public_port_timeout' => $this->publicPortTimeout ?: null,
|
|
]);
|
|
$changedFields = auditChangedFields($this->serviceDatabase);
|
|
$this->serviceDatabase->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceDatabase, $changedFields, [
|
|
'is_public' => (bool) $this->serviceDatabase->is_public,
|
|
'public_port' => $this->serviceDatabase->public_port,
|
|
]);
|
|
}
|
|
|
|
public function submitDatabase()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceDatabase);
|
|
$this->validate();
|
|
$this->syncDatabaseData(true);
|
|
$changedFields = auditChangedFields($this->serviceDatabase);
|
|
$this->serviceDatabase->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceDatabase, $changedFields);
|
|
$this->serviceDatabase->refresh();
|
|
$this->syncDatabaseData(false);
|
|
updateCompose($this->serviceDatabase);
|
|
$this->dispatch('success', 'Database saved.');
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
} finally {
|
|
$this->dispatch('generateDockerCompose');
|
|
}
|
|
}
|
|
|
|
// Application-specific methods
|
|
private function initializeApplicationProperties(): void
|
|
{
|
|
$this->requiredPort = $this->serviceApplication->getRequiredPort();
|
|
$this->syncApplicationData(false);
|
|
}
|
|
|
|
private function syncApplicationData(bool $toModel = false): void
|
|
{
|
|
if ($toModel) {
|
|
$this->serviceApplication->human_name = $this->humanName;
|
|
$this->serviceApplication->description = $this->description;
|
|
$this->serviceApplication->setEditableUrls($this->fqdn);
|
|
$this->serviceApplication->image = $this->image;
|
|
$this->serviceApplication->exclude_from_status = $this->excludeFromStatus;
|
|
$this->serviceApplication->is_log_drain_enabled = $this->isLogDrainEnabled;
|
|
$this->serviceApplication->is_gzip_enabled = $this->isGzipEnabled;
|
|
$this->serviceApplication->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
|
$this->serviceApplication->traefik_certificate_resolver = match ($this->certificateResolverMode) {
|
|
'none' => ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
|
'custom' => $this->certificateResolverName,
|
|
default => null,
|
|
};
|
|
if ($this->serviceApplication->max_restart_count !== (int) $this->maxRestartCount) {
|
|
$this->serviceApplication->restart_limit_reached = false;
|
|
}
|
|
$this->serviceApplication->max_restart_count = $this->maxRestartCount;
|
|
} else {
|
|
$this->humanName = $this->serviceApplication->human_name;
|
|
$this->description = $this->serviceApplication->description;
|
|
$this->fqdn = $this->serviceApplication->url;
|
|
$this->image = $this->serviceApplication->image;
|
|
$this->excludeFromStatus = data_get($this->serviceApplication, 'exclude_from_status', false);
|
|
$this->isLogDrainEnabled = data_get($this->serviceApplication, 'is_log_drain_enabled', false);
|
|
$this->isGzipEnabled = data_get($this->serviceApplication, 'is_gzip_enabled', true);
|
|
$this->isStripprefixEnabled = data_get($this->serviceApplication, 'is_stripprefix_enabled', true);
|
|
$resolver = $this->serviceApplication->traefik_certificate_resolver;
|
|
$this->certificateResolverMode = match (true) {
|
|
blank($resolver) => 'server',
|
|
$resolver === ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE => 'none',
|
|
default => 'custom',
|
|
};
|
|
$this->certificateResolverName = $this->certificateResolverMode === 'custom' ? $resolver : null;
|
|
$this->maxRestartCount = $this->serviceApplication->max_restart_count ?? 0;
|
|
}
|
|
}
|
|
|
|
private function validateCertificateResolver(): void
|
|
{
|
|
$this->validate([
|
|
'certificateResolverMode' => $this->rules['certificateResolverMode'],
|
|
'certificateResolverName' => $this->rules['certificateResolverName'],
|
|
]);
|
|
}
|
|
|
|
public function saveMaxRestartCount(): void
|
|
{
|
|
$this->authorize('update', $this->serviceApplication);
|
|
$validated = $this->validate([
|
|
'maxRestartCount' => 'integer|min:0',
|
|
]);
|
|
|
|
$this->serviceApplication->fill([
|
|
'max_restart_count' => $validated['maxRestartCount'],
|
|
'restart_limit_reached' => false,
|
|
]);
|
|
$changedFields = auditChangedFields($this->serviceApplication);
|
|
$this->serviceApplication->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceApplication, $changedFields);
|
|
$this->dispatch('success', 'Max restart count saved.');
|
|
}
|
|
|
|
public function instantSaveApplication()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceApplication);
|
|
$this->submitApplication();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function instantSaveApplicationSettings()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceApplication);
|
|
$this->serviceApplication->is_gzip_enabled = $this->isGzipEnabled;
|
|
$this->serviceApplication->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
|
$this->serviceApplication->exclude_from_status = $this->excludeFromStatus;
|
|
$changedFields = auditChangedFields($this->serviceApplication);
|
|
$this->serviceApplication->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceApplication, $changedFields);
|
|
$this->dispatch('success', 'Settings saved.');
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function instantSaveApplicationAdvanced()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceApplication);
|
|
if (! $this->serviceApplication->service->destination->server->isLogDrainEnabled()) {
|
|
$this->isLogDrainEnabled = false;
|
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
|
|
|
return;
|
|
}
|
|
$this->validateCertificateResolver();
|
|
$this->syncApplicationData(true);
|
|
$changedFields = auditChangedFields($this->serviceApplication);
|
|
$this->serviceApplication->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceApplication, $changedFields);
|
|
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function deleteApplication($password, $selectedActions = [])
|
|
{
|
|
try {
|
|
$this->authorize('delete', $this->serviceApplication);
|
|
|
|
if (! verifyPasswordConfirmation($password, $this)) {
|
|
return 'The provided password is incorrect.';
|
|
}
|
|
|
|
$containerRemoved = app(DeleteService::class)->removeSubresourceContainer($this->serviceApplication);
|
|
$this->serviceApplication->delete();
|
|
$this->auditServiceSubResource('deleted', $this->serviceApplication, ['container_removed' => $containerRemoved]);
|
|
$containerRemoved
|
|
? $this->dispatch('success', 'Application deleted.')
|
|
: $this->dispatch('warning', 'Application deleted from Coolify. The server does not respond, so its container is removed when the service starts again.');
|
|
|
|
return redirectRoute($this, 'project.service.configuration', $this->parameters);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function convertToDatabase()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->serviceApplication);
|
|
$service = $this->serviceApplication->service;
|
|
$serviceApplication = $this->serviceApplication;
|
|
|
|
if ($service->databases()->where('name', $serviceApplication->name)->exists()) {
|
|
throw new \Exception('A database with this name already exists.');
|
|
}
|
|
|
|
$redirectParams = collect($this->parameters)
|
|
->except('database_uuid')
|
|
->all();
|
|
$serviceDatabase = DB::transaction(function () use ($service, $serviceApplication) {
|
|
$serviceDatabase = $service->databases()->create([
|
|
'name' => $serviceApplication->name,
|
|
'human_name' => $serviceApplication->human_name,
|
|
'description' => $serviceApplication->description,
|
|
'exclude_from_status' => $serviceApplication->exclude_from_status,
|
|
'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled,
|
|
'image' => $serviceApplication->image,
|
|
'service_id' => $service->id,
|
|
'is_migrated' => true,
|
|
]);
|
|
$serviceApplication->delete();
|
|
|
|
return $serviceDatabase;
|
|
});
|
|
$this->auditServiceSubResource('converted_to_database', $serviceApplication, [
|
|
'service_database_uuid' => $serviceDatabase->uuid,
|
|
]);
|
|
|
|
return redirectRoute($this, 'project.service.configuration', $redirectParams);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function confirmDomainUsage()
|
|
{
|
|
$this->forceSaveDomains = true;
|
|
$this->showDomainConflictModal = false;
|
|
$this->submitApplication();
|
|
}
|
|
|
|
public function confirmRemovePort()
|
|
{
|
|
$this->forceRemovePort = true;
|
|
$this->showPortWarningModal = false;
|
|
$this->submitApplication();
|
|
}
|
|
|
|
public function cancelRemovePort()
|
|
{
|
|
$this->showPortWarningModal = false;
|
|
$this->syncApplicationData(false);
|
|
}
|
|
|
|
public function submitApplication()
|
|
{
|
|
try {
|
|
$persistedApplication = $this->serviceApplication->fresh();
|
|
$previousDnsHostnames = app(ManagedDnsRecordCleanup::class)->hostnamesOf($persistedApplication);
|
|
$previousEditableUrls = $persistedApplication->url;
|
|
$previousFqdn = $persistedApplication->fqdn;
|
|
$previousPortOverrides = $persistedApplication->domain_port_overrides;
|
|
$this->authorize('update', $this->serviceApplication);
|
|
$this->validate([
|
|
'fqdn' => ValidationPatterns::applicationDomainRules(),
|
|
]);
|
|
$this->validateCertificateResolver();
|
|
|
|
$this->fqdn = ValidationPatterns::normalizeApplicationDomains($this->fqdn);
|
|
$warning = sslipDomainWarning($this->fqdn);
|
|
if ($warning) {
|
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
|
}
|
|
|
|
$this->syncApplicationData(true);
|
|
|
|
if (! $this->forceSaveDomains) {
|
|
$result = checkDomainUsage(resource: $this->serviceApplication);
|
|
if ($result['hasConflicts']) {
|
|
$this->domainConflicts = $result['conflicts'];
|
|
$this->showDomainConflictModal = true;
|
|
|
|
return;
|
|
}
|
|
} else {
|
|
$this->forceSaveDomains = false;
|
|
}
|
|
|
|
if (! $this->forceRemovePort) {
|
|
$requiredPort = $this->serviceApplication->getRequiredPort();
|
|
|
|
if ($requiredPort !== null) {
|
|
foreach (str($this->fqdn)->trim()->explode(',') as $fqdn) {
|
|
$fqdn = trim((string) $fqdn);
|
|
if ($fqdn === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($this->serviceApplication->portRequiresConfirmation($fqdn, $requiredPort, $previousEditableUrls)) {
|
|
$this->requiredPort = $requiredPort;
|
|
$this->showPortWarningModal = true;
|
|
$this->serviceApplication->fqdn = $previousFqdn;
|
|
$this->serviceApplication->domain_port_overrides = $previousPortOverrides;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$this->forceRemovePort = false;
|
|
}
|
|
|
|
$this->validate();
|
|
$changedFields = auditChangedFields($this->serviceApplication);
|
|
$this->serviceApplication->save();
|
|
$this->auditServiceSubResourceUpdate($this->serviceApplication, $changedFields);
|
|
$this->serviceApplication->refresh();
|
|
app(ManagedDnsRecordCleanup::class)->queueReleaseOfRemovedHostnames($this->serviceApplication, $previousDnsHostnames, $this->service->team()->id);
|
|
$this->syncApplicationData(false);
|
|
updateCompose($this->serviceApplication);
|
|
if (str($this->serviceApplication->fqdn)->contains(',')) {
|
|
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
|
} else {
|
|
! $warning && $this->dispatch('success', 'Service saved.');
|
|
}
|
|
$this->dispatch('generateDockerCompose');
|
|
} catch (\Throwable $e) {
|
|
$originalFqdn = $this->serviceApplication->getOriginal('fqdn');
|
|
if ($originalFqdn !== $this->serviceApplication->fqdn) {
|
|
$this->serviceApplication->fqdn = $originalFqdn;
|
|
$this->syncApplicationData(false);
|
|
}
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$server = $this->resourceType === 'application' ? $this->service?->destination?->server : null;
|
|
|
|
return view('livewire.project.service.index', [
|
|
'showCertificateResolver' => $server?->proxyType() === ProxyTypes::TRAEFIK->value,
|
|
'serverCertificateResolver' => $server?->traefikCertificateResolver() ?? ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Records `ui.service_{application|database}.updated` with the attributes that the save changed.
|
|
*
|
|
* @param array<int, string> $changedFields Fields changed by the save, captured before it.
|
|
* @param array<string, mixed> $extra
|
|
*/
|
|
private function auditServiceSubResourceUpdate(ServiceApplication|ServiceDatabase $resource, array $changedFields, array $extra = []): void
|
|
{
|
|
if ($changedFields === []) {
|
|
return;
|
|
}
|
|
|
|
$this->auditServiceSubResource('updated', $resource, ['changed_fields' => $changedFields, ...$extra]);
|
|
}
|
|
|
|
/**
|
|
* Mirrors the context of `api.service_database.updated` for service applications and databases.
|
|
*
|
|
* @param array<string, mixed> $extra
|
|
*/
|
|
private function auditServiceSubResource(string $action, ServiceApplication|ServiceDatabase $resource, array $extra = []): void
|
|
{
|
|
$resourceKey = $resource instanceof ServiceApplication ? 'service_application' : 'service_database';
|
|
$service = $resource->service ?? $this->service;
|
|
|
|
auditLog("ui.{$resourceKey}.{$action}", [
|
|
'team_id' => $service?->team()?->id,
|
|
'service_uuid' => $service?->uuid,
|
|
'service_name' => $service?->name,
|
|
"{$resourceKey}_uuid" => $resource->uuid,
|
|
"{$resourceKey}_name" => $resource->name,
|
|
...$extra,
|
|
]);
|
|
}
|
|
}
|