diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml index 1fa47a930..681cbda3a 100644 --- a/.github/workflows/development-build.yml +++ b/.github/workflows/development-build.yml @@ -13,7 +13,7 @@ env: jobs: amd64: - runs-on: [self-hosted, x64] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Login to ghcr.io diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml index f6219767b..559145269 100644 --- a/.github/workflows/production-build.yml +++ b/.github/workflows/production-build.yml @@ -10,7 +10,7 @@ env: jobs: amd64: - runs-on: [self-hosted, x64] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Login to ghcr.io diff --git a/.tinkerwell/snippets/SendEmail.php b/.tinkerwell/snippets/SendEmail.php new file mode 100644 index 000000000..3b752bc04 --- /dev/null +++ b/.tinkerwell/snippets/SendEmail.php @@ -0,0 +1,28 @@ +to($user->email) + ->subject("Testing") + ->text( + <<id + +EOF + ); + }); +} diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index 8dd456ccd..12202b13e 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -4,26 +4,26 @@ use App\Models\InstanceSettings; use Illuminate\Support\Facades\Http; +use Lorisleiva\Actions\Concerns\AsAction; + class CheckResaleLicense { - public function __invoke() + use AsAction; + public function handle() { try { $settings = InstanceSettings::get(); - $settings->update([ - 'is_resale_license_active' => false, - ]); if (isDev()) { + $settings->update([ + 'is_resale_license_active' => true, + ]); return; } - if (!$settings->resale_license) { - return; - } + // if (!$settings->resale_license) { + // return; + // } $base_url = config('coolify.license_url'); - if (isDev()) { - $base_url = 'http://host.docker.internal:8787'; - } $instance_id = config('app.id'); ray("Checking license key against $base_url/lemon/validate"); diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index b2516eefb..f7de50165 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -4,7 +4,6 @@ use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Service; -use App\Notifications\Application\StatusChanged; class StopService { diff --git a/app/Console/Commands/ResourcesDelete.php b/app/Console/Commands/ResourcesDelete.php index 1c90f8e6b..84fbbeae6 100644 --- a/app/Console/Commands/ResourcesDelete.php +++ b/app/Console/Commands/ResourcesDelete.php @@ -61,12 +61,14 @@ private function deleteServer() foreach ($serversToDelete as $server) { $toDelete = $servers->where('id', $server)->first(); - $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { - break; + if ($toDelete) { + $this->info($toDelete); + $confirmed = confirm("Are you sure you want to delete all selected resources?"); + if (!$confirmed) { + break; + } + $toDelete->delete(); } - $toDelete->delete(); } } private function deleteApplication() @@ -82,14 +84,15 @@ private function deleteApplication() ); foreach ($applicationsToDelete as $application) { - ray($application); $toDelete = $applications->where('id', $application)->first(); - $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources? "); - if (!$confirmed) { - break; + if ($toDelete) { + $this->info($toDelete); + $confirmed = confirm("Are you sure you want to delete all selected resources? "); + if (!$confirmed) { + break; + } + $toDelete->delete(); } - $toDelete->delete(); } } private function deleteDatabase() @@ -106,12 +109,14 @@ private function deleteDatabase() foreach ($databasesToDelete as $database) { $toDelete = $databases->where('id', $database)->first(); - $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { - return; + if ($toDelete) { + $this->info($toDelete); + $confirmed = confirm("Are you sure you want to delete all selected resources?"); + if (!$confirmed) { + return; + } + $toDelete->delete(); } - $toDelete->delete(); } } private function deleteService() @@ -128,12 +133,14 @@ private function deleteService() foreach ($servicesToDelete as $service) { $toDelete = $services->where('id', $service)->first(); - $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { - return; + if ($toDelete) { + $this->info($toDelete); + $confirmed = confirm("Are you sure you want to delete all selected resources?"); + if (!$confirmed) { + return; + } + $toDelete->delete(); } - $toDelete->delete(); } } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4aa0fc339..c39cb626a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -23,7 +23,7 @@ protected function schedule(Schedule $schedule): void // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); - + // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // Server Jobs $this->check_scheduled_backups($schedule); $this->check_resources($schedule); @@ -34,7 +34,7 @@ protected function schedule(Schedule $schedule): void // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); + // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // Server Jobs $this->instance_auto_update($schedule); diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index aa2787de7..6f58c71e6 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -41,7 +41,7 @@ public function deployments() if (!$application) { return redirect()->route('dashboard'); } - ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 8); + ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40); return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]); } diff --git a/app/Http/Controllers/MagicController.php b/app/Http/Controllers/MagicController.php index d7635fda0..d47acac0c 100644 --- a/app/Http/Controllers/MagicController.php +++ b/app/Http/Controllers/MagicController.php @@ -32,8 +32,14 @@ public function projects() public function environments() { + $project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first(); + if (!$project) { + return response()->json([ + 'environments' => [] + ]); + } return response()->json([ - 'environments' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first()->environments + 'environments' => $project->environments ]); } diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index 1b9093be6..1d57f9b0a 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -164,7 +164,7 @@ public function saveServer() { $this->validate([ 'remoteServerName' => 'required', - 'remoteServerHost' => 'required|ip', + 'remoteServerHost' => 'required', 'remoteServerPort' => 'required|integer', 'remoteServerUser' => 'required', ]); diff --git a/app/Http/Livewire/CheckLicense.php b/app/Http/Livewire/CheckLicense.php index 3c5d1b8f2..3c2933bfc 100644 --- a/app/Http/Livewire/CheckLicense.php +++ b/app/Http/Livewire/CheckLicense.php @@ -32,7 +32,7 @@ public function submit() $this->settings->save(); if ($this->settings->resale_license) { try { - resolve(CheckResaleLicense::class)(); + CheckResaleLicense::run(); $this->emit('reloadWindow'); } catch (\Throwable $e) { session()->flash('error', 'Something went wrong. Please contact support.
Error: ' . $e->getMessage()); diff --git a/app/Http/Livewire/Destination/Form.php b/app/Http/Livewire/Destination/Form.php index fbce335e7..4f1bbc693 100644 --- a/app/Http/Livewire/Destination/Form.php +++ b/app/Http/Livewire/Destination/Form.php @@ -16,7 +16,7 @@ class Form extends Component protected $validationAttributes = [ 'destination.name' => 'name', 'destination.network' => 'network', - 'destination.server.ip' => 'IP Address', + 'destination.server.ip' => 'IP Address/Domain', ]; public function submit() diff --git a/app/Http/Livewire/Project/Application/Deployments.php b/app/Http/Livewire/Project/Application/Deployments.php index 1dd80d710..4f4d5ef67 100644 --- a/app/Http/Livewire/Project/Application/Deployments.php +++ b/app/Http/Livewire/Project/Application/Deployments.php @@ -3,24 +3,31 @@ namespace App\Http\Livewire\Project\Application; use App\Models\Application; +use Illuminate\Support\Collection; use Livewire\Component; class Deployments extends Component { public Application $application; - public $deployments = []; + public Array|Collection $deployments = []; public int $deployments_count = 0; public string $current_url; public int $skip = 0; - public int $default_take = 8; + public int $default_take = 40; public bool $show_next = false; - + public ?string $pull_request_id = null; + protected $queryString = ['pull_request_id']; public function mount() { $this->current_url = url()->current(); + $this->show_pull_request_only(); $this->show_more(); } - + private function show_pull_request_only() { + if ($this->pull_request_id) { + $this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id); + } + } private function show_more() { if (count($this->deployments) !== 0) { @@ -47,6 +54,7 @@ public function load_deployments(int|null $take = null) ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take); $this->deployments = $deployments; $this->deployments_count = $count; + $this->show_pull_request_only(); $this->show_more(); } } diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index c9c0c77ee..703c85b92 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -152,7 +152,7 @@ public function getWildcardDomain() $fqdn = generateFqdn($server, $this->application->uuid); $this->application->fqdn = $fqdn; $this->application->save(); - $this->emit('success', 'Application settings updated!'); + $this->updatedApplicationFqdn(); } } public function resetDefaultLabels($showToaster = true) diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index 0cb25b172..be96d4ffa 100644 --- a/app/Http/Livewire/Project/Application/Heading.php +++ b/app/Http/Livewire/Project/Application/Heading.php @@ -65,4 +65,18 @@ public function stop() $this->application->save(); $this->application->refresh(); } + public function restart() { + $this->setDeploymentUuid(); + queue_application_deployment( + application_id: $this->application->id, + deployment_uuid: $this->deploymentUuid, + restart_only: true, + ); + return redirect()->route('project.application.deployment', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'deployment_uuid' => $this->deploymentUuid, + 'environment_name' => $this->parameters['environment_name'], + ]); + } } diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepository.php b/app/Http/Livewire/Project/New/GithubPrivateRepository.php index 9cf0ee11c..91e9a806a 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepository.php @@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Route; use Livewire\Component; -use Spatie\Url\Url; class GithubPrivateRepository extends Component { diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php index fba473250..79d933369 100644 --- a/app/Http/Livewire/Project/Service/Index.php +++ b/app/Http/Livewire/Project/Service/Index.php @@ -13,7 +13,7 @@ class Index extends Component public $databases; public array $parameters; public array $query; - protected $listeners = ["refreshStacks","checkStatus"]; + protected $listeners = ["refreshStacks", "checkStatus"]; public function render() { return view('livewire.project.service.index'); diff --git a/app/Http/Livewire/Project/Service/Navbar.php b/app/Http/Livewire/Project/Service/Navbar.php index b8e604ad4..23ad062a5 100644 --- a/app/Http/Livewire/Project/Service/Navbar.php +++ b/app/Http/Livewire/Project/Service/Navbar.php @@ -4,7 +4,6 @@ use App\Actions\Service\StartService; use App\Actions\Service\StopService; -use App\Jobs\ContainerStatusJob; use App\Models\Service; use Livewire\Component; @@ -13,15 +12,14 @@ class Navbar extends Component public Service $service; public array $parameters; public array $query; + protected $listeners = ["checkStatus"]; public function render() { return view('livewire.project.service.navbar'); } - - public function checkStatus() - { - $this->emit('checkStatus'); + public function checkStatus() { + $this->service->refresh(); } public function deploy() { @@ -34,6 +32,6 @@ public function stop() StopService::run($this->service); $this->service->refresh(); $this->emit('success', 'Service stopped successfully.'); - $this->checkStatus(); + $this->emit('checkStatus'); } } diff --git a/app/Http/Livewire/Project/Shared/Danger.php b/app/Http/Livewire/Project/Shared/Danger.php index 45bc5d2d7..f2bef04d4 100644 --- a/app/Http/Livewire/Project/Shared/Danger.php +++ b/app/Http/Livewire/Project/Shared/Danger.php @@ -2,7 +2,7 @@ namespace App\Http\Livewire\Project\Shared; -use App\Jobs\StopResourceJob; +use App\Jobs\DeleteResourceJob; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -21,7 +21,7 @@ public function mount() public function delete() { try { - StopResourceJob::dispatchSync($this->resource); + DeleteResourceJob::dispatchSync($this->resource); return redirect()->route('project.resources', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_name' => $this->parameters['environment_name'] diff --git a/app/Http/Livewire/Project/Shared/GetLogs.php b/app/Http/Livewire/Project/Shared/GetLogs.php index 90983785d..e82f20497 100644 --- a/app/Http/Livewire/Project/Shared/GetLogs.php +++ b/app/Http/Livewire/Project/Shared/GetLogs.php @@ -33,9 +33,11 @@ public function getLogs($refresh = false) if ($refresh) { $this->outputs = ''; } - Process::run($sshCommand, function (string $type, string $output) { - $this->doSomethingWithThisChunkOfOutput($output); - }); + $command = Process::run($sshCommand); + $output = $command->output(); + $error = $command->errorOutput(); + $this->doSomethingWithThisChunkOfOutput($output); + $this->doSomethingWithThisChunkOfOutput($error); } } public function render() diff --git a/app/Http/Livewire/Server/Delete.php b/app/Http/Livewire/Server/Delete.php new file mode 100644 index 000000000..966de4f19 --- /dev/null +++ b/app/Http/Livewire/Server/Delete.php @@ -0,0 +1,31 @@ +authorize('delete', $this->server); + if (!$this->server->isEmpty()) { + $this->emit('error', 'Server has defined resources. Please delete them first.'); + return; + } + $this->server->delete(); + return redirect()->route('server.all'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() + { + return view('livewire.server.delete'); + } +} diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 816514aeb..4caa15b3f 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -4,12 +4,10 @@ use App\Actions\Server\InstallDocker; use App\Models\Server; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Form extends Component { - use AuthorizesRequests; public Server $server; public bool $isValidConnection = false; public bool $isValidDocker = false; @@ -32,7 +30,7 @@ class Form extends Component protected $validationAttributes = [ 'server.name' => 'Name', 'server.description' => 'Description', - 'server.ip' => 'IP address', + 'server.ip' => 'IP address/Domain', 'server.user' => 'User', 'server.port' => 'Port', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', @@ -106,26 +104,12 @@ public function validateServer($install = true) } } - public function delete() - { - try { - $this->authorize('delete', $this->server); - if (!$this->server->isEmpty()) { - $this->emit('error', 'Server has defined resources. Please delete them first.'); - return; - } - $this->server->delete(); - return redirect()->route('server.all'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } public function submit() { - if(isCloud() && !isDev()) { + if (isCloud() && !isDev()) { $this->validate(); $this->validate([ - 'server.ip' => 'required|ip', + 'server.ip' => 'required', ]); } else { $this->validate(); diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index ec4827af7..66750db28 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -26,14 +26,14 @@ class ByIp extends Component protected $rules = [ 'name' => 'required|string', 'description' => 'nullable|string', - 'ip' => 'required|ip', + 'ip' => 'required', 'user' => 'required|string', 'port' => 'required|integer', ]; protected $validationAttributes = [ 'name' => 'Name', 'description' => 'Description', - 'ip' => 'IP Address', + 'ip' => 'IP Address/Domain', 'user' => 'User', 'port' => 'Port', ]; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 583342015..e399fb68f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -44,6 +44,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private int $pull_request_id; private string $commit; private bool $force_rebuild; + private bool $restart_only; private ?string $dockerImage = null; private ?string $dockerImageTag = null; @@ -94,6 +95,7 @@ public function __construct(int $application_deployment_queue_id) $this->pull_request_id = $this->application_deployment_queue->pull_request_id; $this->commit = $this->application_deployment_queue->commit; $this->force_rebuild = $this->application_deployment_queue->force_rebuild; + $this->restart_only = $this->application_deployment_queue->restart_only; $source = data_get($this->application, 'source'); if ($source) { @@ -136,9 +138,16 @@ public function __construct(int $application_deployment_queue_id) public function handle(): void { // ray()->measure(); - $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id); - if ($containers->count() > 0) { + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + if ($containers->count() === 1) { $this->currently_running_container_name = data_get($containers[0], 'Names'); + } else { + $foundContainer = $containers->filter(function ($container) { + return !str(data_get($container, 'Names'))->startsWith("{$this->application->uuid}-pr-"); + })->first(); + if ($foundContainer) { + $this->currently_running_container_name = data_get($foundContainer, 'Names'); + } } if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) { $this->currently_running_container_name = $this->container_name; @@ -182,7 +191,9 @@ public function handle(): void $this->application->git_repository = "$gitHost:$gitRepo"; } try { - if ($this->application->dockerfile) { + if ($this->restart_only) { + $this->just_restart(); + } else if ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); } else if ($this->application->build_pack === 'dockerimage') { $this->deploy_dockerimage_buildpack(); @@ -264,6 +275,49 @@ public function handle(): void // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], // ); // } + private function generate_image_names() + { + if ($this->application->dockerfile) { + $this->build_image_name = Str::lower("{$this->application->git_repository}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + } else if ($this->application->build_pack === 'dockerimage') { + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + } else if ($this->pull_request_id !== 0) { + $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); + } else { + $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); + if (strlen($tag) > 128) { + $tag = $tag->substr(0, 128); + } + $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); + } + ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); + } + private function just_restart() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + $this->execute_remote_command([ + "echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'", + ]); + } private function save_environment_variables() { $envs = collect([]); @@ -291,9 +345,7 @@ private function deploy_simple_dockerfile() executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") ], ); - $this->build_image_name = Str::lower("{$this->application->git_repository}:build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); - // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); + $this->generate_image_names(); $this->generate_compose_file(); $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); @@ -311,7 +363,7 @@ private function deploy_dockerimage_buildpack() "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" ], ); - $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + $this->generate_image_names(); $this->prepare_builder_image(); $this->generate_compose_file(); $this->rolling_update(); @@ -328,16 +380,10 @@ private function deploy_dockerfile_buildpack() ], ); $this->prepare_builder_image(); + $this->check_git_if_build_needed(); $this->clone_repository(); $this->set_base_dir(); - $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); - if (strlen($tag) > 128) { - $tag = $tag->substr(0, 128); - } - - $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); - // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); + $this->generate_image_names(); $this->cleanup_git(); $this->generate_compose_file(); $this->generate_build_env_variables(); @@ -355,15 +401,7 @@ private function deploy_nixpacks_buildpack() $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->set_base_dir(); - $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); - if (strlen($tag) > 128) { - $tag = $tag->substr(0, 128); - } - - $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); - // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); - + $this->generate_image_names(); if (!$this->force_rebuild) { $this->execute_remote_command([ "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" @@ -396,7 +434,7 @@ private function rolling_update() { if (count($this->application->ports_mappings_array) > 0) { $this->execute_remote_command( - ["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"], + ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], ); $this->stop_running_container(force: true); $this->start_by_compose_file(); @@ -457,9 +495,7 @@ private function health_check() } private function deploy_pull_request() { - $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); - $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); - // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); + $this->generate_image_names(); $this->execute_remote_command([ "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", ]); @@ -475,7 +511,12 @@ private function deploy_pull_request() // $this->generate_build_env_variables(); // $this->add_build_env_variables_to_dockerfile(); $this->build_image(); - $this->stop_running_container(); + if ($this->currently_running_container_name) { + $this->execute_remote_command( + ["echo -n 'Removing old version of your application.'"], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + } $this->execute_remote_command( ["echo -n 'Starting preview deployment.'"], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], @@ -545,7 +586,9 @@ private function check_git_if_build_needed() ); } - $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + if ($this->saved_outputs->get('git_commit_sha')) { + $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + } } private function clone_repository() { @@ -596,7 +639,11 @@ private function generate_git_import_commands() } if ($this->application->deploymentType() === 'deploy_key') { $this->fullRepoUrl = $this->application->git_repository; - $private_key = base64_encode($this->application->private_key->private_key); + $private_key = data_get($this->application, 'private_key.private_key'); + if (is_null($private_key)) { + throw new Exception('Private key not found. Please add a private key to the application and try again.'); + } + $private_key = base64_encode($private_key); $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}"; $git_clone_command = $this->set_git_import_settings($git_clone_command); $commands = collect([ @@ -659,7 +706,7 @@ private function generate_nixpacks_confs() private function nixpacks_build_cmd() { $this->generate_env_variables(); - $nixpacks_command = "nixpacks build --no-cache -o {$this->workdir} {$this->env_args} --no-error-without-start"; + $nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start"; if ($this->application->build_command) { $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; } @@ -707,6 +754,23 @@ private function generate_compose_file() } else { $labels = collect(generateLabelsApplication($this->application, $this->preview)); } + if ($this->pull_request_id !== 0) { + $newLabels = collect(generateLabelsApplication($this->application, $this->preview)); + $newHostLabel = $newLabels->filter(function ($label) { + return str($label)->contains('Host'); + }); + $labels = $labels->reject(function ($label) { + return str($label)->contains('Host'); + }); + + $labels = $labels->map(function ($label) { + $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/'; + $replacement = "$1-pr-{$this->pull_request_id}-$2-$3"; + $newLabel = preg_replace($pattern, $replacement, $label); + return $newLabel; + }); + $labels = $labels->merge($newHostLabel); + } $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $docker_compose = [ 'version' => '3.8', @@ -907,12 +971,12 @@ private function stop_running_container(bool $force = false) if ($this->newVersionIsHealthy || $force) { $this->execute_remote_command( ["echo -n 'Removing old version of your application.'"], - [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); } else { $this->execute_remote_command( ["echo -n 'New version is not healthy, rolling back to the old version.'"], - [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); } } diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index 08530aeae..fbc951579 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -21,7 +21,7 @@ public function __construct() public function handle(): void { try { - resolve(CheckResaleLicense::class)(); + CheckResaleLicense::run(); } catch (\Throwable $e) { send_internal_notification('CheckResaleLicenseJob failed with: ' . $e->getMessage()); ray($e); diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index ad1593fac..0d4028f8f 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -18,7 +18,6 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Arr; -use Illuminate\Support\Str; class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted { @@ -26,6 +25,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted public function __construct(public Server $server) { + $this->handle(); } public function middleware(): array { @@ -58,6 +58,23 @@ public function handle(): void $this->server->update([ 'unreachable_count' => 0, ]); + // Update all applications, databases and services to exited + foreach ($this->server->applications() as $application) { + $application->update(['status' => 'exited']); + } + foreach ($this->server->databases() as $database) { + $database->update(['status' => 'exited']); + } + foreach ($this->server->services() as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->update(['status' => 'exited']); + } + foreach ($dbs as $db) { + $db->update(['status' => 'exited']); + } + } return; } $result = $this->server->validateConnection(); @@ -138,11 +155,10 @@ public function handle(): void $containerStatus = "$containerStatus ($containerHealth)"; $labels = data_get($container, 'Config.Labels'); $labels = Arr::undot(format_docker_labels_to_json($labels)); - $labelId = data_get($labels, 'coolify.applicationId'); - if ($labelId) { - if (str_contains($labelId, '-pr-')) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - $applicationId = (int) Str::before($labelId, '-pr-'); + $applicationId = data_get($labels, 'coolify.applicationId'); + if ($applicationId) { + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + if ($pullRequestId) { $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); if ($preview) { $foundApplicationPreviews[] = $preview->id; @@ -154,7 +170,7 @@ public function handle(): void //Notify user that this container should not be there. } } else { - $application = $applications->where('id', $labelId)->first(); + $application = $applications->where('id', $applicationId)->first(); if ($application) { $foundApplications[] = $application->id; $statusFromDb = $application->status; @@ -230,10 +246,13 @@ public function handle(): void $name = data_get($exitedService, 'name'); $fqdn = data_get($exitedService, 'fqdn'); $containerName = $name ? "$name ($fqdn)" : $fqdn; - $project = data_get($service, 'environment.project'); - $environment = data_get($service, 'environment'); + $projectUuid = data_get($service, 'environment.project.uuid'); + $serviceUuid = data_get($service, 'uuid'); + $environmentName = data_get($service, 'environment.name'); - $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid; + if ($projectUuid && $serviceUuid && $environmentName) { + $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; + } $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $exitedService->update(['status' => 'exited']); } @@ -251,10 +270,13 @@ public function handle(): void $containerName = $name ? "$name ($fqdn)" : $fqdn; - $project = data_get($application, 'environment.project'); - $environment = data_get($application, 'environment'); + $projectUuid = data_get($application, 'environment.project.uuid'); + $applicationUuid = data_get($application, 'uuid'); + $environment = data_get($application, 'environment.name'); - $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid; + if ($projectUuid && $applicationUuid && $environment) { + $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; + } $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); } @@ -271,10 +293,14 @@ public function handle(): void $containerName = $name ? "$name ($fqdn)" : $fqdn; - $project = data_get($preview, 'application.environment.project'); - $environment = data_get($preview, 'application.environment'); + $projectUuid = data_get($preview, 'application.environment.project.uuid'); + $environmentName = data_get($preview, 'application.environment.name'); + $applicationUuid = data_get($preview, 'application.uuid'); + + if ($projectUuid && $applicationUuid && $environmentName) { + $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; + } - $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $preview->application->uuid; $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); } $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); @@ -290,10 +316,13 @@ public function handle(): void $containerName = $name; - $project = data_get($database, 'environment.project'); - $environment = data_get($database, 'environment'); + $projectUuid = data_get($database, 'environment.project.uuid'); + $environmentName = data_get($database, 'environment.name'); + $databaseUuid = data_get($database, 'uuid'); - $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid; + if ($projectUuid && $databaseUuid && $environmentName) { + $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; + } $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); } } catch (\Throwable $e) { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index da660c449..804233de9 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Actions\Database\StopDatabase; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; @@ -22,6 +23,7 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; +use Throwable; class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted { @@ -64,6 +66,14 @@ public function uniqueId(): int public function handle(): void { try { + // Check if team is exists + if (is_null($this->team)) { + $this->backup->update(['status' => 'failed']); + StopDatabase::run($this->database); + $this->database->delete(); + return; + } + $status = Str::of(data_get($this->database, 'status')); if (!$status->startsWith('running') && $this->database->id !== 0) { ray('database not running'); diff --git a/app/Jobs/StopResourceJob.php b/app/Jobs/DeleteResourceJob.php similarity index 95% rename from app/Jobs/StopResourceJob.php rename to app/Jobs/DeleteResourceJob.php index 76c5588b8..0393d9f56 100644 --- a/app/Jobs/StopResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -19,7 +19,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class StopResourceJob implements ShouldQueue, ShouldBeEncrypted +class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -32,6 +32,7 @@ public function handle() try { $server = $this->resource->destination->server; if (!$server->isFunctional()) { + $this->resource->delete(); return 'Server is not functional'; } switch ($this->resource->type()) { @@ -57,11 +58,10 @@ public function handle() StopService::run($this->resource); break; } + $this->resource->delete(); } catch (\Throwable $e) { send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); throw $e; - } finally { - $this->resource->delete(); } } } diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 23d49c40e..4785da669 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -46,11 +46,12 @@ public function handle(): void if (!empty($this->buttons)) { foreach ($this->buttons as $button) { $buttonUrl = data_get($button, 'url'); + $text = data_get($button, 'text', 'Click here'); if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) { $buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl); } $inlineButtons[] = [ - 'text' => $button['text'], + 'text' => $text, 'url' => $buttonUrl, ]; } diff --git a/app/Models/Service.php b/app/Models/Service.php index ce676f63e..5b4d5fc09 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -40,7 +40,6 @@ protected static function booted() instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false); }); } - instant_remote_process(["docker network rm {$service->uuid}"], $service->server, false); }); } public function type() @@ -90,6 +89,10 @@ public function environment_variables(): HasMany { return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); } + public function environment_variables_preview(): HasMany + { + return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); + } public function workdir() { return service_configuration_dir() . "/{$this->uuid}"; @@ -257,7 +260,7 @@ public function parse(bool $isNew = false): Collection $networks = $serviceNetworks->toArray(); foreach ($definedNetwork as $key => $network) { $networks = array_merge($networks, [ - $network + $network => null ]); } data_set($service, 'networks', $networks); diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 277a250c9..cba7122ca 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -55,6 +55,6 @@ public function databases() public function attachedTo() { - return $this->applications?->count() > 0 || $this->databases?->count() > 0; + return $this->applications?->count() > 0 || $this->databases()->count() > 0; } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 06a6cb537..6e3b9e583 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -32,11 +32,11 @@ protected static function booted() ]); }); static::deleting(function ($database) { - $database->scheduledBackups()->delete(); $storages = $database->persistentStorages()->get(); foreach ($storages as $storage) { instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false); } + $database->scheduledBackups()->delete(); $database->persistentStorages()->delete(); $database->environment_variables()->delete(); }); diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index cd6dc4b56..82fc9a65c 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -34,7 +34,9 @@ public function send(SendsEmail $notifiable, Notification $notification): void if (isset($recepients)) { $message .= implode(', ', $recepients); } - $message .= " with subject: {$mailMessage->subject}"; + if (isset($mailMessage)) { + $message .= " with subject: {$mailMessage->subject}"; + } send_internal_notification($message); throw $e; } @@ -49,8 +51,8 @@ private function bootConfigs($notifiable): void } return; } - config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address')); - config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name')); + config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address', 'test@example.com')); + config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name', 'Test')); if (data_get($notifiable, 'resend_enabled')) { config()->set('mail.default', 'resend'); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key')); diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 427ef5a57..d78d19992 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -4,7 +4,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; -function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false) +function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false) { $deployment = ApplicationDeploymentQueue::create([ 'application_id' => $application_id, @@ -12,6 +12,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu 'pull_request_id' => $pull_request_id, 'force_rebuild' => $force_rebuild, 'is_webhook' => $is_webhook, + 'restart_only' => $restart_only, 'commit' => $commit, ]); $queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at'); diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index a61bb177c..8a01ec549 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -6,11 +6,14 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; -use Visus\Cuid2\Cuid2; -function getCurrentApplicationContainerStatus(Server $server, int $id): Collection +function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection { - $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); + if ($pullRequestId) { + $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --filter='label=coolify.pullRequestId={$pullRequestId}' --format '{{json .}}' "], $server); + } else { + $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}'"], $server); + } if (!$containers) { return collect([]); } @@ -77,20 +80,6 @@ function executeInDocker(string $containerId, string $command) // return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'"; } -function getApplicationContainerStatus(Application $application) -{ - $server = data_get($application, 'destination.server'); - $id = $application->id; - if (!$server) { - return 'exited'; - } - $containers = getCurrentApplicationContainerStatus($server, $id); - if ($containers->count() > 0) { - $status = data_get($containers[0], 'State', 'exited'); - return $status; - } - return 'exited'; -} function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false) { $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); @@ -212,9 +201,9 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $onlyPort = $ports[0]; } $pull_request_id = data_get($preview, 'pull_request_id', 0); - $appId = $application->id; - if ($pull_request_id !== 0 && $pull_request_id !== null) { - $appId = $appId . '-pr-' . $pull_request_id; + $appUuid = $application->uuid; + if ($pull_request_id !== 0) { + $appUuid = $appUuid . '-pr-' . $pull_request_id; } $labels = collect([]); if ($application->fqdn) { @@ -224,7 +213,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $domains = Str::of(data_get($application, 'fqdn'))->explode(','); } // Add Traefik labels no matter which proxy is selected - $labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort)); + $labels = $labels->merge(fqdnLabelsForTraefik($appUuid, $domains, $application->settings->is_force_https_enabled, $onlyPort)); } return $labels->all(); } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 25a6d2db7..e3d263a11 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -174,8 +174,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d return $formatted; } -function refresh_server_connection(PrivateKey $private_key) +function refresh_server_connection(?PrivateKey $private_key = null) { + if (is_null($private_key)) { + return; + } foreach ($private_key->servers as $server) { Storage::disk('ssh-mux')->delete($server->muxFilename()); } diff --git a/config/sentry.php b/config/sentry.php index 235f3db1e..e5bd40ee5 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // 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.108', + 'release' => '4.0.0-beta.109', // 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 f3852a439..de5c5d268 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('restart_only')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('restart_only'); + }); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 54863f2d6..276c5d53c 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -26,7 +26,7 @@ public function run(): void 'environment_id' => 1, 'destination_id' => 0, 'destination_type' => StandaloneDocker::class, - 'source_id' => 0, + 'source_id' => 1, 'source_type' => GithubApp::class ]); Application::create([ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9b7af1c02..0eb280c08 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -45,7 +45,7 @@ services: - /data/coolify/_volumes/redis/:/data # - coolify-redis-data-dev:/data vite: - image: node:19 + image: node:20 working_dir: /var/www/html ports: - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 020299189..850cad7a1 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.11.2 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.31.0 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.17.0 +ARG NIXPACKS_VERSION=1.18.0 USER root WORKDIR /artifacts diff --git a/resources/css/app.css b/resources/css/app.css index ee4ec7b77..05769567a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -121,3 +121,6 @@ .buyme { .subtitle { @apply pt-2 pb-10; } +.fullscreen { + @apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ; +} diff --git a/resources/views/components/applications/navbar.blade.php b/resources/views/components/applications/navbar.blade.php index d38790789..cab687b98 100644 --- a/resources/views/components/applications/navbar.blade.php +++ b/resources/views/components/applications/navbar.blade.php @@ -17,7 +17,7 @@ @if ($application->status !== 'exited') + + + + + +
+ @if (decode_remote_command_output($application_deployment_queue)->count() > 0) + @foreach (decode_remote_command_output($application_deployment_queue) as $line) +
$line['type'] == 'stdout', + 'text-error' => $line['type'] == 'stderr', + 'text-warning' => $line['hidden'], + ])>[{{ $line['timestamp'] }}] @if ($line['hidden']) +
COMMAND:
{{ $line['command'] }}

