diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
new file mode 100644
index 000000000..e8d73ef47
--- /dev/null
+++ b/app/Console/Commands/OpenApi.php
@@ -0,0 +1,26 @@
+errorOutput();
+ $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
+ $error = preg_replace('/^\h*\v+/m', '', $error);
+ echo $error;
+ echo $process->output();
+
+ }
+}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index cd4d724b4..aee9433e3 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -201,7 +201,7 @@ class ApplicationsController extends Controller
#[OA\Post(
summary: 'Create (Private - GH App)',
description: 'Create new application based on a private repository through a Github App.',
- path: '/applications/private-gh-app',
+ path: '/applications/private-github-app',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/EnvironmentVariablesController.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php
deleted file mode 100644
index d127d0525..000000000
--- a/app/Http/Controllers/Api/EnvironmentVariablesController.php
+++ /dev/null
@@ -1,35 +0,0 @@
-env_uuid)->first();
- if (! $env) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
- if (! $found_app) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $env->delete();
-
- return response()->json([
- 'message' => 'Environment variable deleted.',
- ]);
- }
-}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 473cbc679..437bd57f5 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -157,7 +157,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
- private bool $preserveRepository = true;
+ private bool $preserveRepository = false;
public $tries = 1;
@@ -480,6 +480,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
// Start compose file
+ $server_workdir = $this->application->workdir();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
@@ -488,7 +489,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$this->write_deployment_configurations();
- $server_workdir = $this->application->workdir();
$this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
@@ -508,15 +508,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$command = "{$this->coolify_variables} docker compose";
- if ($this->env_filename) {
- $command .= " --env-file {$this->workdir}/{$this->env_filename}";
- }
- $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ if ($this->preserveRepository) {
+ if ($this->env_filename) {
+ $command .= " --env-file {$server_workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
+ $this->write_deployment_configurations();
+
+ $this->execute_remote_command(
+ ['command' => $command, 'hidden' => true],
+ );
+ } else {
+ if ($this->env_filename) {
+ $command .= " --env-file {$this->workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
+ );
+ }
- $this->write_deployment_configurations();
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
- );
}
}
@@ -619,6 +630,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
);
}
+ $this->application->fileStorages()->each(function ($fileStorage) {
+ if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
+ $fileStorage->saveStorageOnServer();
+ }
+ });
if ($this->use_build_server) {
$this->server = $this->build_server;
}
@@ -1708,13 +1724,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
}
+ if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
+ $docker_compose['services'][$this->container_name]['volumes'] = [];
+ }
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages);
}
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
- })->toArray();
+ })->toArray());
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 79b00e9cd..c8adf75ca 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -336,7 +336,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
@@ -351,13 +351,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
$commands[] = 'mkdir -p '.$this->backup_dir;
if ($collectionsToExclude->count() === 0) {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
}
} else {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 77593bf0a..cf5f0979a 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -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;
@@ -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,6 +193,11 @@ 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.');
@@ -186,32 +205,6 @@ class General extends Component
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->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 633ce5bda..199a20cf6 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -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([
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 2d9c95daa..438383a8d 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -33,6 +33,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 +46,7 @@ class FileStorage extends Component
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
+ $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -68,6 +70,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) {
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index e63871602..342259fd1 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -24,7 +24,8 @@ class Show extends Component
public string $type;
protected $listeners = [
- 'refresh' => 'refresh',
+ 'refreshEnvs' => 'refresh',
+ 'refresh',
'compose_loaded' => '$refresh',
];
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/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/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index a436f5797..0fe48c5c4 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -24,6 +24,32 @@ class LocalFileVolume extends BaseModel
return $this->morphTo('resource');
}
+ public function loadStorageOnServer()
+ {
+ $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);
+ if ($isFile === 'OK') {
+ $content = instant_remote_process(["cat $path"], $server, false);
+ $this->content = $content;
+ $this->is_directory = false;
+ $this->save();
+ }
+ }
+
public function deleteStorageOnServer()
{
$isService = data_get($this->resource, 'service');
@@ -35,17 +61,20 @@ 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);
+ $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 +84,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 +104,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.
Please delete the directory on the server or mark it as directory.');
+ }
+ instant_remote_process([
+ "rm -fr $path",
+ "touch $path",
+ ], $server, false);
+ FileStorageChanged::dispatch(data_get($server, 'team_id'));
}
- if ($isDir == 'NOK' && ! $fileVolume->is_directory) {
- $chmod = data_get($fileVolume, 'chmod');
- $chown = data_get($fileVolume, 'chown');
+ if ($isDir == 'NOK' && ! $this->is_directory) {
+ $chmod = data_get($this, 'chmod');
+ $chown = data_get($this, 'chown');
if ($content) {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
@@ -111,7 +147,7 @@ class LocalFileVolume extends BaseModel
if ($chmod) {
$commands->push("chmod $chmod $path");
}
- } elseif ($isDir == 'NOK' && $fileVolume->is_directory) {
+ } elseif ($isDir == 'NOK' && $this->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 33238281e..6c9068334 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -23,6 +23,7 @@ use Symfony\Component\Yaml\Yaml;
'description' => ['type' => 'string', 'description' => 'The description of the service.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'],
'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'],
+ 'destination_type' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'],
@@ -205,6 +206,41 @@ class Service extends BaseModel
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
+ case str($image)?->contains('rabbitmq'):
+ $data = collect([]);
+ $host_port = $this->environment_variables()->where('key', 'PORT')->first();
+ $username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_RABBITMQ')->first();
+ if ($host_port) {
+ $data = $data->merge([
+ 'Host Port Binding' => [
+ 'key' => data_get($host_port, 'key'),
+ 'value' => data_get($host_port, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('RabbitMQ', $data->toArray());
+ break;
case str($image)?->contains('tolgee'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
@@ -504,6 +540,9 @@ class Service extends BaseModel
default:
$data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
+ // Chaskiq
+ $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
+
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
if ($admin_user) {
$data = $data->merge([
@@ -525,6 +564,15 @@ class Service extends BaseModel
],
]);
}
+ if ($admin_email) {
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => 'ADMIN_EMAIL',
+ 'value' => data_get($admin_email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
$fields->put('Admin', $data->toArray());
break;
case str($image)?->contains('vaultwarden'):
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index aae4fafd4..57cce2642 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -794,7 +794,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
$topLevelVolumes = collect($tempTopLevelVolumes);
}
- $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices) {
+ $services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices, $topLevelVolumes) {
// Workarounds for beta users.
if ($serviceName === 'registry') {
$tempServiceName = 'docker-registry';
@@ -963,102 +963,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
- $serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
- $type = null;
- $source = null;
- $target = null;
- $content = null;
- $isDirectory = false;
- if (is_string($volume)) {
- $source = str($volume)->before(':');
- $target = str($volume)->after(':')->beforeLast(':');
- if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
- $type = str('bind');
- // By default, we cannot determine if the bind is a directory or not, so we set it to directory
- $isDirectory = true;
- } else {
- $type = str('volume');
- }
- } elseif (is_array($volume)) {
- $type = data_get_str($volume, 'type');
- $source = data_get_str($volume, 'source');
- $target = data_get_str($volume, 'target');
- $content = data_get($volume, 'content');
- $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
- $foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
- if ($foundConfig) {
- $contentNotNull = data_get($foundConfig, 'content');
- if ($contentNotNull) {
- $content = $contentNotNull;
- }
- $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
- }
- if (is_null($isDirectory) && is_null($content)) {
- // if isDirectory is not set & content is also not set, we assume it is a directory
- ray('setting isDirectory to true');
- $isDirectory = true;
- }
- }
- if ($type?->value() === 'bind') {
- if ($source->value() === '/var/run/docker.sock') {
- return $volume;
- }
- if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
- return $volume;
- }
- LocalFileVolume::updateOrCreate(
- [
- 'mount_path' => $target,
- 'resource_id' => $savedService->id,
- 'resource_type' => get_class($savedService),
- ],
- [
- 'fs_path' => $source,
- 'mount_path' => $target,
- 'content' => $content,
- 'is_directory' => $isDirectory,
- 'resource_id' => $savedService->id,
- 'resource_type' => get_class($savedService),
- ]
- );
- } elseif ($type->value() === 'volume') {
- if ($topLevelVolumes->has($source->value())) {
- $v = $topLevelVolumes->get($source->value());
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- return $volume;
- }
- }
- $slugWithoutUuid = Str::slug($source, '-');
- $name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
- if (is_string($volume)) {
- $source = str($volume)->before(':');
- $target = str($volume)->after(':')->beforeLast(':');
- $source = $name;
- $volume = "$source:$target";
- } elseif (is_array($volume)) {
- data_set($volume, 'source', $name);
- }
- $topLevelVolumes->put($name, [
- 'name' => $name,
- ]);
- LocalPersistentVolume::updateOrCreate(
- [
- 'mount_path' => $target,
- 'resource_id' => $savedService->id,
- 'resource_type' => get_class($savedService),
- ],
- [
- 'name' => $name,
- 'mount_path' => $target,
- 'resource_id' => $savedService->id,
- 'resource_type' => get_class($savedService),
- ]
- );
- }
- dispatch(new ServerFilesFromServerJob($savedService));
-
- return $volume;
- });
+ ['serviceVolumes' => $serviceVolumes, 'topLevelVolumes' => $topLevelVolumes] = parseServiceVolumes($serviceVolumes, $savedService, $topLevelVolumes);
data_set($service, 'volumes', $serviceVolumes->toArray());
}
@@ -1645,131 +1550,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} elseif ($resource->compose_parsing_version === '2') {
if (count($serviceVolumes) > 0) {
- $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
- if (is_string($volume)) {
- $volume = str($volume);
- if ($volume->contains(':') && ! $volume->startsWith('/')) {
- $name = $volume->before(':');
- $mount = $volume->after(':');
- if ($name->startsWith('.') || $name->startsWith('~')) {
- $dir = base_configuration_dir().'/applications/'.$resource->uuid;
- if ($name->startsWith('.')) {
- $name = $name->replaceFirst('.', $dir);
- }
- if ($name->startsWith('~')) {
- $name = $name->replaceFirst('~', $dir);
- }
- if ($pull_request_id !== 0) {
- $name = $name."-pr-$pull_request_id";
- }
- $volume = str("$name:$mount");
- } else {
- if ($pull_request_id !== 0) {
- $uuid = $resource->uuid;
- $name = $uuid."-$name-pr-$pull_request_id";
- $volume = str("$name:$mount");
- if ($topLevelVolumes->has($name)) {
- $v = $topLevelVolumes->get($name);
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($v, 'name', $name);
- data_set($topLevelVolumes, $name, $v);
- }
- }
- } else {
- $topLevelVolumes->put($name, [
- 'name' => $name,
- ]);
- }
- } else {
- $uuid = $resource->uuid;
- $name = str($uuid."-$name");
- $volume = str("$name:$mount");
- if ($topLevelVolumes->has($name->value())) {
- $v = $topLevelVolumes->get($name->value());
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($topLevelVolumes, $name->value(), $v);
- }
- }
- } else {
- $topLevelVolumes->put($name->value(), [
- 'name' => $name->value(),
- ]);
- }
- }
- }
- } else {
- if ($volume->startsWith('/')) {
- $name = $volume->before(':');
- $mount = $volume->after(':');
- if ($pull_request_id !== 0) {
- $name = $name."-pr-$pull_request_id";
- }
- $volume = str("$name:$mount");
- }
- }
- } elseif (is_array($volume)) {
- $source = data_get($volume, 'source');
- $target = data_get($volume, 'target');
- $read_only = data_get($volume, 'read_only');
- if ($source && $target) {
- $uuid = $resource->uuid;
- if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) {
- $dir = base_configuration_dir().'/applications/'.$resource->uuid;
- if (str($source, '.')) {
- $source = str($source)->replaceFirst('.', $dir);
- }
- if (str($source, '~')) {
- $source = str($source)->replaceFirst('~', $dir);
- }
- if ($read_only) {
- data_set($volume, 'source', $source.':'.$target.':ro');
- } else {
- data_set($volume, 'source', $source.':'.$target);
- }
- } else {
- if ($pull_request_id === 0) {
- $source = $uuid."-$source";
- } else {
- $source = $uuid."-$source-pr-$pull_request_id";
- }
- if ($read_only) {
- data_set($volume, 'source', $source.':'.$target.':ro');
- } else {
- data_set($volume, 'source', $source.':'.$target);
- }
- if (! str($source)->startsWith('/')) {
- if ($topLevelVolumes->has($source)) {
- $v = $topLevelVolumes->get($source);
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($v, 'name', $source);
- data_set($topLevelVolumes, $source, $v);
- }
- }
- } else {
- $topLevelVolumes->put($source, [
- 'name' => $source,
- ]);
- }
- }
- }
- }
- }
- if (is_array($volume)) {
- return data_get($volume, 'source');
- }
- dispatch(new ServerFilesFromServerJob($resource));
-
- return $volume->value();
- });
+ ['serviceVolumes' => $serviceVolumes, 'topLevelVolumes' => $topLevelVolumes] = parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull_request_id);
data_set($service, 'volumes', $serviceVolumes->toArray());
}
}
@@ -2662,3 +2443,127 @@ function customApiValidator(Collection|array $item, array $rules)
'required' => 'This field is required.',
]);
}
+
+function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull_request_id = 0)
+{
+ $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
+ $type = null;
+ $source = null;
+ $target = null;
+ $content = null;
+ $isDirectory = false;
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
+ $type = str('bind');
+ // By default, we cannot determine if the bind is a directory or not, so we set it to directory
+ $isDirectory = true;
+ } else {
+ $type = str('volume');
+ }
+ } elseif (is_array($volume)) {
+ $type = data_get_str($volume, 'type');
+ $source = data_get_str($volume, 'source');
+ $target = data_get_str($volume, 'target');
+ $content = data_get($volume, 'content');
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+ $foundConfig = $resource->fileStorages()->whereMountPath($target)->first();
+ if ($foundConfig) {
+ $contentNotNull = data_get($foundConfig, 'content');
+ if ($contentNotNull) {
+ $content = $contentNotNull;
+ }
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+ }
+ if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
+ // if isDirectory is not set (or false) & content is also not set, we assume it is a directory
+ ray('setting isDirectory to true');
+ $isDirectory = true;
+ }
+ }
+ if ($type?->value() === 'bind') {
+ if ($source->value() === '/var/run/docker.sock') {
+ return $volume;
+ }
+ if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
+ return $volume;
+ }
+ if (get_class($resource) === "App\Models\Application") {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ } else {
+ $dir = base_configuration_dir().'/services/'.$resource->service->uuid;
+ }
+
+ if ($source->startsWith('.')) {
+ $source = $source->replaceFirst('.', $dir);
+ }
+ if ($source->startsWith('~')) {
+ $source = $source->replaceFirst('~', $dir);
+ }
+ if ($pull_request_id !== 0) {
+ $source = $source."-pr-$pull_request_id";
+ }
+ LocalFileVolume::updateOrCreate(
+ [
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ],
+ [
+ 'fs_path' => $source,
+ 'mount_path' => $target,
+ 'content' => $content,
+ 'is_directory' => $isDirectory,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ]
+ );
+ } elseif ($type->value() === 'volume') {
+ if ($topLevelVolumes->has($source->value())) {
+ $v = $topLevelVolumes->get($source->value());
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ return $volume;
+ }
+ }
+ $slugWithoutUuid = Str::slug($source, '-');
+ if (get_class($resource) === "App\Models\Application") {
+ $name = "{$resource->uuid}_{$slugWithoutUuid}";
+ } else {
+ $name = "{$resource->service->uuid}_{$slugWithoutUuid}";
+ }
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ $source = $name;
+ $volume = "$source:$target";
+ } elseif (is_array($volume)) {
+ data_set($volume, 'source', $name);
+ }
+ $topLevelVolumes->put($name, [
+ 'name' => $name,
+ ]);
+ LocalPersistentVolume::updateOrCreate(
+ [
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ],
+ [
+ 'name' => $name,
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ]
+ );
+ }
+ dispatch(new ServerFilesFromServerJob($resource));
+
+ return $volume;
+ });
+
+ return [
+ 'serviceVolumes' => $serviceVolumes,
+ 'topLevelVolumes' => $topLevelVolumes,
+ ];
+}
diff --git a/config/sentry.php b/config/sentry.php
index 7b48b96b0..657377edf 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.323',
+ 'release' => '4.0.0-beta.324',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/version.php b/config/version.php
index 73f3e1263..7af787ece 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
boolean('is_based_on_git')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('local_file_volumes', function (Blueprint $table) {
+ $table->dropColumn('is_based_on_git');
+ });
+ }
+};
diff --git a/openapi.yaml b/openapi.yaml
index 00d7bff43..9f6bc3889 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -247,13 +247,13 @@ paths:
security:
-
bearerAuth: []
- /applications/private-gh-app:
+ /applications/private-github-app:
post:
tags:
- Applications
summary: 'Create (Private - GH App)'
description: 'Create new application based on a private repository through a Github App.'
- operationId: 4d46c84bda4f1a411f6dda15fce4061f
+ operationId: 8b7af9c9a509385963bf3e72eeeea786
requestBody:
description: 'Application object that needs to be created.'
required: true
diff --git a/public/svgs/budibase.svg b/public/svgs/budibase.svg
new file mode 100644
index 000000000..c77636bfe
--- /dev/null
+++ b/public/svgs/budibase.svg
@@ -0,0 +1,21 @@
+
diff --git a/public/svgs/chaskiq.png b/public/svgs/chaskiq.png
new file mode 100644
index 000000000..8f9f142ab
Binary files /dev/null and b/public/svgs/chaskiq.png differ
diff --git a/public/svgs/rabbitmq.svg b/public/svgs/rabbitmq.svg
new file mode 100644
index 000000000..7a94d71bb
--- /dev/null
+++ b/public/svgs/rabbitmq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/windmill.svg b/public/svgs/windmill.svg
new file mode 100644
index 000000000..2b06716f9
--- /dev/null
+++ b/public/svgs/windmill.svg
@@ -0,0 +1,29 @@
+
+
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php
index 96590cada..78cf70429 100644
--- a/resources/views/livewire/project/service/file-storage.blade.php
+++ b/resources/views/livewire/project/service/file-storage.blade.php
@@ -10,6 +10,7 @@
@endif