Merge branch 'coollabsio:main' into main

This commit is contained in:
Jakubko
2024-10-13 22:02:15 +02:00
committed by GitHub
205 changed files with 7535 additions and 1666 deletions

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class GenerateConfig
{
use AsAction;
public function handle(Application $application, bool $is_json = false)
{
ray()->clearAll();
return $application->generateConfig(is_json: $is_json);
}
}

View File

@@ -22,7 +22,7 @@ class CheckConfiguration
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
}
if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration');

View File

@@ -2,14 +2,17 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class CheckProxy
{
use AsAction;
public function handle(Server $server, $fromUI = false)
// It should return if the proxy should be started (true) or not (false)
public function handle(Server $server, $fromUI = false): bool
{
if (! $server->isFunctional()) {
return false;
@@ -62,22 +65,42 @@ class CheckProxy
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
$portsToCheck = ['80', '443'];
try {
if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server);
if (isset($proxyCompose)) {
$yaml = Yaml::parse($proxyCompose);
$portsToCheck = [];
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$ports = data_get($yaml, 'services.traefik.ports');
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
$ports = data_get($yaml, 'services.caddy.ports');
}
if (isset($ports)) {
foreach ($ports as $port) {
$portsToCheck[] = str($port)->before(':')->value();
}
}
}
} else {
return false;
$portsToCheck = [];
}
} catch (\Exception $e) {
ray($e->getMessage());
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
if (count($portsToCheck) === 0) {
return false;
}
foreach ($portsToCheck as $port) {
$connection = @fsockopen($ip, $port);
if (is_resource($connection) && fclose($connection)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
}

View File

@@ -26,7 +26,7 @@ class StartProxy
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save();
if ($server->isSwarm()) {
$commands = $commands->merge([

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CheckApplicationDeploymentQueue extends Command
{
protected $signature = 'check:deployment-queue {--force} {--seconds=3600}';
protected $description = 'Check application deployment queue.';
public function handle()
{
$seconds = $this->option('seconds');
$deployments = ApplicationDeploymentQueue::whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS,
ApplicationDeploymentStatus::QUEUED,
])->where('created_at', '<=', now()->subSeconds($seconds))->get();
if ($deployments->isEmpty()) {
$this->info('No deployments found in the last '.$seconds.' seconds.');
return;
}
$this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
foreach ($deployments as $deployment) {
if ($this->option('force')) {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
$this->cancelDeployment($deployment);
} else {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
if ($this->confirm('Do you want to cancel this deployment?', true)) {
$this->cancelDeployment($deployment);
}
}
}
}
private function cancelDeployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
if ($deployment->server?->isFunctional()) {
remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
}
}
}

View File

@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command
{
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
protected $signature = 'cleanup:deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue';
protected $description = 'Cleanup application deployment queue.';
public function handle()
{

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
@@ -47,6 +48,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
try {
$applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
if (is_null($applicationDeploymentQueue->application)) {
echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
$applicationDeploymentQueue->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
}
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {

View File

@@ -39,8 +39,8 @@ class ServicesGenerate extends Command
$serviceTemplatesJson[$name] = $parsed;
}
}
$serviceTemplatesJson = json_encode($serviceTemplatesJson);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
}
private function process_file($file)
@@ -78,7 +78,7 @@ class ServicesGenerate extends Command
if ($logo->count() > 0) {
$logo = str($logo[0])->after('# logo:')->trim()->value();
} else {
$logo = 'svgs/unknown.svg';
$logo = 'svgs/coolify.png';
}
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
if ($minversion->count() > 0) {

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Enums;
enum StaticImageTypes: string
{
case NGINX_ALPINE = 'nginx:alpine';
}

View File

@@ -94,7 +94,9 @@ class SshMultiplexingHelper
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
@@ -136,8 +138,8 @@ class SshMultiplexingHelper
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL

View File

@@ -132,6 +132,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -236,6 +237,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -339,6 +341,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -633,7 +636,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server'];
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -672,6 +675,7 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -700,8 +704,7 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -709,19 +712,21 @@ class ApplicationsController extends Controller
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
@@ -744,9 +749,15 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
$application->save();
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
@@ -780,8 +791,7 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -790,10 +800,10 @@ class ApplicationsController extends Controller
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -842,8 +852,10 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -878,8 +890,8 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -888,10 +900,10 @@ class ApplicationsController extends Controller
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
@@ -936,8 +948,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -969,10 +983,13 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'dockerfile' => 'string|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1017,8 +1034,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1049,12 +1068,14 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1077,8 +1098,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1125,10 +1148,12 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'service'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'docker_compose_raw' => 'string|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1478,8 +1503,7 @@ class ApplicationsController extends Controller
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'name' => 'string|max:255',
'description' => 'string|nullable',
'static_image' => 'string',
@@ -1489,7 +1513,9 @@ class ApplicationsController extends Controller
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
// Validate ports_exposes
if ($request->has('ports_exposes')) {
@@ -1555,8 +1581,11 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy;
$use_build_server = $request->use_build_server;
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save();
if (isset($use_build_server)) {
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save();
}
removeUnnecessaryFieldsFromRequest($request);

View File

@@ -2,7 +2,6 @@
namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
@@ -24,7 +23,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -63,31 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
}
public function handle(): void
{
try {
// Check if team is exists
if (is_null($this->team)) {
StopDatabase::run($this->database);
$this->database->delete();
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
if (is_null($this->server)) {
throw new \Exception('Server not found?!');
}
if (is_null($this->database)) {
throw new \Exception('Database not found?!');
}
BackupCreated::dispatch($this->team->id);
@@ -237,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db';
@@ -250,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try {
if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -278,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -287,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -325,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
BackupCreated::dispatch($this->team->id);
if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
}
}
@@ -384,7 +393,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
}
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
} else {
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
}
$commands[] = $backupCommand;
ray($commands);
@@ -405,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
ray($commands);
if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
}
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -424,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
}
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
@@ -466,34 +486,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void
{
try {
@@ -515,10 +507,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
if ($this->s3->isHetzner()) {
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
} else {
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
}
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());

View File

@@ -30,7 +30,7 @@ class Dashboard extends Component
public function cleanup_queue()
{
Artisan::queue('cleanup:application-deployment-queue', [
Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id,
]);
}

View File

@@ -61,6 +61,7 @@ class Help extends Component
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
}
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
$this->reset('description', 'subject');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,9 +2,11 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component
@@ -182,9 +184,7 @@ class General extends Component
$storage->save();
});
}
}
}
public function loadComposeFile($isInit = false)
@@ -241,16 +241,6 @@ class General extends Component
}
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->resetDefaultLabels();
}
public function updatedApplicationBuildPack()
{
@@ -287,18 +277,22 @@ class General extends Component
public function resetDefaultLabels()
{
if ($this->application->settings->is_container_label_readonly_enabled) {
return;
try {
if ($this->application->settings->is_container_label_readonly_enabled) {
return;
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
$this->dispatch('configurationChanged');
}
public function checkFqdns($showToaster = true)
@@ -320,7 +314,7 @@ class General extends Component
public function set_redirect()
{
try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
$has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
@@ -337,15 +331,18 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
}
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
}
$this->checkFqdns();
@@ -408,9 +405,25 @@ class General extends Component
$this->application->save();
$showToaster && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
return handleError($e, $this);
} finally {
$this->dispatch('configurationChanged');
}
}
public function downloadConfig()
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
return response()->streamDownload(function () use ($config) {
echo $config;
}, $fileName, [
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename=' . $fileName,
]);
}
}

