Merge branch 'coollabsio:main' into fix-redis-db-ui
This commit is contained in:
@@ -66,9 +66,9 @@ class Advanced extends Component
|
||||
$this->dispatch('resetDefaultLabels', false);
|
||||
}
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
$this->application->parseRawCompose();
|
||||
$this->application->oldRawParser();
|
||||
} else {
|
||||
$this->application->parseCompose();
|
||||
$this->application->parse();
|
||||
}
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
@@ -96,6 +96,12 @@ class Advanced extends Component
|
||||
} else {
|
||||
$this->application->settings->custom_internal_name = null;
|
||||
}
|
||||
if (is_null($this->application->settings->custom_internal_name)) {
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Custom name saved.');
|
||||
|
||||
return;
|
||||
}
|
||||
$customInternalName = $this->application->settings->custom_internal_name;
|
||||
$server = $this->application->destination->server;
|
||||
$allApplications = $server->applications();
|
||||
|
||||
@@ -69,6 +69,20 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogLinesProperty()
|
||||
{
|
||||
return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
|
||||
$logLine['line'] = e($logLine['line']);
|
||||
$logLine['line'] = preg_replace(
|
||||
'/(https?:\/\/[^\s]+)/',
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
|
||||
$logLine['line'],
|
||||
);
|
||||
|
||||
return $logLine;
|
||||
});
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.deployment.show');
|
||||
|
||||
@@ -55,9 +55,14 @@ class DeploymentNavbar extends Component
|
||||
public function cancel()
|
||||
{
|
||||
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
|
||||
$build_server_id = $this->application_deployment_queue->build_server_id;
|
||||
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
|
||||
try {
|
||||
$server = Server::find($server_id);
|
||||
if ($this->application->settings->is_build_server_enabled) {
|
||||
$server = Server::find($build_server_id);
|
||||
} else {
|
||||
$server = Server::find($server_id);
|
||||
}
|
||||
if ($this->application_deployment_queue->logs) {
|
||||
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\LocalFileVolume;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -30,6 +29,8 @@ class General extends Component
|
||||
|
||||
public ?string $ports_exposes = null;
|
||||
|
||||
public bool $is_preserve_repository_enabled = false;
|
||||
|
||||
public bool $is_container_label_escape_enabled = true;
|
||||
|
||||
public $customLabels;
|
||||
@@ -130,7 +131,7 @@ class General extends Component
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->parsedServices = $this->application->parseCompose();
|
||||
$this->parsedServices = $this->application->parse();
|
||||
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
@@ -145,6 +146,7 @@ class General extends Component
|
||||
}
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
|
||||
@@ -168,9 +170,21 @@ class General extends Component
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
|
||||
// If port_exposes changed, reset default labels
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
|
||||
if ($this->application->settings->is_preserve_repository_enabled === false) {
|
||||
$this->application->fileStorages->each(function ($storage) {
|
||||
$storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
|
||||
$storage->save();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
@@ -179,39 +193,18 @@ class General extends Component
|
||||
if ($isInit && $this->application->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must reload the application to get the latest database changes
|
||||
// Why? Not sure, but it works.
|
||||
// $this->application->refresh();
|
||||
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
|
||||
if (is_null($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
$compose = $this->application->parseCompose();
|
||||
$services = data_get($compose, 'services');
|
||||
if ($services) {
|
||||
$volumes = collect($services)->map(function ($service) {
|
||||
return data_get($service, 'volumes');
|
||||
})->flatten()->filter(function ($volume) {
|
||||
return str($volume)->startsWith('/data/coolify');
|
||||
})->unique()->values();
|
||||
foreach ($volumes as $volume) {
|
||||
$source = str($volume)->before(':');
|
||||
$target = str($volume)->after(':')->beforeLast(':');
|
||||
|
||||
LocalFileVolume::updateOrCreate(
|
||||
[
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => get_class($this->application),
|
||||
],
|
||||
[
|
||||
'fs_path' => $source,
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => get_class($this->application),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->application->parse();
|
||||
$this->dispatch('success', 'Docker compose file loaded.');
|
||||
$this->dispatch('compose_loaded');
|
||||
$this->dispatch('refreshStorages');
|
||||
|
||||
@@ -21,6 +21,8 @@ class Heading extends Component
|
||||
|
||||
protected string $deploymentUuid;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
@@ -102,7 +104,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopApplication::run($this->application);
|
||||
StopApplication::run($this->application, false, $this->docker_cleanup);
|
||||
$this->application->status = 'exited';
|
||||
$this->application->save();
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
@@ -135,4 +137,13 @@ class Heading extends Component
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -79,8 +81,15 @@ class Previews extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$this->application->refresh();
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
|
||||
$url = Url::fromString($fqdn);
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
@@ -177,17 +186,20 @@ class Previews extends Component
|
||||
public function stop(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $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);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
|
||||
$this->dispatch('reloadWindow');
|
||||
|
||||
GetContainersStatus::run($server);
|
||||
$this->application->refresh();
|
||||
$this->dispatch('containerStatusUpdated');
|
||||
$this->dispatch('success', 'Preview Deployment stopped.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -196,16 +208,21 @@ class Previews extends Component
|
||||
public function delete(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $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);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
|
||||
|
||||
ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first()
|
||||
->delete();
|
||||
|
||||
$this->application->refresh();
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview deleted.');
|
||||
@@ -213,4 +230,49 @@ class Previews extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainers(array $containers, $server, int $timeout)
|
||||
{
|
||||
$processes = [];
|
||||
foreach ($containers as $container) {
|
||||
$containerName = str_replace('/', '', $container['Names']);
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||
}
|
||||
|
||||
$startTime = time();
|
||||
while (count($processes) > 0) {
|
||||
$finishedProcesses = array_filter($processes, function ($process) {
|
||||
return ! $process->running();
|
||||
});
|
||||
foreach (array_keys($finishedProcesses) as $containerName) {
|
||||
unset($processes[$containerName]);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, $server)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||
}
|
||||
|
||||
private function forceStopRemainingContainers(array $containerNames, $server)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
@@ -12,6 +14,12 @@ class BackupEdit extends Component
|
||||
|
||||
public $s3s;
|
||||
|
||||
public bool $delete_associated_backups_locally = false;
|
||||
|
||||
public bool $delete_associated_backups_s3 = false;
|
||||
|
||||
public bool $delete_associated_backups_sftp = false;
|
||||
|
||||
public ?string $status = null;
|
||||
|
||||
public array $parameters;
|
||||
@@ -46,10 +54,24 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->delete_associated_backups_locally) {
|
||||
$this->deleteAssociatedBackupsLocally();
|
||||
}
|
||||
if ($this->delete_associated_backups_s3) {
|
||||
$this->deleteAssociatedBackupsS3();
|
||||
}
|
||||
|
||||
$this->backup->delete();
|
||||
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$previousUrl = url()->previous();
|
||||
$url = Url::fromString($previousUrl);
|
||||
@@ -104,4 +126,66 @@ class BackupEdit extends Component
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsLocally()
|
||||
{
|
||||
$executions = $this->backup->executions;
|
||||
$backupFolder = null;
|
||||
|
||||
foreach ($executions as $execution) {
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$server = $this->backup->database->service->destination->server;
|
||||
} else {
|
||||
$server = $this->backup->database->destination->server;
|
||||
}
|
||||
|
||||
if (! $backupFolder) {
|
||||
$backupFolder = dirname($execution->filename);
|
||||
}
|
||||
|
||||
delete_backup_locally($execution->filename, $server);
|
||||
$execution->delete();
|
||||
}
|
||||
|
||||
if ($backupFolder) {
|
||||
$this->deleteEmptyBackupFolder($backupFolder, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsS3()
|
||||
{
|
||||
//Add function to delete backups from S3
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsSftp()
|
||||
{
|
||||
//Add function to delete backups from SFTP
|
||||
}
|
||||
|
||||
private function deleteEmptyBackupFolder($folderPath, $server)
|
||||
{
|
||||
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$folderPath'"], $server);
|
||||
|
||||
$parentFolder = dirname($folderPath);
|
||||
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkParentEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$parentFolder'"], $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-edit', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,28 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
{
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
|
||||
public $database;
|
||||
|
||||
public $executions = [];
|
||||
|
||||
public $setDeletableBackup;
|
||||
|
||||
public $delete_backup_s3 = true;
|
||||
|
||||
public $delete_backup_sftp = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
||||
@@ -32,19 +41,36 @@ class BackupExecutions extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($exeuctionId)
|
||||
#[On('deleteBackup')]
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
if (is_null($execution)) {
|
||||
$this->dispatch('error', 'Backup execution not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
||||
} else {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||
}
|
||||
|
||||
if ($this->delete_backup_s3) {
|
||||
// Add logic to delete from S3
|
||||
}
|
||||
|
||||
if ($this->delete_backup_sftp) {
|
||||
// Add logic to delete from SFTP
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
$this->dispatch('success', 'Backup deleted.');
|
||||
$this->refreshBackupExecutions();
|
||||
@@ -58,7 +84,66 @@ class BackupExecutions extends Component
|
||||
public function refreshBackupExecutions(): void
|
||||
{
|
||||
if ($this->backup) {
|
||||
$this->executions = $this->backup->executions()->get()->sortBy('created_at');
|
||||
$this->executions = $this->backup->executions()->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function mount(ScheduledDatabaseBackup $backup)
|
||||
{
|
||||
$this->backup = $backup;
|
||||
$this->database = $backup->database;
|
||||
$this->refreshBackupExecutions();
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if ($this->database) {
|
||||
$server = null;
|
||||
|
||||
if ($this->database instanceof \App\Models\ServiceDatabase) {
|
||||
$server = $this->database->service->destination->server;
|
||||
} elseif ($this->database->destination && $this->database->destination->server) {
|
||||
$server = $this->database->destination->server;
|
||||
}
|
||||
if ($server) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
return $serverTimezone;
|
||||
}
|
||||
|
||||
public function formatDateInServerTimezone($date)
|
||||
{
|
||||
$serverTimezone = $this->getServerTimezone();
|
||||
$dateObj = new \DateTime($date);
|
||||
try {
|
||||
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
|
||||
} catch (\Exception $e) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-executions', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
|
||||
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -42,6 +43,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -30,6 +30,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -40,6 +41,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -14,6 +14,8 @@ class Heading extends Component
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
@@ -54,7 +56,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopDatabase::run($this->database);
|
||||
StopDatabase::run($this->database, false, $this->docker_cleanup);
|
||||
$this->database->status = 'exited';
|
||||
$this->database->save();
|
||||
$this->check_status();
|
||||
@@ -71,4 +73,13 @@ class Heading extends Component
|
||||
$activity = StartDatabase::run($this->database);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -42,6 +43,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -34,6 +34,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -48,6 +49,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -33,6 +33,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -46,6 +47,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -34,6 +34,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -48,6 +49,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -49,6 +49,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -65,6 +66,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -32,6 +32,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -44,6 +45,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $environmentName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteProject extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $projectName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->projectName = Project::findOrFail($this->project_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\New;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -58,12 +60,26 @@ class DockerCompose extends Component
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
|
||||
|
||||
$destination_uuid = $this->query['destination'];
|
||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||
if (! $destination) {
|
||||
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
|
||||
}
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found. What?!');
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$service = Service::create([
|
||||
'name' => 'service'.Str::random(10),
|
||||
'docker_compose_raw' => $this->dockerComposeRaw,
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => (int) $server_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
]);
|
||||
|
||||
$variables = parseEnvFormatToArray($this->envFile);
|
||||
foreach ($variables as $key => $variable) {
|
||||
EnvironmentVariable::create([
|
||||
|
||||
@@ -99,6 +99,16 @@ class PublicGitRepository extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDockerComposeLocation()
|
||||
{
|
||||
if ($this->docker_compose_location) {
|
||||
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
|
||||
if (! str($this->docker_compose_location)->startsWith('/')) {
|
||||
$this->docker_compose_location = '/'.$this->docker_compose_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
||||
@@ -45,6 +45,8 @@ class Select extends Component
|
||||
|
||||
public ?string $selectedEnvironment = null;
|
||||
|
||||
public string $postgresql_type = 'postgres:16-alpine';
|
||||
|
||||
public ?string $existingPostgresqlUrl = null;
|
||||
|
||||
public ?string $search = null;
|
||||
@@ -202,6 +204,8 @@ class Select extends Component
|
||||
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
|
||||
if ($docker) {
|
||||
$this->setDestination($docker->uuid);
|
||||
|
||||
return $this->whatToDoNext();
|
||||
}
|
||||
}
|
||||
$this->current_step = 'destinations';
|
||||
@@ -211,15 +215,38 @@ class Select extends Component
|
||||
{
|
||||
$this->destination_uuid = $destination_uuid;
|
||||
|
||||
return $this->whatToDoNext();
|
||||
}
|
||||
|
||||
public function setPostgresqlType(string $type)
|
||||
{
|
||||
$this->postgresql_type = $type;
|
||||
|
||||
return redirect()->route('project.resource.create', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
'type' => $this->type,
|
||||
'destination' => $this->destination_uuid,
|
||||
'server_id' => $this->server_id,
|
||||
'database_image' => $this->postgresql_type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function whatToDoNext()
|
||||
{
|
||||
if ($this->type === 'postgresql') {
|
||||
$this->current_step = 'select-postgresql-type';
|
||||
} else {
|
||||
return redirect()->route('project.resource.create', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
'type' => $this->type,
|
||||
'destination' => $this->destination_uuid,
|
||||
'server_id' => $this->server_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadServers()
|
||||
{
|
||||
$this->servers = Server::isUsable()->get();
|
||||
|
||||
@@ -18,6 +18,7 @@ class Create extends Component
|
||||
$type = str(request()->query('type'));
|
||||
$destination_uuid = request()->query('destination');
|
||||
$server_id = request()->query('server_id');
|
||||
$database_image = request()->query('database_image');
|
||||
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (! $project) {
|
||||
@@ -33,7 +34,11 @@ class Create extends Component
|
||||
|
||||
if (in_array($type, DATABASE_TYPES)) {
|
||||
if ($type->value() === 'postgresql') {
|
||||
$database = create_standalone_postgresql($environment->id, $destination_uuid);
|
||||
$database = create_standalone_postgresql(
|
||||
environmentId: $environment->id,
|
||||
destinationUuid: $destination_uuid,
|
||||
databaseImage: $database_image
|
||||
);
|
||||
} elseif ($type->value() === 'redis') {
|
||||
$database = create_standalone_redis($environment->id, $destination_uuid);
|
||||
} elseif ($type->value() === 'mongodb') {
|
||||
@@ -86,18 +91,16 @@ class Create extends Component
|
||||
$oneClickDotEnvs->each(function ($value) use ($service) {
|
||||
$key = str()->before($value, '=');
|
||||
$value = str(str()->after($value, '='));
|
||||
$generatedValue = $value;
|
||||
if ($value->contains('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$generatedValue = generateEnvValue($command->value(), $service);
|
||||
if ($value) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'service_id' => $service->id,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $generatedValue,
|
||||
'service_id' => $service->id,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
});
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
|
||||
@@ -25,6 +25,7 @@ class Configuration extends Component
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
|
||||
'check_status',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ class Configuration extends Component
|
||||
$application = $this->service->applications->find($id);
|
||||
if ($application) {
|
||||
$application->restart();
|
||||
$this->dispatch('success', 'Application restarted successfully.');
|
||||
$this->dispatch('success', 'Service application restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -64,7 +65,7 @@ class Configuration extends Component
|
||||
$database = $this->service->databases->find($id);
|
||||
if ($database) {
|
||||
$database->restart();
|
||||
$this->dispatch('success', 'Database restarted successfully.');
|
||||
$this->dispatch('success', 'Service database restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -75,6 +76,12 @@ class Configuration extends Component
|
||||
{
|
||||
try {
|
||||
GetContainersStatus::run($this->service->server);
|
||||
$this->service->applications->each(function ($application) {
|
||||
$application->refresh();
|
||||
});
|
||||
$this->service->databases->each(function ($database) {
|
||||
$database->refresh();
|
||||
});
|
||||
$this->dispatch('$refresh');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -11,7 +11,11 @@ class EditCompose extends Component
|
||||
|
||||
public $serviceId;
|
||||
|
||||
protected $listeners = ['refreshEnvs', 'envsUpdated'];
|
||||
protected $listeners = [
|
||||
'refreshEnvs',
|
||||
'envsUpdated',
|
||||
'refresh' => 'envsUpdated',
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
@@ -39,6 +43,7 @@ class EditCompose extends Component
|
||||
{
|
||||
$this->dispatch('info', 'Saving new docker compose...');
|
||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
||||
@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
@@ -33,6 +35,7 @@ class FileStorage extends Component
|
||||
'fileStorage.fs_path' => 'required',
|
||||
'fileStorage.mount_path' => 'required',
|
||||
'fileStorage.content' => 'nullable',
|
||||
'fileStorage.is_based_on_git' => 'required|boolean',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -45,6 +48,7 @@ class FileStorage extends Component
|
||||
$this->workdir = null;
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
@@ -53,6 +57,7 @@ class FileStorage extends Component
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = true;
|
||||
$this->fileStorage->content = null;
|
||||
$this->fileStorage->is_based_on_git = false;
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -68,6 +73,9 @@ class FileStorage extends Component
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = false;
|
||||
$this->fileStorage->content = null;
|
||||
if (data_get($this->resource, 'settings.is_preserve_repository_enabled')) {
|
||||
$this->fileStorage->is_based_on_git = true;
|
||||
}
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -77,8 +85,14 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$message = 'File deleted.';
|
||||
if ($this->fileStorage->is_directory) {
|
||||
@@ -123,6 +137,13 @@ class FileStorage extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.file-storage');
|
||||
return view('livewire.project.service.file-storage', [
|
||||
'directoryDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
|
||||
],
|
||||
'fileDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ class Navbar extends Component
|
||||
|
||||
public $isDeploymentProgress = false;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $title = 'Configuration';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
|
||||
@@ -40,7 +44,7 @@ class Navbar extends Component
|
||||
|
||||
public function serviceStarted()
|
||||
{
|
||||
$this->dispatch('success', 'Service status changed.');
|
||||
// $this->dispatch('success', 'Service status changed.');
|
||||
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
||||
$this->service->isConfigurationChanged(true);
|
||||
$this->dispatch('configurationChanged');
|
||||
@@ -60,11 +64,6 @@ class Navbar extends Component
|
||||
$this->dispatch('success', 'Service status updated.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar');
|
||||
}
|
||||
|
||||
public function checkDeployments()
|
||||
{
|
||||
try {
|
||||
@@ -95,14 +94,9 @@ class Navbar extends Component
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function stop(bool $forceCleanup = false)
|
||||
public function stop()
|
||||
{
|
||||
StopService::run($this->service);
|
||||
if ($forceCleanup) {
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
StopService::run($this->service, false, $this->docker_cleanup);
|
||||
ServiceStatusChanged::dispatch();
|
||||
}
|
||||
|
||||
@@ -121,4 +115,13 @@ class Navbar extends Component
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $delete_volumes = true;
|
||||
|
||||
protected $rules = [
|
||||
'application.human_name' => 'nullable',
|
||||
'application.description' => 'nullable',
|
||||
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view');
|
||||
}
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->application->delete();
|
||||
$this->dispatch('success', 'Application deleted.');
|
||||
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('generateDockerCompose');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class StackForm extends Component
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules', 'nullable');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$isPassword = data_get($field, 'isPassword', false);
|
||||
$this->fields->put($key, [
|
||||
'serviceName' => $serviceName,
|
||||
'key' => $key,
|
||||
@@ -47,13 +47,21 @@ class StackForm extends Component
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
}
|
||||
}
|
||||
$this->fields = $this->fields->sortBy('name');
|
||||
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
|
||||
return $group->sortBy(function ($field) {
|
||||
return data_get($field, 'isPassword') ? 1 : 0;
|
||||
})->mapWithKeys(function ($field) {
|
||||
return [$field['key'] => $field];
|
||||
});
|
||||
})->flatMap(function ($group) {
|
||||
return $group;
|
||||
});
|
||||
}
|
||||
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->submit();
|
||||
$this->submit(notify: false);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -62,7 +70,7 @@ class StackForm extends Component
|
||||
$this->dispatch('success', 'Service settings saved.');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
public function submit($notify = true)
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
@@ -76,7 +84,7 @@ class StackForm extends Component
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
$this->dispatch('refreshEnvs');
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
$notify && $this->dispatch('success', 'Service saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -10,6 +15,8 @@ class Danger extends Component
|
||||
{
|
||||
public $resource;
|
||||
|
||||
public $resourceName;
|
||||
|
||||
public $projectUuid;
|
||||
|
||||
public $environmentName;
|
||||
@@ -18,22 +25,93 @@ class Danger extends Component
|
||||
|
||||
public bool $delete_volumes = true;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public bool $delete_connected_networks = true;
|
||||
|
||||
public ?string $modalId = null;
|
||||
|
||||
public string $resourceDomain = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->modalId = new Cuid2;
|
||||
$parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2;
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentName = data_get($parameters, 'environment_name');
|
||||
|
||||
if ($this->resource === null) {
|
||||
if (isset($parameters['service_uuid'])) {
|
||||
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->resource === null) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! method_exists($this->resource, 'type')) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$this->resourceName = $this->resource->name ?? 'Application';
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
case 'standalone-redis':
|
||||
case 'standalone-mongodb':
|
||||
case 'standalone-mysql':
|
||||
case 'standalone-mariadb':
|
||||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$this->resourceName = $this->resource->name ?? 'Database';
|
||||
break;
|
||||
case 'service':
|
||||
$this->resourceName = $this->resource->name ?? 'Service';
|
||||
break;
|
||||
case 'service-application':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Application';
|
||||
break;
|
||||
case 'service-database':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Database';
|
||||
break;
|
||||
default:
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
$this->addError('resource', 'Resource not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// $this->authorize('delete', $this->resource);
|
||||
$this->resource->delete();
|
||||
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
|
||||
DeleteResourceJob::dispatch(
|
||||
$this->resource,
|
||||
$this->delete_configurations,
|
||||
$this->delete_volumes,
|
||||
$this->docker_cleanup,
|
||||
$this->delete_connected_networks
|
||||
);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
@@ -43,4 +121,19 @@ class Danger extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.danger', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
|
||||
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -115,8 +117,14 @@ class Destination extends Component
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
}
|
||||
|
||||
public function removeServer(int $network_id, int $server_id)
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ class All extends Component
|
||||
public string $view = 'normal';
|
||||
|
||||
protected $listeners = [
|
||||
'refreshEnvs',
|
||||
'saveKey' => 'submit',
|
||||
'refreshEnvs',
|
||||
'environmentVariableDeleted' => 'refreshEnvs',
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
@@ -40,220 +41,240 @@ class All extends Component
|
||||
$this->showPreview = true;
|
||||
}
|
||||
$this->modalId = new Cuid2;
|
||||
$this->sortMe();
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function sortMe()
|
||||
{
|
||||
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
|
||||
if ($this->resource->settings->is_env_sorting_enabled) {
|
||||
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
|
||||
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
|
||||
} else {
|
||||
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
|
||||
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
|
||||
}
|
||||
}
|
||||
$this->getDevView();
|
||||
$this->sortEnvironmentVariables();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
|
||||
$this->resource->settings->save();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
$this->sortMe();
|
||||
$this->resource->settings->save();
|
||||
$this->sortEnvironmentVariables();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
}
|
||||
|
||||
public function sortEnvironmentVariables()
|
||||
{
|
||||
if ($this->resource->type() === 'application') {
|
||||
$this->resource->load(['environment_variables', 'environment_variables_preview']);
|
||||
} else {
|
||||
$this->resource->load(['environment_variables']);
|
||||
}
|
||||
|
||||
$sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order';
|
||||
|
||||
$sortFunction = function ($variables) use ($sortBy) {
|
||||
if (! $variables) {
|
||||
return $variables;
|
||||
}
|
||||
if ($sortBy === 'key') {
|
||||
return $variables->sortBy(function ($item) {
|
||||
return strtolower($item->key);
|
||||
}, SORT_NATURAL | SORT_FLAG_CASE)->values();
|
||||
} else {
|
||||
return $variables->sortBy('order')->values();
|
||||
}
|
||||
};
|
||||
|
||||
$this->resource->environment_variables = $sortFunction($this->resource->environment_variables);
|
||||
$this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview);
|
||||
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->resource->environment_variables->map(function ($item) {
|
||||
$this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables);
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(locked secret)";
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
if ($item->is_multiline) {
|
||||
return "$item->key=(multiline, edit in normal view)";
|
||||
return "$item->key=(Multiline environment variable, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$item->value";
|
||||
})->join('
|
||||
');
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(locked secret)";
|
||||
}
|
||||
if ($item->is_multiline) {
|
||||
return "$item->key=(multiline, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$item->value";
|
||||
})->join('
|
||||
');
|
||||
}
|
||||
})->join("\n");
|
||||
}
|
||||
|
||||
public function switch()
|
||||
{
|
||||
if ($this->view === 'normal') {
|
||||
$this->view = 'dev';
|
||||
} else {
|
||||
$this->view = 'normal';
|
||||
}
|
||||
$this->sortMe();
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->sortEnvironmentVariables();
|
||||
}
|
||||
|
||||
public function saveVariables($isPreview)
|
||||
public function submit($data = null)
|
||||
{
|
||||
if ($isPreview) {
|
||||
$variables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
|
||||
} else {
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
|
||||
}
|
||||
foreach ($variables as $key => $variable) {
|
||||
if ($isPreview) {
|
||||
$found = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
try {
|
||||
if ($data === null) {
|
||||
$this->handleBulkSubmit();
|
||||
} else {
|
||||
$found = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
$this->handleSingleSubmit($data);
|
||||
}
|
||||
if ($found) {
|
||||
if ($found->is_shown_once || $found->is_multiline) {
|
||||
continue;
|
||||
|
||||
$this->updateOrder();
|
||||
$this->sortEnvironmentVariables();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateOrder()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$order = 1;
|
||||
foreach ($variables as $key => $value) {
|
||||
$env = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
if ($env) {
|
||||
$env->order = $order;
|
||||
$env->save();
|
||||
}
|
||||
$order++;
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$order = 1;
|
||||
foreach ($previewVariables as $key => $value) {
|
||||
$env = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
if ($env) {
|
||||
$env->order = $order;
|
||||
$env->save();
|
||||
}
|
||||
$found->value = $variable;
|
||||
// if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
|
||||
// $type = str($found->value)->after('{{')->before('.')->value;
|
||||
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
|
||||
$order++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
$found->save();
|
||||
private function handleBulkSubmit()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$this->deleteRemovedVariables(false, $variables);
|
||||
$this->updateOrCreateVariables(false, $variables);
|
||||
|
||||
continue;
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->deleteRemovedVariables(true, $previewVariables);
|
||||
$this->updateOrCreateVariables(true, $previewVariables);
|
||||
}
|
||||
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
}
|
||||
|
||||
private function handleSingleSubmit($data)
|
||||
{
|
||||
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Environment variable already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
|
||||
$environment = $this->createEnvironmentVariable($data);
|
||||
$environment->order = $maxOrder + 1;
|
||||
$environment->save();
|
||||
}
|
||||
|
||||
private function createEnvironmentVariable($data)
|
||||
{
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $data['key'];
|
||||
$environment->value = $data['value'];
|
||||
$environment->is_build_time = $data['is_build_time'] ?? false;
|
||||
$environment->is_multiline = $data['is_multiline'] ?? false;
|
||||
$environment->is_literal = $data['is_literal'] ?? false;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
|
||||
$resourceType = $this->resource->type();
|
||||
$resourceIdField = $this->getResourceIdField($resourceType);
|
||||
|
||||
if ($resourceIdField) {
|
||||
$environment->$resourceIdField = $this->resource->id;
|
||||
}
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
private function getResourceIdField($resourceType)
|
||||
{
|
||||
$resourceTypes = [
|
||||
'application' => 'application_id',
|
||||
'standalone-postgresql' => 'standalone_postgresql_id',
|
||||
'standalone-redis' => 'standalone_redis_id',
|
||||
'standalone-mongodb' => 'standalone_mongodb_id',
|
||||
'standalone-mysql' => 'standalone_mysql_id',
|
||||
'standalone-mariadb' => 'standalone_mariadb_id',
|
||||
'standalone-keydb' => 'standalone_keydb_id',
|
||||
'standalone-dragonfly' => 'standalone_dragonfly_id',
|
||||
'standalone-clickhouse' => 'standalone_clickhouse_id',
|
||||
'service' => 'service_id',
|
||||
];
|
||||
|
||||
return $resourceTypes[$resourceType] ?? null;
|
||||
}
|
||||
|
||||
private function deleteRemovedVariables($isPreview, $variables)
|
||||
{
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
|
||||
}
|
||||
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
foreach ($variables as $key => $value) {
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
}
|
||||
} else {
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $variable;
|
||||
// if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
|
||||
// $type = str($environment->value)->after('{{')->before('.')->value;
|
||||
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
|
||||
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
$environment->value = $value;
|
||||
$environment->is_build_time = false;
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = $isPreview ? true : false;
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$environment->application_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$environment->standalone_redis_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$environment->standalone_mongodb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$environment->standalone_mysql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mariadb':
|
||||
$environment->standalone_mariadb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$environment->standalone_keydb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$environment->standalone_dragonfly_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$environment->standalone_clickhouse_id = $this->resource->id;
|
||||
break;
|
||||
case 'service':
|
||||
$environment->service_id = $this->resource->id;
|
||||
break;
|
||||
}
|
||||
$environment->is_preview = $isPreview;
|
||||
|
||||
$this->setEnvironmentResourceId($environment);
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
if ($isPreview) {
|
||||
$this->dispatch('success', 'Preview environment variables updated.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
}
|
||||
|
||||
private function setEnvironmentResourceId($environment)
|
||||
{
|
||||
$resourceTypes = [
|
||||
'application' => 'application_id',
|
||||
'standalone-postgresql' => 'standalone_postgresql_id',
|
||||
'standalone-redis' => 'standalone_redis_id',
|
||||
'standalone-mongodb' => 'standalone_mongodb_id',
|
||||
'standalone-mysql' => 'standalone_mysql_id',
|
||||
'standalone-mariadb' => 'standalone_mariadb_id',
|
||||
'standalone-keydb' => 'standalone_keydb_id',
|
||||
'standalone-dragonfly' => 'standalone_dragonfly_id',
|
||||
'standalone-clickhouse' => 'standalone_clickhouse_id',
|
||||
'service' => 'service_id',
|
||||
];
|
||||
|
||||
$resourceType = $this->resource->type();
|
||||
if (isset($resourceTypes[$resourceType])) {
|
||||
$environment->{$resourceTypes[$resourceType]} = $this->resource->id;
|
||||
}
|
||||
$this->refreshEnvs();
|
||||
}
|
||||
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
$this->sortEnvironmentVariables();
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function submit($data)
|
||||
{
|
||||
try {
|
||||
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Environment variable already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $data['key'];
|
||||
$environment->value = $data['value'];
|
||||
$environment->is_build_time = $data['is_build_time'];
|
||||
$environment->is_multiline = $data['is_multiline'];
|
||||
$environment->is_literal = $data['is_literal'];
|
||||
$environment->is_preview = $data['is_preview'];
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$environment->application_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$environment->standalone_redis_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$environment->standalone_mongodb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$environment->standalone_mysql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mariadb':
|
||||
$environment->standalone_mariadb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$environment->standalone_keydb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$environment->standalone_dragonfly_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$environment->standalone_clickhouse_id = $this->resource->id;
|
||||
break;
|
||||
case 'service':
|
||||
$environment->service_id = $this->resource->id;
|
||||
break;
|
||||
}
|
||||
$environment->save();
|
||||
$this->refreshEnvs();
|
||||
$this->dispatch('success', 'Environment variable added.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ class Show extends Component
|
||||
public string $type;
|
||||
|
||||
protected $listeners = [
|
||||
'refresh' => 'refresh',
|
||||
'refreshEnvs' => 'refresh',
|
||||
'refresh',
|
||||
'compose_loaded' => '$refresh',
|
||||
];
|
||||
|
||||
@@ -129,7 +130,8 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->env->delete();
|
||||
$this->dispatch('refreshEnvs');
|
||||
$this->dispatch('environmentVariableDeleted');
|
||||
$this->dispatch('success', 'Environment variable deleted successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Actions\Server\RunCommand;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
|
||||
public string $container;
|
||||
public $container;
|
||||
|
||||
public Collection $containers;
|
||||
|
||||
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public string $workDir = '';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public Collection $servers;
|
||||
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
'command' => 'required',
|
||||
'workDir' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! auth()->user()->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->containers = collect();
|
||||
$this->servers = collect();
|
||||
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
$this->container = $this->resource->uuid;
|
||||
$this->containers->push($this->container);
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadContainers()
|
||||
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
|
||||
];
|
||||
$this->containers = $this->containers->push($payload);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
if ($this->resource->isRunning()) {
|
||||
$this->containers = $this->containers->push([
|
||||
'server' => $server,
|
||||
'container' => [
|
||||
'Names' => $this->resource->uuid,
|
||||
],
|
||||
]);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
ray($application);
|
||||
if ($application->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if ($database->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->container = data_get($this->containers->first(), 'container.Names');
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
#[On('connectToContainer')]
|
||||
public function connectToContainer()
|
||||
{
|
||||
try {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$container = $this->containers->where('container.Names', $this->container)->first();
|
||||
$container_name = data_get($container, 'container.Names');
|
||||
if (is_null($container)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($container, 'server');
|
||||
} else {
|
||||
$container_name = $this->container;
|
||||
$server = $this->servers->first();
|
||||
$container_name = data_get($this->container, 'container.Names');
|
||||
if (is_null($container_name)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($this->container, 'server');
|
||||
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
|
||||
if (! empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
||||
} else {
|
||||
$exec = "docker exec {$container_name} {$cmd}";
|
||||
}
|
||||
$activity = RunCommand::run(server: $server, command: $exec);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
|
||||
$this->dispatch('send-terminal-command',
|
||||
true,
|
||||
$container_name,
|
||||
$server->uuid,
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
@@ -97,7 +98,7 @@ class GetLogs extends Component
|
||||
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if (! $this->numberOfLines) {
|
||||
if ($this->numberOfLines <= 0) {
|
||||
$this->numberOfLines = 1000;
|
||||
}
|
||||
if ($this->container) {
|
||||
@@ -108,14 +109,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
@@ -124,14 +125,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
|
||||
@@ -26,7 +26,7 @@ class All extends Component
|
||||
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
|
||||
} elseif ($this->resource->type() == 'application') {
|
||||
if ($this->resource->build_pack === 'dockercompose') {
|
||||
$parsed = $this->resource->parseCompose();
|
||||
$parsed = $this->resource->parse();
|
||||
$containers = collect(data_get($parsed, 'services'))->keys();
|
||||
$this->containerNames = $containers;
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,8 @@ class Executions extends Component
|
||||
|
||||
public $selectedKey;
|
||||
|
||||
public $task;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
return [
|
||||
@@ -26,4 +28,47 @@ class Executions extends Component
|
||||
}
|
||||
$this->selectedKey = $key;
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if (! $this->task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->task->application) {
|
||||
if ($this->task->application->destination && $this->task->application->destination->server) {
|
||||
return $this->task->application->destination->server;
|
||||
}
|
||||
} elseif ($this->task->service) {
|
||||
if ($this->task->service->destination && $this->task->service->destination->server) {
|
||||
return $this->task->service->destination->server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
return $serverTimezone;
|
||||
}
|
||||
|
||||
public function formatDateInServerTimezone($date)
|
||||
{
|
||||
$serverTimezone = $this->getServerTimezone();
|
||||
$dateObj = new \DateTime($date);
|
||||
try {
|
||||
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
|
||||
} catch (\Exception $e) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ class Show extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public string $scheduledTaskName;
|
||||
|
||||
protected $rules = [
|
||||
'task.enabled' => 'required|boolean',
|
||||
'task.name' => 'required|string',
|
||||
@@ -49,6 +51,7 @@ class Show extends Component
|
||||
|
||||
$this->modalId = new Cuid2;
|
||||
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
|
||||
$this->scheduledTaskName = $this->task->name;
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -75,9 +78,9 @@ class Show extends Component
|
||||
$this->task->delete();
|
||||
|
||||
if ($this->type == 'application') {
|
||||
return redirect()->route('project.application.configuration', $this->parameters);
|
||||
return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
|
||||
} else {
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Shared\Storages;
|
||||
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -36,8 +38,14 @@ class Show extends Component
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
58
app/Livewire/Project/Shared/Terminal.php
Normal file
58
app/Livewire/Project/Shared/Terminal.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Terminal extends Component
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
|
||||
];
|
||||
}
|
||||
|
||||
public function closeTerminal()
|
||||
{
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
|
||||
#[On('send-terminal-command')]
|
||||
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||
{
|
||||
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||
|
||||
if ($isContainer) {
|
||||
$status = getContainerStatus($server, $identifier);
|
||||
if ($status !== 'running') {
|
||||
return;
|
||||
}
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||
} else {
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||
}
|
||||
|
||||
// ssh command is sent back to frontend then to websocket
|
||||
// this is done because the websocket connection is not available here
|
||||
// a better solution would be to remove websocket on NodeJS and work with something like
|
||||
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
|
||||
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
|
||||
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
|
||||
// 4. Follow-up discussions here:
|
||||
// - https://github.com/coollabsio/coolify/issues/2298
|
||||
// - https://github.com/coollabsio/coolify/discussions/3362
|
||||
$this->dispatch('send-back-command', $command);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.terminal');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user