OUTPUT: + @endif{{ $line['output'] }}@if ($line['hidden']) + @endif +
+ @endforeach + @else + No logs yet. + @endif +
+ + diff --git a/resources/views/livewire/project/application/deployments.blade.php b/resources/views/livewire/project/application/deployments.blade.php index a8de050fc..ecf95ccaa 100644 --- a/resources/views/livewire/project/application/deployments.blade.php +++ b/resources/views/livewire/project/application/deployments.blade.php @@ -1,10 +1,16 @@ -
-

Deployments ({{ $deployments_count }})

- @if ($show_next) - Show More - - @endif - @foreach ($deployments as $deployment) +
+
+

Deployments ({{ $deployments_count }})

+ @if ($show_next) + Next Page + + @endif +
+
+ + Filter +
+ @forelse ($deployments as $deployment) @@ -16,45 +22,45 @@ data_get($deployment, 'status') === 'error', 'border-success hover:bg-success' => data_get($deployment, 'status') === 'finished', - ]) @if (data_get($deployment, 'status') !== 'cancelled by system' && data_get($deployment, 'status') !== 'queued') - href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" - @endif - class="hover:no-underline"> -
-
- {{ $deployment->id }} > {{ $deployment->deployment_uuid }} - > - {{ $deployment->status }} -
- @if (data_get($deployment, 'pull_request_id')) -
- Pull Request #{{ data_get($deployment, 'pull_request_id') }} - @if (data_get($deployment, 'is_webhook')) - (Webhook) + ]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" + class="hover:no-underline"> +
+
+ {{ $deployment->created_at }} UTC + > + {{ $deployment->status }} +
+ @if (data_get($deployment, 'pull_request_id')) +
+ > + Pull Request #{{ data_get($deployment, 'pull_request_id') }} + @if (data_get($deployment, 'is_webhook')) + (Webhook) + @endif + Webhook (SHA + @if (data_get($deployment, 'commit')) + {{ data_get($deployment, 'commit') }}) + @else + HEAD) + @endif +
@endif
- @elseif (data_get($deployment, 'is_webhook')) -
Webhook (sha - @if (data_get($deployment, 'commit')) - {{ data_get($deployment, 'commit') }}) - @else - HEAD) - @endif + +
+
+ @if ($deployment->status !== 'in_progress') + Finished 0s in + @else + Running for + @endif + 0s +
- @endif -
-
- @if ($deployment->status !== 'in_progress') - Finished 0s in - @else - Running for - @endif - 0s -
-
-
-
- @endforeach + + @empty +
No deployments found
+ @endforelse diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 79948bd6a..cfa0b0e23 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -49,14 +49,21 @@ @if ($application->build_pack !== 'dockerimage')