View File

@@ -31,10 +31,14 @@ class Form extends Component
public function generate_real_url()
{
if (data_get($this->application, 'fqdn')) {
$firstFqdn = str($this->application->fqdn)->before(',');
$url = Url::fromString($firstFqdn);
$host = $url->getHost();
$this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
try {
$firstFqdn = str($this->application->fqdn)->before(',');
$url = Url::fromString($firstFqdn);
$host = $url->getHost();
$this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
} catch (\Exception $e) {
$this->dispatch('error', 'Invalid FQDN.');
}
}
}

View File

@@ -31,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
'backup.dump_all' => 'required|boolean',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
'backup.dump_all' => 'Backup All Databases',
];
protected $messages = [

View File

@@ -26,7 +26,7 @@ class ScheduledBackups extends Component
public function mount(): void
{
if ($this->selectedBackupId) {
$this->setSelectedBackup($this->selectedBackupId);
$this->setSelectedBackup($this->selectedBackupId, true);
}
$this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
@@ -37,10 +37,13 @@ class ScheduledBackups extends Component
$this->s3s = currentTeam()->s3s;
}
public function setSelectedBackup($backupId)
public function setSelectedBackup($backupId, $force = false)
{
if ($this->selectedBackupId === $backupId && ! $force) {
return;
}
$this->selectedBackupId = $backupId;
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
$this->selectedBackup = $this->database->scheduledBackups->find($backupId);
if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null;
}

View File

@@ -31,10 +31,12 @@ class PublicGitRepository extends Component
public bool $isStatic = false;
public bool $checkCoolifyConfig = true;
public ?string $publish_directory = null;
// In case of docker compose
public ?string $base_directory = null;
public string $base_directory = '/';
public ?string $docker_compose_location = '/docker-compose.yaml';
// End of docker compose
@@ -97,6 +99,7 @@ class PublicGitRepository extends Component
$this->base_directory = '/'.$this->base_directory;
}
}
}
public function updatedDockerComposeLocation()
@@ -275,6 +278,7 @@ class PublicGitRepository extends Component
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
'base_directory' => $this->base_directory,
];
} else {
$application_init = [
@@ -289,6 +293,7 @@ class PublicGitRepository extends Component
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
'base_directory' => $this->base_directory,
];
}
@@ -303,11 +308,15 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->isStatic;
$application->settings->save();
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->save();
if ($this->checkCoolifyConfig) {
// $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
// if ($config) {
// $application->setConfig($config);
// }
}
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
use Spatie\Url\Url;
class EditDomain extends Component
{
@@ -20,21 +21,16 @@ class EditDomain extends Component
{
$this->application = ServiceApplication::find($this->applicationId);
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function submit()
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -44,12 +40,15 @@ class EditDomain extends Component
} else {
$this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
return handleError($e, $this);
}
}

