Error: '.$error);
return;
}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index f593fb78b..c52970258 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -40,6 +40,7 @@ class Index extends Component
'settings.is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'settings.instance_timezone' => 'required|string|timezone',
];
protected $validationAttributes = [
@@ -54,6 +55,8 @@ class Index extends Component
'update_check_frequency' => 'Update Check Frequency',
];
+ public $timezones;
+
public function mount()
{
if (isInstanceAdmin()) {
@@ -65,6 +68,7 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
} else {
return redirect()->route('dashboard');
}
@@ -166,6 +170,13 @@ class Index extends Component
}
}
+ public function updatedSettingsInstanceTimezone($value)
+ {
+ $this->settings->instance_timezone = $value;
+ $this->settings->save();
+ $this->dispatch('success', 'Instance timezone updated.');
+ }
+
public function render()
{
return view('livewire.settings.index');
diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php
index e025d8f7c..daf1df212 100644
--- a/app/Livewire/SharedVariables/Environment/Show.php
+++ b/app/Livewire/SharedVariables/Environment/Show.php
@@ -16,7 +16,7 @@ class Show extends Component
public array $parameters;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey'];
public function saveKey($data)
{
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 97d4fcdbf..3026cb297 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
@@ -73,8 +75,13 @@ class AdminView extends Component
$team->delete();
}
- public function delete($id)
+ public function delete($id, $password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}
diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php
new file mode 100644
index 000000000..945b25714
--- /dev/null
+++ b/app/Livewire/Terminal/Index.php
@@ -0,0 +1,76 @@
+user()->isAdmin()) {
+ abort(403);
+ }
+ $this->servers = Server::isReachable()->get();
+ $this->containers = $this->getAllActiveContainers();
+ }
+
+ private function getAllActiveContainers()
+ {
+ return collect($this->servers)->flatMap(function ($server) {
+ if (! $server->isFunctional()) {
+ return [];
+ }
+
+ return $server->loadAllContainers()->map(function ($container) use ($server) {
+ $state = data_get_str($container, 'State')->lower();
+ if ($state->contains('running')) {
+ return [
+ 'name' => data_get($container, 'Names'),
+ 'connection_name' => data_get($container, 'Names'),
+ 'uuid' => data_get($container, 'Names'),
+ 'status' => data_get_str($container, 'State')->lower(),
+ 'server' => $server,
+ 'server_uuid' => $server->uuid,
+ ];
+ }
+
+ return null;
+ })->filter();
+ });
+ }
+
+ public function updatedSelectedUuid()
+ {
+ $this->connectToContainer();
+ }
+
+ #[On('connectToContainer')]
+ public function connectToContainer()
+ {
+ if ($this->selected_uuid === 'default') {
+ $this->dispatch('error', 'Please select a server or a container.');
+
+ return;
+ }
+ $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
+ $this->dispatch('send-terminal-command',
+ isset($container),
+ $container['connection_name'] ?? $this->selected_uuid,
+ $container['server_uuid'] ?? $this->selected_uuid
+ );
+ }
+
+ public function render()
+ {
+ return view('livewire.terminal.index');
+ }
+}
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index da7b5860d..dfbd945f5 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -4,7 +4,6 @@ namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Upgrade extends Component
@@ -22,13 +21,8 @@ class Upgrade extends Component
public function checkUpdate()
{
try {
- $settings = InstanceSettings::get();
- $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
- if ($response->successful()) {
- $versions = $response->json();
- $this->latestVersion = data_get($versions, 'coolify.v4.version');
- }
- $this->isUpgradeAvailable = $settings->new_version_available;
+ $this->latestVersion = get_latest_version_of_coolify();
+ $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Models/Application.php b/app/Models/Application.php
index e2871da4b..55006745a 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -102,6 +104,8 @@ class Application extends BaseModel
{
use SoftDeletes;
+ private static $parserVersion = '3';
+
protected $guarded = [];
protected $appends = ['server_status'];
@@ -125,7 +129,7 @@ class Application extends BaseModel
ApplicationSetting::create([
'application_id' => $application->id,
]);
- $application->compose_parsing_version = '2';
+ $application->compose_parsing_version = self::$parserVersion;
$application->save();
});
static::forceDeleting(function ($application) {
@@ -138,6 +142,7 @@ class Application extends BaseModel
$task->delete();
}
$application->tags()->detach();
+ $application->previews()->delete();
});
}
@@ -146,12 +151,64 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public function getContainersToStop(bool $previewDeployments = false): array
+ {
+ $containers = $previewDeployments
+ ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
+ : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
+
+ return $containers->pluck('Names')->toArray();
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 600)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach ($finishedProcesses as $containerName => $process) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- ray('Deleting workdir');
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
@@ -173,6 +230,13 @@ class Application extends BaseModel
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -412,23 +476,6 @@ class Application extends BaseModel
);
}
- public function dockerComposePrLocation(): Attribute
- {
- return Attribute::make(
- set: function ($value) {
- if (is_null($value) || $value === '') {
- return '/docker-compose.yaml';
- } else {
- if ($value !== '/') {
- return Str::start(Str::replaceEnd('/', '', $value), '/');
- }
-
- return Str::start($value, '/');
- }
- }
- );
- }
-
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -479,12 +526,12 @@ class Application extends BaseModel
$main_server_status = $this->destination->server->isFunctional();
foreach ($additional_servers_status as $status) {
$server_status = str($status)->before(':')->value();
- if ($main_server_status !== $server_status) {
+ if ($server_status !== 'running') {
return false;
}
}
- return true;
+ return $main_server_status;
}
}
);
@@ -1040,7 +1087,7 @@ class Application extends BaseModel
}
}
- public function parseRawCompose()
+ public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1048,6 +1095,7 @@ class Application extends BaseModel
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
+
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -1100,9 +1148,11 @@ class Application extends BaseModel
instant_remote_process($commands, $this->destination->server, false);
}
- public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null)
+ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($this->docker_compose_raw) {
+ if ($this->compose_parsing_version === '3') {
+ return newParser($this, $pull_request_id, $preview_id);
+ } elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
} else {
return collect([]);
@@ -1154,7 +1204,7 @@ class Application extends BaseModel
if ($composeFileContent) {
$this->docker_compose_raw = $composeFileContent;
$this->save();
- $parsedServices = $this->parseCompose();
+ $parsedServices = $this->parse();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
@@ -1178,7 +1228,6 @@ class Application extends BaseModel
} else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
-
}
public function parseContainerLabels(?ApplicationPreview $preview = null)
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index 57d20e3aa..04a0ab27e 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel
protected static function booted()
{
static::deleting(function ($preview) {
- if ($preview->application->build_pack === 'dockercompose') {
+ if (data_get($preview, 'application.build_pack') === 'dockercompose') {
$server = $preview->application->destination->server;
- $composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
+ $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 5e1d8ae13..138775aba 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -6,7 +6,6 @@ use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
-use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -97,8 +96,22 @@ class EnvironmentVariable extends Model
$resource = Application::find($this->application_id);
} elseif ($this->service_id) {
$resource = Service::find($this->service_id);
- } elseif ($this->database_id) {
- $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
+ } elseif ($this->standalone_postgresql_id) {
+ $resource = StandalonePostgresql::find($this->standalone_postgresql_id);
+ } elseif ($this->standalone_redis_id) {
+ $resource = StandaloneRedis::find($this->standalone_redis_id);
+ } elseif ($this->standalone_mongodb_id) {
+ $resource = StandaloneMongodb::find($this->standalone_mongodb_id);
+ } elseif ($this->standalone_mysql_id) {
+ $resource = StandaloneMysql::find($this->standalone_mysql_id);
+ } elseif ($this->standalone_mariadb_id) {
+ $resource = StandaloneMariadb::find($this->standalone_mariadb_id);
+ } elseif ($this->standalone_keydb_id) {
+ $resource = StandaloneKeydb::find($this->standalone_keydb_id);
+ } elseif ($this->standalone_dragonfly_id) {
+ $resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
+ } elseif ($this->standalone_clickhouse_id) {
+ $resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
}
return $resource;
@@ -122,63 +135,6 @@ class EnvironmentVariable extends Model
);
}
- protected function isFoundInCompose(): Attribute
- {
- return Attribute::make(
- get: function () {
- if (! $this->application_id) {
- return true;
- }
- $found_in_compose = false;
- $found_in_args = false;
- $resource = $this->resource();
- $compose = data_get($resource, 'docker_compose_raw');
- if (! $compose) {
- return true;
- }
- $yaml = Yaml::parse($compose);
- $services = collect(data_get($yaml, 'services'));
- if ($services->isEmpty()) {
- return false;
- }
- foreach ($services as $service) {
- $environments = collect(data_get($service, 'environment'));
- $args = collect(data_get($service, 'build.args'));
- if ($environments->isEmpty() && $args->isEmpty()) {
- $found_in_compose = false;
- break;
- }
-
- $found_in_compose = $environments->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_compose) {
- break;
- }
-
- $found_in_args = $args->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_args) {
- break;
- }
- }
-
- return $found_in_compose || $found_in_args;
- }
- );
- }
-
protected function isShared(): Attribute
{
return Attribute::make(
@@ -201,8 +157,10 @@ class EnvironmentVariable extends Model
$environment_variable = trim($environment_variable);
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
if ($sharedEnvsFound->isEmpty()) {
+
return $environment_variable;
}
+
foreach ($sharedEnvsFound as $sharedEnv) {
$type = str($sharedEnv)->match('/(.*?)\./');
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 5bd421956..27a181ee4 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -37,6 +37,30 @@ class InstanceSettings extends Model implements SendsEmail
);
}
+ public function updateCheckFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
+ public function autoUpdateFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
public static function get()
{
return InstanceSettings::findOrFail(0);
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index a436f5797..d528099ff 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -24,8 +24,9 @@ class LocalFileVolume extends BaseModel
return $this->morphTo('resource');
}
- public function deleteStorageOnServer()
+ public function loadStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -35,17 +36,46 @@ class LocalFileVolume extends BaseModel
$server = $this->resource->destination->server;
}
$commands = collect([]);
- $fs_path = data_get($this, 'fs_path');
- $isFile = instant_remote_process(["test -f $fs_path && echo OK || echo NOK"], $server);
- $isDir = instant_remote_process(["test -d $fs_path && echo OK || echo NOK"], $server);
- if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
- ray($isFile, $isDir);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ if ($isFile === 'OK') {
+ $content = instant_remote_process(["cat $path"], $server, false);
+ $this->content = $content;
+ $this->is_directory = false;
+ $this->save();
+ }
+ }
+
+ public function deleteStorageOnServer()
+ {
+ $this->load(['service']);
+ $isService = data_get($this->resource, 'service');
+ if ($isService) {
+ $workdir = $this->resource->service->workdir();
+ $server = $this->resource->service->server;
+ } else {
+ $workdir = $this->resource->workdir();
+ $server = $this->resource->destination->server;
+ }
+ $commands = collect([]);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
+ if ($path && $path != '/' && $path != '.' && $path != '..') {
if ($isFile === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
} elseif ($isDir === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
- $commands->push("rmdir $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
+ $commands->push("rmdir $path > /dev/null 2>&1 || true");
}
}
if ($commands->count() > 0) {
@@ -55,6 +85,7 @@ class LocalFileVolume extends BaseModel
public function saveStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -74,30 +105,36 @@ class LocalFileVolume extends BaseModel
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
}
}
- $fileVolume = $this;
- $path = str(data_get($fileVolume, 'fs_path'));
- $content = data_get($fileVolume, 'content');
+ $path = data_get_str($this, 'fs_path');
+ $content = data_get($this, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$path = $workdir.$path;
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
- if ($isFile == 'OK' && $fileVolume->is_directory) {
+ if ($isFile == 'OK' && $this->is_directory) {
$content = instant_remote_process(["cat $path"], $server, false);
- $fileVolume->is_directory = false;
- $fileVolume->content = $content;
- $fileVolume->save();
+ $this->is_directory = false;
+ $this->content = $content;
+ $this->save();
FileStorageChanged::dispatch(data_get($server, 'team_id'));
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
- } elseif ($isDir == 'OK' && ! $fileVolume->is_directory) {
- $fileVolume->is_directory = true;
- $fileVolume->save();
- throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.
Please delete the directory on the server or mark it as directory.');
+ } elseif ($isDir == 'OK' && ! $this->is_directory) {
+ if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) {
+ $this->is_directory = true;
+ $this->save();
+ throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.
https://github.com/coollabsio/coolify-examples main branch will be selected https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch will be selected. https://gitea.com/sedlav/expressjs.git main branch will be selected. https://gitlab.com/andrasbacsai/nodejs-example.git main branch will be selected."
+ "repository.url": "Examples For Public repositories, use https://.... For Private repositories, use git@....
- This will reset the container labels. Are you sure?
@endif
@@ -254,6 +256,11 @@
helper="You need to modify the docker compose file." monacoEditorLanguage="yaml"
useMonacoEditor />
@else
+ @if ($application->compose_parsing_version === '3')
+
+ @endif
@@ -297,12 +304,15 @@
helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically.
Be careful, it could break the proxy configuration after you restart the container."
id="application.settings.is_container_label_readonly_enabled" instantSave>
-
- Are you sure you want to reset the labels to Coolify generated labels? It could break the proxy
- configuration after you restart the container.
-
-
+
@endif