Build

@if ($application->could_set_build_commands()) -
- - - -
+ @if ($application->build_pack === 'nixpacks') +
Nixpacks will detect your package manager/configurations: Nixpacks documentation
+
You probably do not need to modify the commands below.
+
+ + + +
+ @endif @endif +
diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index 5e5d4d024..8f78ea517 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -78,11 +78,15 @@ Redeploy @endif - @if (data_get($preview, 'status') !== 'exited') - Remove - Preview + Remove + Preview + + + + Get Deployment Logs - @endif +
@endforeach diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index 933c89ecd..ea2ab7ff5 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -1,6 +1,6 @@ -
+
- +
Documentation diff --git a/resources/views/livewire/project/service/navbar.blade.php b/resources/views/livewire/project/service/navbar.blade.php index d662f026a..781ff6c7a 100644 --- a/resources/views/livewire/project/service/navbar.blade.php +++ b/resources/views/livewire/project/service/navbar.blade.php @@ -1,4 +1,4 @@ -
+

Configuration

diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index eed8f143b..16b2853cf 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -13,11 +13,25 @@ Refresh - -
-
- +
+
+ +
{{ $outputs }}
diff --git a/resources/views/livewire/server/delete.blade.php b/resources/views/livewire/server/delete.blade.php new file mode 100644 index 000000000..b4a9a030f --- /dev/null +++ b/resources/views/livewire/server/delete.blade.php @@ -0,0 +1,18 @@ +
+ + +