View File

@@ -108,6 +108,21 @@ class Navbar extends Component
return;
}
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
PullImage::run($this->service);
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();

View File

@@ -6,6 +6,7 @@ use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
class ServiceApplicationView extends Component
{
@@ -31,13 +32,7 @@ class ServiceApplicationView extends Component
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function instantSave()
@@ -83,6 +78,14 @@ class ServiceApplicationView extends Component
public function submit()
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -92,10 +95,13 @@ class ServiceApplicationView extends Component
} else {
$this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('generateDockerCompose');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
return handleError($e, $this);
}
}

View File

@@ -34,6 +34,7 @@ class StackForm extends Component
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword', false);
$customHelper = data_get($field, 'customHelper', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -41,6 +42,7 @@ class StackForm extends Component
'value' => $value,
'isPassword' => $isPassword,
'rules' => $rules,
'customHelper' => $customHelper,
]);
$this->rules["fields.$key.value"] = $rules;

View File

@@ -48,14 +48,6 @@ class Add extends Component
public function submit()
{
$this->validate();
// if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
// $type = str($this->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;
// }
// }
$this->dispatch('saveKey', [
'key' => $this->key,
'value' => $this->value,

View File

@@ -53,30 +53,16 @@ class All extends Component
public function sortEnvironmentVariables()
{
if ($this->resource->type() === 'application') {
$this->resource->load(['environment_variables', 'environment_variables_preview']);
} else {
$this->resource->load(['environment_variables']);
if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
if ($this->resource->environment_variables) {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
}
if ($this->resource->environment_variables_preview) {
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values();
}
}
$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();
}
@@ -121,6 +107,8 @@ class All extends Component
$this->sortEnvironmentVariables();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->refreshEnvs();
}
}

View File

@@ -34,9 +34,9 @@ class Terminal extends Component
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'");
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && 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'");
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && 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

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use Livewire\Component;
class UploadConfig extends Component
{
public $config;
public $applicationId;
public function mount() {
if (isDev()) {
$this->config = '{
"build_pack": "nixpacks",
"base_directory": "/nodejs",
"publish_directory": "/",
"ports_exposes": "3000",
"settings": {
"is_static": false
}
}';
}
}
public function uploadConfig()
{
try {
$application = Application::findOrFail($this->applicationId);
$application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
return;
}
}
public function render()
{
return view('livewire.project.shared.upload-config');
}
}