This server will be deleted. It is not reversible.
Please think again..

+
+
+ @if ($server->id !== 0) +

Danger Zone

+
Woah. I hope you know what are you doing.
+

Delete Server

+
This will remove this server from Coolify. Beware! There is no coming + back! +
+ + Delete + + @endif +
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 462d07857..047bf1882 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -1,9 +1,4 @@
- - -

This server will be deleted. It is not reversible.
Please think again..

-
-

You could lost a lot of functionalities if you change the server details of the server where Coolify is @@ -47,7 +42,8 @@ label="Is it part of a Swarm cluster?" /> --}}

- +
@@ -66,21 +62,9 @@ helper="Disk cleanup job will be executed if disk usage is more than this number." /> @endif - @if ($server->id !== 0) -

Danger Zone

-
Woah. I hope you know what are you doing.
-

Delete Server

-
This will remove this server from Coolify. Beware! There is no coming - back! -
- - Delete - - @endif - + diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index 0a80b14a4..7dfb3a5b0 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -10,8 +10,8 @@
- +
diff --git a/resources/views/livewire/source/github/create.blade.php b/resources/views/livewire/source/github/create.blade.php index 4d267a2c2..57897ce27 100644 --- a/resources/views/livewire/source/github/create.blade.php +++ b/resources/views/livewire/source/github/create.blade.php @@ -12,7 +12,7 @@
- +
@if (!isCloud()) diff --git a/resources/views/livewire/team/delete.blade.php b/resources/views/livewire/team/delete.blade.php index 18e3889a5..4e4bd5977 100644 --- a/resources/views/livewire/team/delete.blade.php +++ b/resources/views/livewire/team/delete.blade.php @@ -11,8 +11,7 @@
This is the default team. You can't delete it.
@elseif(auth()->user()->teams()->get()->count() === 1)
You can't delete your last team.
- @elseif(currentTeam()->subscription && - currentTeam()->subscription?->lemon_status !== 'cancelled') + @elseif(currentTeam()->subscription && currentTeam()->subscription?->lemon_status !== 'cancelled')
Please cancel your subscription before delete this team (Manage My Subscription).
@else @if (currentTeam()->isEmpty()) @@ -23,30 +22,38 @@ @else
You need to delete the following resources to be able to delete the team:
-