View File

@@ -15,6 +15,8 @@ class ApiTokens extends Component
public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only'];
public $isApiEnabled;
@@ -35,12 +37,11 @@ class ApiTokens extends Component
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
if (count($this->permissions) == 0) {
$this->permissions = ['*'];
}
$this->makeSureOneIsSelected();
}
public function updatedReadOnly()
@@ -48,11 +49,30 @@ class ApiTokens extends Component
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
if (count($this->permissions) == 0) {
$this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
}
}
@@ -62,12 +82,6 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
// if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);

View File

@@ -30,6 +30,11 @@ class ConfigureCloudflareTunnels extends Component
public function submit()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;

View File

@@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
@@ -44,7 +44,10 @@ class Status extends Component
}
$this->numberOfPolls++;
}
CheckProxy::run($this->server, true);
$shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;

View File

@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint',
];
public function mount()
public function updatedEndpoint($value)
{
if (isDev()) {
$this->name = 'Local MinIO';
$this->description = 'Local MinIO';
$this->key = 'minioadmin';
$this->secret = 'minioadmin';
$this->bucket = 'local';
$this->endpoint = 'http://coolify-minio:9000';
if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
$this->endpoint = 'https://'.$value;
$value = $this->endpoint;
}
if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
$this->bucket = str($value)->after('//')->before('.');
} elseif (str($value)->contains('your-objectstorage.com')) {
$this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
}
}

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -143,6 +144,9 @@ class Application extends BaseModel
}
$application->tags()->detach();
$application->previews()->delete();
foreach ($application->deployment_queue as $deployment) {
$deployment->delete();
}
});
}
@@ -710,6 +714,11 @@ class Application extends BaseModel
return $this->hasMany(ApplicationPreview::class);
}
public function deployment_queue()
{
return $this->hasMany(ApplicationDeploymentQueue::class);
}
public function destination()
{
return $this->morphTo();
@@ -1419,4 +1428,67 @@ class Application extends BaseModel
return $parsedCollection->toArray();
}
}
public function generateConfig($is_json = false)
{
$config = collect([]);
if ($this->build_pack = 'nixpacks') {
$config = collect([
'build_pack' => 'nixpacks',
'docker_registry_image_name' => $this->docker_registry_image_name,
'docker_registry_image_tag' => $this->docker_registry_image_tag,
'install_command' => $this->install_command,
'build_command' => $this->build_command,
'start_command' => $this->start_command,
'base_directory' => $this->base_directory,
'publish_directory' => $this->publish_directory,
'custom_docker_run_options' => $this->custom_docker_run_options,
'ports_exposes' => $this->ports_exposes,
'ports_mappings' => $this->ports_mapping,
'settings' => collect([
'is_static' => $this->settings->is_static,
]),
]);
}
$config = $config->filter(function ($value) {
return str($value)->isNotEmpty();
});
if ($is_json) {
return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
return $config;
}
public function setConfig($config) {
$config = $config;
$validator = Validator::make(['config' => $config], [
'config' => 'required|json',
]);
if ($validator->fails()) {
throw new \Exception('Invalid JSON format');
}
$config = json_decode($config, true);
$deepValidator = Validator::make(['config' => $config], [
'config.build_pack' => 'required|string',
'config.base_directory' => 'required|string',
'config.publish_directory' => 'required|string',
'config.ports_exposes' => 'required|string',
'config.settings.is_static' => 'required|boolean',
]);
if ($deepValidator->fails()) {
throw new \Exception('Invalid data');
}
$config = $deepValidator->validated()['config'];
try {
$settings = data_get($config, 'settings', []);
data_forget($config, 'settings');
$this->update($config);
$this->settings()->update($settings);
} catch (\Exception $e) {
throw new \Exception('Failed to update application settings');
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
public function application(): Attribute
{
return Attribute::make(
get: fn () => Application::find($this->application_id),
);
}
public function server(): Attribute
{
return Attribute::make(
get: fn () => Server::find($this->server_id),
);
}
public function setStatus(string $status)
{
$this->update([

View File

@@ -24,9 +24,11 @@ class Project extends BaseModel
{
protected $guarded = [];
protected $appends = ['default_environment'];
public static function ownedByCurrentTeam()
{
return Project::whereTeamId(currentTeam()->id)->orderBy('name');
return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
}
protected static function booted()
@@ -131,7 +133,7 @@ class Project extends BaseModel
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
public function default_environment()
public function getDefaultEnvironmentAttribute()
{
$default = $this->environments()->where('name', 'production')->first();
if ($default) {

View File

@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}";
}
public function isHetzner()
{
return str($this->endpoint)->contains('your-objectstorage.com');
}
public function isDigitalOcean()
{
return str($this->endpoint)->contains('digitaloceanspaces.com');
}
public function testConnection(bool $shouldSave = false)
{
try {

View File

@@ -39,13 +39,19 @@ class ScheduledDatabaseBackup extends BaseModel
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
if ($this->database instanceof ServiceDatabase) {
$destination = data_get($this->database->service, 'destination');
$server = data_get($destination, 'server');
} else {
$destination = data_get($this->database, 'destination');
$server = data_get($destination, 'server');
}
if ($server) {
return $server;
}
}
return null;
}
}

View File

@@ -209,10 +209,13 @@ respond 404
1 => 'https',
],
'service' => 'noop',
'rule' => 'HostRegexp(`{catchall:.*}`)',
'rule' => 'HostRegexp(`.+`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp@file',
0 => 'redirect-regexp',
],
],
],
@@ -448,11 +451,19 @@ $schema://$host {
// Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK->value) {
$proxy_path = $proxy_path;
// Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = $proxy_path.'/caddy';
if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
} else {
$proxy_path = $proxy_path.'/caddy';
}
} elseif ($proxyType === ProxyTypes::NGINX->value) {
$proxy_path = $proxy_path.'/nginx';
if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
} else {
$proxy_path = $proxy_path.'/nginx';
}
}
return $proxy_path;
@@ -460,15 +471,6 @@ $schema://$host {
public function proxyType()
{
// $proxyType = $this->proxy->get('type');
// if ($proxyType === ProxyTypes::NONE->value) {
// return $proxyType;
// }
// if (is_null($proxyType)) {
// $this->proxy->type = ProxyTypes::TRAEFIK->value;
// $this->proxy->status = ProxyStatus::EXITED->value;
// $this->save();
// }
return data_get($this->proxy, 'type');
}
@@ -1221,4 +1223,9 @@ $schema://$host {
return instant_remote_process($commands, $this, false);
}
public function isIpv6(): bool
{
return str($this->ip)->contains(':');
}
}