Projects:

-
    - @foreach (currentTeam()->projects as $resource) -
  • {{ $resource->name }}
  • - @endforeach -
-

Servers:

-
    - @foreach (currentTeam()->servers as $resource) -
  • {{ $resource->name }}
  • - @endforeach -
-

Private Keys:

-
    - @foreach (currentTeam()->privateKeys as $resource) -
  • {{ $resource->name }}
  • - @endforeach -
-

Sources:

-
    - @foreach (currentTeam()->sources() as $resource) -
  • {{ $resource->name }}
  • - @endforeach -
+ @if (currentTeam()->projects()->count() > 0) +

Projects:

+
    + @foreach (currentTeam()->projects as $resource) +
  • {{ $resource->name }}
  • + @endforeach +
+ @endif + @if (currentTeam()->servers()->count() > 0) +

Servers:

+
    + @foreach (currentTeam()->servers as $resource) +
  • {{ $resource->name }}
  • + @endforeach +
+ @endif + @if (currentTeam()->privateKeys()->count() > 0) +

Private Keys:

+
    + @foreach (currentTeam()->privateKeys as $resource) +
  • {{ $resource->name }}
  • + @endforeach +
+ @endif + @if (currentTeam()->sources()->count() > 0) +

Sources:

+
    + @foreach (currentTeam()->sources() as $resource) +
  • {{ $resource->name }}
  • + @endforeach +
+ @endif @endif @endif diff --git a/routes/webhooks.php b/routes/webhooks.php index dc1caa7c4..c38c62ae1 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -12,6 +12,7 @@ use App\Models\Webhook; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; use Visus\Cuid2\Cuid2; @@ -64,6 +65,7 @@ }); Route::post('/source/github/events', function () { try { + $id = null; $x_github_delivery = request()->header('X-GitHub-Delivery'); $x_github_event = Str::lower(request()->header('X-GitHub-Event')); $x_github_hook_installation_target_id = request()->header('X-GitHub-Hook-Installation-Target-Id'); @@ -73,7 +75,7 @@ // Just pong return response('pong'); } - if ($x_github_event === 'installation') { + if ($x_github_event === 'installation' || $x_github_event === 'installation_repositories') { // Installation handled by setup redirect url. Repositories queried on-demand. return response('cool'); } @@ -87,7 +89,6 @@ return response('not cool'); } } - if ($x_github_event === 'push') { $id = data_get($payload, 'repository.id'); $branch = data_get($payload, 'ref'); @@ -281,7 +282,11 @@ break; case 'invoice.paid': $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if (!$subscription) { + Sleep::for(5); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + } $planId = data_get($data, 'lines.data.0.plan.id'); $subscription->update([ 'stripe_plan_id' => $planId, diff --git a/templates/compose/budge.yaml b/templates/compose/budge.yaml new file mode 100644 index 000000000..379b5a6c3 --- /dev/null +++ b/templates/compose/budge.yaml @@ -0,0 +1,19 @@ +# documentation: https://github.com/linuxserver/budge +# slogan: BudgE is an open-source 'budgeting with envelopes' personal finance app, helping you manage your finances effectively. +# tags: personal finance, budgeting, expense tracking + +services: + budge: + image: lscr.io/linuxserver/budge:latest + environment: + - SERVICE_FQDN_BUDGE + - PUID=1000 + - PGID=1000 + - TZ=Europe/Madrid + volumes: + - budge-config:/config + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/duplicati.yaml b/templates/compose/duplicati.yaml new file mode 100644 index 000000000..0314f82e7 --- /dev/null +++ b/templates/compose/duplicati.yaml @@ -0,0 +1,20 @@ +# documentation: https://duplicati.readthedocs.io/en/latest/02-installation/ +# slogan: Duplicati is an open-source backup solution, allowing you to safeguard your data with ease through scheduled backups and encryption. +# tags: backup, encryption + +services: + duplicati: + image: lscr.io/linuxserver/duplicati:latest + environment: + - SERVICE_FQDN_DUPLICATI + - PUID=1000 + - PGID=1000 + - TZ=Europe/Madrid + volumes: + - duplicati-config:/config + - duplicati-backups:/backups + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8200"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/filebrowser.yaml b/templates/compose/filebrowser.yaml new file mode 100644 index 000000000..900964f87 --- /dev/null +++ b/templates/compose/filebrowser.yaml @@ -0,0 +1,20 @@ +# documentation: https://filebrowser.org/configuration +# slogan: FileBrowser is a self-hosted, web-based file manager and file explorer with a user-friendly interface. It allows you to manage and organize your files and directories directly from your web browser. +# tags: file-management, storage-access, data-organization, file-utilization, administration-tool + +services: + filebrowser: + image: filebrowser/filebrowser:latest + environment: + - SERVICE_FQDN_FILEBROWSER + - PUID=1000 + - PGID=1000 + volumes: + - filebrowser-srv:/srv + - filebrowser-database:/database/filebrowser.db + - filebrowser-config:/config/settings.json + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/jellyfin.yaml b/templates/compose/jellyfin.yaml new file mode 100644 index 000000000..c3cb90541 --- /dev/null +++ b/templates/compose/jellyfin.yaml @@ -0,0 +1,22 @@ +# documentation: https://jellyfin.org/docs/ +# slogan: Jellyfin is an open-source media server for hosting and streaming your media collection, providing an alternative to proprietary media platforms. +# tags: media, server, movies, tv, music + +services: + jellyfin: + image: lscr.io/linuxserver/jellyfin:latest + environment: + - SERVICE_FQDN_JELLYFIN + - PUID=1000 + - PGID=1000 + - TZ=Europe/Madrid + - JELLYFIN_PublishedServerUrl=$SERVICE_FQDN_JELLYFIN + volumes: + - jellyfin-config:/config + - jellyfin-tvshows:/data/tvshows + - jellyfin-movies:/data/movies + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8096"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/kuzzle.yaml b/templates/compose/kuzzle.yaml new file mode 100644 index 000000000..d9ea70629 --- /dev/null +++ b/templates/compose/kuzzle.yaml @@ -0,0 +1,60 @@ +# documentation: https://docs.kuzzle.io/ +# slogan: Kuzzle is a generic backend offering the basic building blocks common to every application. +# tags: backend, api, realtime, websocket, mqtt, rest, sdk, iot, geofencing, low-code + +services: + + redis: + image: redis:6.2.4 + command: redis-server --appendonly yes + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 1s + timeout: 3s + retries: 30 + + elasticsearch: + image: kuzzleio/elasticsearch:7 + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9200" ] + interval: 2s + timeout: 2s + retries: 10 + ulimits: + nofile: 65536 + + kuzzle: + image: kuzzleio/kuzzle:latest + environment: + - SERVICE_FQDN_KUZZLE_7512 + - kuzzle_services__storageEngine__client__node=http://elasticsearch:9200 + - kuzzle_services__storageEngine__commonMapping__dynamic=true + - kuzzle_services__internalCache__node__host=redis + - kuzzle_services__memoryStorage__node__host=redis + - kuzzle_server__protocols__mqtt__enabled=true + - kuzzle_server__protocols__mqtt__developmentMode=false + - kuzzle_limits__loginsPerSecond=50 + - NODE_ENV=production + # - DEBUG=${DEBUG:-kuzzle:*,-kuzzle:network:protocols:websocket,-kuzzle:events} + - DEBUG=${DEBUG:-kuzzle:cluster:sync} + - DEBUG_DEPTH=${DEBUG_DEPTH:-0} + - DEBUG_MAX_ARRAY_LENGTH=${DEBUG_MAX_ARRAY:-100} + - DEBUG_EXPAND=${DEBUG_EXPAND:-off} + - DEBUG_SHOW_HIDDEN={$DEBUG_SHOW_HIDDEN:-on} + - DEBUG_COLORS=${DEBUG_COLORS:-on} + cap_add: + - SYS_PTRACE + ulimits: + nofile: 65536 + sysctls: + - net.core.somaxconn=8192 + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:7512/_healthcheck" ] + timeout: 1s + interval: 2s + retries: 30 + depends_on: + redis: + condition: service_healthy + elasticsearch: + condition: service_healthy diff --git a/templates/compose/moodle.yaml b/templates/compose/moodle.yaml new file mode 100644 index 000000000..df5bce6f1 --- /dev/null +++ b/templates/compose/moodle.yaml @@ -0,0 +1,37 @@ +# documentation: https://moodle.org +# slogan: Moodle is the world’s most customisable and trusted eLearning solution that empowers educators to improve our world. +# tags: moodle, elearning, education, lms, cms, open, source, low, code + +services: + mariadb: + image: mariadb:11.1 + environment: + - ALLOW_EMPTY_PASSWORD=no + - MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_ROOT + - MYSQL_DATABASE=bitnami_moodle + - MYSQL_USER=$SERVICE_USER_MARIADB + - MYSQL_PASSWORD=$SERVICE_PASSWORD_MARIADB + - MARIADB_CHARACTER_SET=utf8mb4 + - MARIADB_COLLATE=utf8mb4_unicode_ci + volumes: + - mariadb-data:/var/lib/mysql + + moodle: + image: docker.io/bitnami/moodle:4.3 + environment: + - SERVICE_FQDN_MOODLE + - MOODLE_DATABASE_HOST=mariadb + - MOODLE_DATABASE_PORT_NUMBER=3306 + - MOODLE_DATABASE_USER=$SERVICE_USER_MARIADB + - MOODLE_DATABASE_NAME=bitnami_moodle + - MOODLE_DATABASE_PASSWORD=$SERVICE_PASSWORD_MARIADB + - ALLOW_EMPTY_PASSWORD=no + - MOODLE_USERNAME=${MOODLE_USERNAME:-user} + - MOODLE_PASSWORD=$SERVICE_PASSWORD_MOODLE + - MOODLE_EMAIL=user@example.com + - MOODLE_SITE_NAME=${MOODLE_SITE_NAME:-New Site} + volumes: + - moodle-data:/bitnami/moodle + - moodledata-data:/bitnami/moodledata + depends_on: + - mariadb diff --git a/templates/compose/phpmyadmin.yaml b/templates/compose/phpmyadmin.yaml new file mode 100644 index 000000000..4243362c0 --- /dev/null +++ b/templates/compose/phpmyadmin.yaml @@ -0,0 +1,21 @@ +# documentation: https://docs.phpmyadmin.net/en/latest/ +# slogan: phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface. +# tags: database management + +services: + phpmyadmin: + image: lscr.io/linuxserver/phpmyadmin:latest + environment: + - SERVICE_FQDN_PHPMYADMIN + - PUID=1000 + - PGID=1000 + - TZ=Europe/Madrid + - PMA_ARBITRARY=1 + - PMA_ABSOLUTE_URI=$SERVICE_FQDN_PHPMYADMIN + volumes: + - phpmyadmin-config:/config + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/rabbitmq.yaml b/templates/compose/rabbitmq.yaml new file mode 100644 index 000000000..b28171f1a --- /dev/null +++ b/templates/compose/rabbitmq.yaml @@ -0,0 +1,17 @@ +# ignore: true +# documentation: https://www.rabbitmq.com/documentation.html +# slogan: With tens of thousands of users, RabbitMQ is one of the most popular open source message brokers. +# tags: message broker, message queue, message-oriented middleware, MOM, AMQP, MQTT, STOMP, messaging + +services: + rabbitmq: + image: rabbitmq:3 + environment: + - SERVICE_FQDN_RABBITMQ_5672 + - RABBITMQ_DEFAULT_USER=$SERVICE_USER_RABBITMQ + - RABBITMQ_DEFAULT_PASS=$SERVICE_PASSWORD_RABBITMQ + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 30s + timeout: 30s + retries: 3 diff --git a/templates/compose/sonarqube.yaml b/templates/compose/sonarqube.yaml new file mode 100644 index 000000000..3f3f5d8e5 --- /dev/null +++ b/templates/compose/sonarqube.yaml @@ -0,0 +1,36 @@ +# ignore: true +# documentation: https://hub.docker.com/_/sonarqube/ +# slogan: SonarQube is a self-managed, automatic code review tool that systematically helps you deliver Clean Code +# tags: sonarqube, code-review, clean-code, quality, code-quality, code-analysis, code-smells, code-coverage, code-security + +services: + sonarqube: + image: sonarqube:community + environment: + - SERVICE_FQDN_SONARQUBE_9000 + - SONAR_JDBC_URL=jdbc:postgresql://postgresql:5432/${POSTGRES_DB:-sonar} + - SONAR_JDBC_USERNAME=$SERVICE_USER_POSTGRES + - SONAR_JDBC_PASSWORD=$SERVICE_PASSWORD_POSTGRES + volumes: + - sonarqube-data:/opt/sonarqube + - sonarqube-conf:/opt/sonarqube/conf + - sonarqube-extensions:/opt/sonarqube/extensions + - sonarqube-logs:/opt/sonarqube/logs + - sonarqube-bundled-plugins:/opt/sonarqube/lib/bundled-plugins + depends_on: + postgresql: + condition: service_healthy + postgresql: + image: postgres:15-alpine + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-sonar} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + diff --git a/templates/compose/vaultwarden.yaml b/templates/compose/vaultwarden.yaml new file mode 100644 index 000000000..5ced66f43 --- /dev/null +++ b/templates/compose/vaultwarden.yaml @@ -0,0 +1,16 @@ +# documentation: https://github.com/dani-garcia/vaultwarden/wiki/FAQs +# slogan: Vaultwarden is an open-source password manager that allows you to securely store and manage your passwords, helping you stay organized and protected. +# tags: password manager, security + +services: + vaultwarden: + image: vaultwarden/server:latest + environment: + - SERVICE_FQDN_VAULTWARDEN + volumes: + - vaultwarden-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/whoogle.yaml b/templates/compose/whoogle.yaml new file mode 100644 index 000000000..bb9fcf223 --- /dev/null +++ b/templates/compose/whoogle.yaml @@ -0,0 +1,14 @@ +# documentation: https://github.com/benbusby/whoogle-search#install +# slogan: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection. +# tags: privacy, search engine + +services: + whoogle: + image: benbusby/whoogle-search:latest + environment: + - SERVICE_FQDN_WHOOGLE + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/service-templates.json b/templates/service-templates.json index 883fbfe5a..31f4928e6 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -33,6 +33,16 @@ "activities" ] }, + "budge": { + "documentation": "https:\/\/github.com\/linuxserver\/budge", + "slogan": "BudgE is an open-source 'budgeting with envelopes' personal finance app, helping you manage your finances effectively.", + "compose": "c2VydmljZXM6CiAgYnVkZ2U6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvYnVkZ2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JVREdFCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnYnVkZ2UtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "personal finance", + "budgeting", + "expense tracking" + ] + }, "code-server": { "documentation": "https:\/\/coder.com\/docs\/code-server\/latest\/guide", "slogan": "Code-Server is a self-hosted, web-based code editor that enables remote coding and collaboration from any device, anywhere.", @@ -66,6 +76,15 @@ "base" ] }, + "duplicati": { + "documentation": "https:\/\/duplicati.readthedocs.io\/en\/latest\/02-installation\/", + "slogan": "Duplicati is an open-source backup solution, allowing you to safeguard your data with ease through scheduled backups and encryption.", + "compose": "c2VydmljZXM6CiAgZHVwbGljYXRpOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2R1cGxpY2F0aTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRFVQTElDQVRJCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZHVwbGljYXRpLWNvbmZpZzovY29uZmlnJwogICAgICAtICdkdXBsaWNhdGktYmFja3VwczovYmFja3VwcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MjAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "backup", + "encryption" + ] + }, "emby": { "documentation": "https:\/\/emby.media\/support\/articles\/Home.html", "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly, making it easy to enjoy your favorite movies, TV shows, music, and more.", @@ -99,6 +118,18 @@ "user-feedback" ] }, + "filebrowser": { + "documentation": "https:\/\/filebrowser.org\/configuration", + "slogan": "FileBrowser is a self-hosted, web-based file manager and file explorer with a user-friendly interface. It allows you to manage and organize your files and directories directly from your web browser.", + "compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUgogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgdm9sdW1lczoKICAgICAgLSAnZmlsZWJyb3dzZXItc3J2Oi9zcnYnCiAgICAgIC0gJ2ZpbGVicm93c2VyLWRhdGFiYXNlOi9kYXRhYmFzZS9maWxlYnJvd3Nlci5kYicKICAgICAgLSAnZmlsZWJyb3dzZXItY29uZmlnOi9jb25maWcvc2V0dGluZ3MuanNvbicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", + "tags": [ + "file-management", + "storage-access", + "data-organization", + "file-utilization", + "administration-tool" + ] + }, "ghost": { "documentation": "https:\/\/ghost.org\/docs", "slogan": "Ghost is a popular open-source content management system (CMS) and blogging platform, known for its simplicity and focus on content creation.", @@ -156,6 +187,35 @@ "interface" ] }, + "jellyfin": { + "documentation": "https:\/\/jellyfin.org\/docs\/", + "slogan": "Jellyfin is an open-source media server for hosting and streaming your media collection, providing an alternative to proprietary media platforms.", + "compose": "c2VydmljZXM6CiAgamVsbHlmaW46CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvamVsbHlmaW46bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pFTExZRklOCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIEpFTExZRklOX1B1Ymxpc2hlZFNlcnZlclVybD0kU0VSVklDRV9GUUROX0pFTExZRklOCiAgICB2b2x1bWVzOgogICAgICAtICdqZWxseWZpbi1jb25maWc6L2NvbmZpZycKICAgICAgLSAnamVsbHlmaW4tdHZzaG93czovZGF0YS90dnNob3dzJwogICAgICAtICdqZWxseWZpbi1tb3ZpZXM6L2RhdGEvbW92aWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwOTYnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "media", + "server", + "movies", + "tv", + "music" + ] + }, + "kuzzle": { + "documentation": "https:\/\/docs.kuzzle.io\/", + "slogan": "Kuzzle is a generic backend offering the basic building blocks common to every application.", + "compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYuMi40JwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMXMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogMzAKICBlbGFzdGljc2VhcmNoOgogICAgaW1hZ2U6ICdrdXp6bGVpby9lbGFzdGljc2VhcmNoOjcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6OTIwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDJzCiAgICAgIHJldHJpZXM6IDEwCiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6IDY1NTM2CiAga3V6emxlOgogICAgaW1hZ2U6ICdrdXp6bGVpby9rdXp6bGU6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0tVWlpMRV83NTEyCiAgICAgIC0gJ2t1enpsZV9zZXJ2aWNlc19fc3RvcmFnZUVuZ2luZV9fY2xpZW50X19ub2RlPWh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAnCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19zdG9yYWdlRW5naW5lX19jb21tb25NYXBwaW5nX19keW5hbWljPXRydWUKICAgICAgLSBrdXp6bGVfc2VydmljZXNfX2ludGVybmFsQ2FjaGVfX25vZGVfX2hvc3Q9cmVkaXMKICAgICAgLSBrdXp6bGVfc2VydmljZXNfX21lbW9yeVN0b3JhZ2VfX25vZGVfX2hvc3Q9cmVkaXMKICAgICAgLSBrdXp6bGVfc2VydmVyX19wcm90b2NvbHNfX21xdHRfX2VuYWJsZWQ9dHJ1ZQogICAgICAtIGt1enpsZV9zZXJ2ZXJfX3Byb3RvY29sc19fbXF0dF9fZGV2ZWxvcG1lbnRNb2RlPWZhbHNlCiAgICAgIC0ga3V6emxlX2xpbWl0c19fbG9naW5zUGVyU2Vjb25kPTUwCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdERUJVRz0ke0RFQlVHOi1rdXp6bGU6Y2x1c3RlcjpzeW5jfScKICAgICAgLSAnREVCVUdfREVQVEg9JHtERUJVR19ERVBUSDotMH0nCiAgICAgIC0gJ0RFQlVHX01BWF9BUlJBWV9MRU5HVEg9JHtERUJVR19NQVhfQVJSQVk6LTEwMH0nCiAgICAgIC0gJ0RFQlVHX0VYUEFORD0ke0RFQlVHX0VYUEFORDotb2ZmfScKICAgICAgLSAnREVCVUdfU0hPV19ISURERU49eyRERUJVR19TSE9XX0hJRERFTjotb259JwogICAgICAtICdERUJVR19DT0xPUlM9JHtERUJVR19DT0xPUlM6LW9ufScKICAgIGNhcF9hZGQ6CiAgICAgIC0gU1lTX1BUUkFDRQogICAgdWxpbWl0czoKICAgICAgbm9maWxlOiA2NTUzNgogICAgc3lzY3RsczoKICAgICAgLSBuZXQuY29yZS5zb21heGNvbm49ODE5MgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0Ojc1MTIvX2hlYWx0aGNoZWNrJwogICAgICB0aW1lb3V0OiAxcwogICAgICBpbnRlcnZhbDogMnMKICAgICAgcmV0cmllczogMzAKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGVsYXN0aWNzZWFyY2g6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkK", + "tags": [ + "backend", + "api", + "realtime", + "websocket", + "mqtt", + "rest", + "sdk", + "iot", + "geofencing", + "low-code" + ] + }, "metube": { "documentation": "https:\/\/github.com\/alexta69\/metube", "slogan": "A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.", @@ -179,6 +239,22 @@ "api" ] }, + "moodle": { + "documentation": "https:\/\/moodle.org", + "slogan": "Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.", + "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogIG1vb2RsZToKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWkvbW9vZGxlOjQuMycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NT09ETEUKICAgICAgLSBNT09ETEVfREFUQUJBU0VfSE9TVD1tYXJpYWRiCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1BPUlRfTlVNQkVSPTMzMDYKICAgICAgLSBNT09ETEVfREFUQUJBU0VfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBNT09ETEVfREFUQUJBU0VfTkFNRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSAnTU9PRExFX1VTRVJOQU1FPSR7TU9PRExFX1VTRVJOQU1FOi11c2VyfScKICAgICAgLSBNT09ETEVfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTU9PRExFCiAgICAgIC0gTU9PRExFX0VNQUlMPXVzZXJAZXhhbXBsZS5jb20KICAgICAgLSAnTU9PRExFX1NJVEVfTkFNRT0ke01PT0RMRV9TSVRFX05BTUU6LU5ldyBTaXRlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21vb2RsZS1kYXRhOi9iaXRuYW1pL21vb2RsZScKICAgICAgLSAnbW9vZGxlZGF0YS1kYXRhOi9iaXRuYW1pL21vb2RsZWRhdGEnCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIK", + "tags": [ + "moodle", + "elearning", + "education", + "lms", + "cms", + "open", + "source", + "low", + "code" + ] + }, "n8n-with-postgresql": { "documentation": "https:\/\/docs.n8n.io\/hosting\/", "slogan": "n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model.", @@ -247,6 +323,14 @@ "teamwork" ] }, + "phpmyadmin": { + "documentation": "https:\/\/docs.phpmyadmin.net\/en\/latest\/", + "slogan": "phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface.", + "compose": "c2VydmljZXM6CiAgcGhwbXlhZG1pbjoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9waHBteWFkbWluOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIFBNQV9BUkJJVFJBUlk9MQogICAgICAtIFBNQV9BQlNPTFVURV9VUkk9JFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICB2b2x1bWVzOgogICAgICAtICdwaHBteWFkbWluLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "database management" + ] + }, "pocketbase": { "documentation": "https:\/\/pocketbase.io\/docs\/", "slogan": "Open Source backend for your next SaaS and Mobile app in 1 file", @@ -296,6 +380,24 @@ "real-time" ] }, + "vaultwarden": { + "documentation": "https:\/\/github.com\/dani-garcia\/vaultwarden\/wiki\/FAQs", + "slogan": "Vaultwarden is an open-source password manager that allows you to securely store and manage your passwords, helping you stay organized and protected.", + "compose": "c2VydmljZXM6CiAgdmF1bHR3YXJkZW46CiAgICBpbWFnZTogJ3ZhdWx0d2FyZGVuL3NlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVkFVTFRXQVJERU4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ3ZhdWx0d2FyZGVuLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "password manager", + "security" + ] + }, + "whoogle": { + "documentation": "https:\/\/github.com\/benbusby\/whoogle-search#install", + "slogan": "Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.", + "compose": "c2VydmljZXM6CiAgd2hvb2dsZToKICAgIGltYWdlOiAnYmVuYnVzYnkvd2hvb2dsZS1zZWFyY2g6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dIT09HTEUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo1MDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "privacy", + "search engine" + ] + }, "wordpress-with-mariadb": { "documentation": "https:\/\/wordpress.org\/documentation\/", "slogan": "WordPress with MariaDB. Wordpress is open source software you can use to create a beautiful website, blog, or app.", diff --git a/versions.json b/versions.json index 4592bb435..e61bb360c 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.108" + "version": "4.0.0-beta.109" } } }