View File

@@ -283,9 +283,162 @@ class Service extends BaseModel
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
$image = str($application->image)->before(':');
if ($image->isEmpty()) {
continue;
}
switch ($image) {
case str($image)?->contains('rabbitmq'):
case $image->contains('castopod'):
$data = collect([]);
$disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
if ($disable_https) {
$data = $data->merge([
'Disable HTTPS' => [
'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'),
'rules' => 'required',
'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
],
]);
}
$fields->put('Castopod', $data->toArray());
break;
case $image->contains('label-studio'):
$data = collect([]);
$username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first();
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first();
if ($username) {
$data = $data->merge([
'Username' => [
'key' => 'LABEL_STUDIO_USERNAME',
'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('Label Studio', $data->toArray());
break;
case $image->contains('litellm'):
$data = collect([]);
$username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first();
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first();
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('Litellm', $data->toArray());
break;
case $image->contains('langfuse'):
$data = collect([]);
$email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first();
if ($email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
]);
}
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
ray('password', $password);
if ($password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Langfuse', $data->toArray());
break;
case $image->contains('invoiceninja'):
$data = collect([]);
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
$data = $data->merge([
'Email' => [
'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
$data = $data->merge([
'Password' => [
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
$fields->put('Invoice Ninja', $data->toArray());
break;
case $image->contains('argilla'):
$data = collect([]);
$api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first();
$data = $data->merge([
'API Key' => [
'key' => data_get($api_key, 'key'),
'value' => data_get($api_key, 'value'),
'isPassword' => true,
'rules' => 'required',
],
]);
$data = $data->merge([
'API Key' => [
'key' => data_get($api_key, 'key'),
'value' => data_get($api_key, 'value'),
'isPassword' => true,
'rules' => 'required',
],
]);
$username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first();
$data = $data->merge([
'Username' => [
'key' => data_get($username, 'key'),
'value' => data_get($username, 'value'),
'rules' => 'required',
],
]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first();
$data = $data->merge([
'Password' => [
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
$fields->put('Argilla', $data->toArray());
break;
case $image->contains('rabbitmq'):
$data = collect([]);
$host_port = $this->environment_variables()->where('key', 'PORT')->first();
$username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first();
@@ -320,7 +473,7 @@ class Service extends BaseModel
}
$fields->put('RabbitMQ', $data->toArray());
break;
case str($image)?->contains('tolgee'):
case $image->contains('tolgee'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
$data = $data->merge([
@@ -334,7 +487,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'SERVICE_PASSWORD_TOLGEE',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -343,7 +496,7 @@ class Service extends BaseModel
}
$fields->put('Tolgee', $data->toArray());
break;
case str($image)?->contains('logto'):
case $image->contains('logto'):
$data = collect([]);
$logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first();
$logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
@@ -367,7 +520,7 @@ class Service extends BaseModel
}
$fields->put('Logto', $data->toArray());
break;
case str($image)?->contains('unleash-server'):
case $image->contains('unleash-server'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first();
$data = $data->merge([
@@ -381,7 +534,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'SERVICE_PASSWORD_UNLEASH',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -390,7 +543,7 @@ class Service extends BaseModel
}
$fields->put('Unleash', $data->toArray());
break;
case str($image)?->contains('grafana'):
case $image->contains('grafana'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
$data = $data->merge([
@@ -404,7 +557,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'GF_SECURITY_ADMIN_PASSWORD',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -413,7 +566,7 @@ class Service extends BaseModel
}
$fields->put('Grafana', $data->toArray());
break;
case str($image)?->contains('directus'):
case $image->contains('directus'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -439,7 +592,7 @@ class Service extends BaseModel
}
$fields->put('Directus', $data->toArray());
break;
case str($image)?->contains('kong'):
case $image->contains('kong'):
$data = collect([]);
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -463,7 +616,7 @@ class Service extends BaseModel
]);
}
$fields->put('Supabase', $data->toArray());
case str($image)?->contains('minio'):
case $image->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
@@ -516,7 +669,7 @@ class Service extends BaseModel
$fields->put('MinIO', $data->toArray());
break;
case str($image)?->contains('weblate'):
case $image->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
@@ -542,7 +695,7 @@ class Service extends BaseModel
}
$fields->put('Weblate', $data->toArray());
break;
case str($image)?->contains('meilisearch'):
case $image->contains('meilisearch'):
$data = collect([]);
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
if ($SERVICE_PASSWORD_MEILISEARCH) {
@@ -556,7 +709,7 @@ class Service extends BaseModel
}
$fields->put('Meilisearch', $data->toArray());
break;
case str($image)?->contains('ghost'):
case $image->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
@@ -616,45 +769,8 @@ class Service extends BaseModel
$fields->put('Ghost', $data->toArray());
break;
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([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Password' => [
'key' => 'SERVICE_PASSWORD_ADMIN',
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
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'):
case $image->contains('vaultwarden'):
$data = collect([]);
$DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first();
@@ -720,7 +836,7 @@ class Service extends BaseModel
$fields->put('Vaultwarden', $data);
break;
case str($image)->contains('gitlab/gitlab'):
case $image->contains('gitlab/gitlab'):
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first();
$data = collect([]);
if ($password) {
@@ -744,7 +860,7 @@ class Service extends BaseModel
$fields->put('GitLab', $data->toArray());
break;
case str($image)->contains('code-server'):
case $image->contains('code-server'):
$data = collect([]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first();
if ($password) {
@@ -770,14 +886,78 @@ class Service extends BaseModel
}
$fields->put('Code Server', $data->toArray());
break;
case $image->contains('elestio/strapi'):
$data = collect([]);
$license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
if ($license) {
$data = $data->merge([
'License' => [
'key' => data_get($license, 'key'),
'value' => data_get($license, 'value'),
],
]);
}
$nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
if ($nodeEnv) {
$data = $data->merge([
'Node Environment' => [
'key' => data_get($nodeEnv, 'key'),
'value' => data_get($nodeEnv, 'value'),
],
]);
}
$fields->put('Strapi', $data->toArray());
break;
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([
'User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($admin_email) {
$data = $data->merge([
'Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
$fields->put('Admin', $data->toArray());
break;
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
$image = str($database->image)->before(':')->value();
$image = str($database->image)->before(':');
if ($image->isEmpty()) {
continue;
}
switch ($image) {
case str($image)->contains('postgres'):
case $image->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
@@ -815,10 +995,10 @@ class Service extends BaseModel
}
$fields->put('PostgreSQL', $data->toArray());
break;
case str($image)->contains('mysql'):
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -865,7 +1045,7 @@ class Service extends BaseModel
}
$fields->put('MySQL', $data->toArray());
break;
case str($image)->contains('mariadb'):
case $image->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
@@ -928,6 +1108,7 @@ class Service extends BaseModel
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
ray($key, $value);
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
@@ -1052,12 +1233,12 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC");
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function workdir()
@@ -1108,7 +1289,6 @@ class Service extends BaseModel
$real_value = escapeEnvVariables($env->real_value);
}
}
ray("echo \"{$env->key}={$real_value}\" >> .env");
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
}
}

View File

@@ -112,4 +112,9 @@ class ServiceApplication extends BaseModel
{
getFilesystemVolumesFromServer($this, $isInit);
}
public function isBackupSolutionAvailable()
{
return false;
}
}

View File

@@ -115,4 +115,13 @@ class ServiceDatabase extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function isBackupSolutionAvailable()
{
return str($this->databaseType())->contains('mysql') ||
str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongodb');
}
}

View File

@@ -294,4 +294,9 @@ class StandaloneClickhouse extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return false;
}
}

View File

@@ -294,4 +294,9 @@ class StandaloneDragonfly extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return false;
}
}

View File

@@ -294,4 +294,9 @@ class StandaloneKeydb extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return false;
}
}

View File

@@ -294,4 +294,9 @@ class StandaloneMariadb extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return true;
}
}

View File

@@ -314,4 +314,9 @@ class StandaloneMongodb extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return true;
}
}

View File

@@ -295,4 +295,9 @@ class StandaloneMysql extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return true;
}
}

View File

@@ -296,4 +296,9 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return true;
}
}

View File

@@ -290,4 +290,9 @@ class StandaloneRedis extends BaseModel
return $parsedCollection->toArray();
}
}
public function isBackupSolutionAvailable()
{
return false;
}
}