Merge pull request #2205 from coollabsio/next

v4.0.0-beta.278
This commit is contained in:
Andras Bacsai 2024-05-16 10:20:00 +02:00 committed by GitHub
commit 5700f2f78a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 716 additions and 562 deletions

View File

@ -35,6 +35,10 @@ # Donations
## Github Sponsors ($40+) ## Github Sponsors ($40+)
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a> <a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a> <a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://www.quantcdn.io/?utm_source=coolify.io"><img src="https://github.com/quantcdn.png" width="60px" alt="QuantCDN"/></a>
<a href="https://www.runpod.io/?utm_source=coolify.io">
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
<a href="https://www.flint.sh/en/home?utm_source=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
<a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a> <a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a> <a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a> <a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
@ -62,6 +66,7 @@ ## Organizations
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals ## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a> <a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>

View File

@ -339,7 +339,7 @@ private function sentinel()
instant_remote_process($connectProxyToDockerNetworks, $this->server, false); instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
return handleError($e); return handleError($e);
} }

View File

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use App\Models\User;
use Illuminate\Console\Command;
class AdminRemoveUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'admin:remove-user {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove User from database';
/**
* Execute the console command.
*/
public function handle()
{
try {
$email = $this->argument('email');
$confirm = $this->confirm('Are you sure you want to remove user with email: ' . $email . '?');
if (!$confirm) {
$this->info('User removal cancelled.');
return;
}
$this->info("Removing user with email: $email");
$user = User::whereEmail($email)->firstOrFail();
$teams = $user->teams;
foreach ($teams as $team) {
if ($team->members->count() > 1) {
$this->error('User is a member of a team with more than one member. Please remove user from team first.');
return;
}
$team->delete();
}
$user->delete();
} catch (\Exception $e) {
$this->error('Failed to remove user.');
$this->error($e->getMessage());
return;
}
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
class Cloud extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:unused-servers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get Unused Servers from Cloud';
/**
* Execute the console command.
*/
public function handle()
{
Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
$this->info($server->name);
});
}
}

View File

@ -29,7 +29,6 @@ class RootResetPassword extends Command
*/ */
public function handle() public function handle()
{ {
//
$this->info('You are about to reset the root password.'); $this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: '); $password = password('Give me a new password for root user: ');
$passwordAgain = password('Again'); $passwordAgain = password('Again');

View File

@ -18,6 +18,7 @@
use App\Models\Team; use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Sleep;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@ -76,13 +77,28 @@ private function check_resources($schedule)
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} }
foreach ($containerServers as $server) { foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyTwoMinutes()->onOneServer()->before(function () {
if (isCloud()) {
$wait = rand(5, 20);
Sleep::for($wait)->seconds();
}
});
if ($server->isLogDrainEnabled()) { if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); $schedule->job(new CheckLogDrainContainerJob($server))->everyTwoMinutes()->onOneServer()->before(function () {
if (isCloud()) {
$wait = rand(5, 20);
Sleep::for($wait)->seconds();
}
});
} }
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ServerStatusJob($server))->everyTwoMinutes()->onOneServer()->before(function () {
if (isCloud()) {
$wait = rand(5, 20);
Sleep::for($wait)->seconds();
}
});
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)
@ -138,7 +154,16 @@ private function check_scheduled_tasks($schedule)
$scheduled_task->delete(); $scheduled_task->delete();
continue; continue;
} }
if ($application) {
if (str($application->status)->contains('running') === false) {
continue;
}
}
if ($service) {
if (str($service->status())->contains('running') === false) {
continue;
}
}
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
} }

View File

@ -47,7 +47,7 @@ public function manual(Request $request)
if ($x_bitbucket_event === 'repo:push') { if ($x_bitbucket_event === 'repo:push') {
$branch = data_get($payload, 'push.changes.0.new.name'); $branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name'); $full_name = data_get($payload, 'repository.full_name');
$commit = data_get($payload, 'push.changes.0.new.target.hash');
if (!$branch) { if (!$branch) {
return response([ return response([
'status' => 'failed', 'status' => 'failed',
@ -104,6 +104,7 @@ public function manual(Request $request)
queue_application_deployment( queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: $commit,
force_rebuild: false, force_rebuild: false,
is_webhook: true is_webhook: true
); );

View File

@ -129,6 +129,7 @@ public function manual(Request $request)
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true, is_webhook: true,
); );
$return_payloads->push([ $return_payloads->push([
@ -177,6 +178,7 @@ public function manual(Request $request)
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true, is_webhook: true,
git_type: 'github' git_type: 'github'
); );
@ -338,6 +340,7 @@ public function normal(Request $request)
queue_application_deployment( queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false, force_rebuild: false,
is_webhook: true, is_webhook: true,
); );
@ -387,6 +390,7 @@ public function normal(Request $request)
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true, is_webhook: true,
git_type: 'github' git_type: 'github'
); );

View File

@ -38,6 +38,15 @@ public function manual(Request $request)
$headers = $request->headers->all(); $headers = $request->headers->all();
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0'); $x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
$x_gitlab_event = data_get($payload, 'object_kind'); $x_gitlab_event = data_get($payload, 'object_kind');
$allowed_events = ['push', 'merge_request'];
if (!in_array($x_gitlab_event, $allowed_events)) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Event not allowed. Only push and merge_request events are allowed.',
]);
return response($return_payloads);
}
if ($x_gitlab_event === 'push') { if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref'); $branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace'); $full_name = data_get($payload, 'project.path_with_namespace');
@ -124,6 +133,7 @@ public function manual(Request $request)
queue_application_deployment( queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false, force_rebuild: false,
is_webhook: true, is_webhook: true,
); );
@ -173,6 +183,7 @@ public function manual(Request $request)
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'object_attributes.last_commit.id', 'HEAD'),
force_rebuild: false, force_rebuild: false,
is_webhook: true, is_webhook: true,
git_type: 'gitlab' git_type: 'gitlab'

View File

@ -107,6 +107,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $fullRepoUrl = null; private ?string $fullRepoUrl = null;
private ?string $branch = null; private ?string $branch = null;
private ?string $coolify_variables = null;
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
@ -406,7 +408,7 @@ private function deploy_docker_compose_buildpack()
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
); );
} }
@ -436,9 +438,9 @@ private function deploy_docker_compose_buildpack()
} else { } else {
$this->write_deployment_configurations(); $this->write_deployment_configurations();
$server_workdir = $this->application->workdir(); $server_workdir = $this->application->workdir();
ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"); ray("{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
$this->execute_remote_command( $this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true], ["{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
); );
} }
} else { } else {
@ -449,7 +451,7 @@ private function deploy_docker_compose_buildpack()
$this->write_deployment_configurations(); $this->write_deployment_configurations();
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
); );
$this->write_deployment_configurations(); $this->write_deployment_configurations();
} }
@ -743,6 +745,16 @@ private function save_environment_variables()
$envs->push("SOURCE_COMMIT=unknown"); $envs->push("SOURCE_COMMIT=unknown");
} }
} }
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$this->application->git_branch}");
}
$envs = $envs->sort(function ($a, $b) { $envs = $envs->sort(function ($a, $b) {
return strpos($a, '$') === false ? -1 : 1; return strpos($a, '$') === false ? -1 : 1;
}); });
@ -777,6 +789,16 @@ private function save_environment_variables()
$envs->push("SOURCE_COMMIT=unknown"); $envs->push("SOURCE_COMMIT=unknown");
} }
} }
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$this->application->git_branch}");
}
$envs = $envs->sort(function ($a, $b) { $envs = $envs->sort(function ($a, $b) {
return strpos($a, '$') === false ? -1 : 1; return strpos($a, '$') === false ? -1 : 1;
}); });
@ -1080,6 +1102,23 @@ private function set_base_dir()
{ {
$this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}."); $this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
} }
private function set_coolify_variables()
{
$this->coolify_variables = "SOURCE_COMMIT={$this->commit} ";
if ($this->pull_request_id === 0) {
$fqdn = $this->application->fqdn;
} else {
$fqdn = $this->preview->fqdn;
}
if (isset($fqdn)) {
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
$url = str($fqdn)->replace('http://', '')->replace('https://', '');
$this->coolify_variables .= "COOLIFY_URL={$url} ";
}
if (isset($this->application->git_branch)) {
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
}
}
private function check_git_if_build_needed() private function check_git_if_build_needed()
{ {
$this->generate_git_import_commands(); $this->generate_git_import_commands();
@ -1113,7 +1152,10 @@ private function check_git_if_build_needed()
} }
if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) { if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
$this->application_deployment_queue->commit = $this->commit;
$this->application_deployment_queue->save();
} }
$this->set_coolify_variables();
} }
private function clone_repository() private function clone_repository()
{ {
@ -1129,6 +1171,18 @@ private function clone_repository()
] ]
); );
$this->create_workdir(); $this->create_workdir();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"),
"hidden" => true,
"save" => "commit_message"
]
);
if ($this->saved_outputs->get('commit_message')) {
$this->application_deployment_queue->commit_message = $this->saved_outputs->get('commit_message');
ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(['commit_message' => $this->saved_outputs->get('commit_message')]);
$this->application_deployment_queue->save();
}
} }
private function generate_git_import_commands() private function generate_git_import_commands()
@ -1277,9 +1331,11 @@ private function generate_compose_file()
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
$labels = $labels->map(function ($value, $key) { if ($this->application->settings->is_container_label_escape_enabled) {
return escapeDollarSign($value); $labels = $labels->map(function ($value, $key) {
}); return escapeDollarSign($value);
});
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
// Check for custom HEALTHCHECK // Check for custom HEALTHCHECK
@ -1767,11 +1823,11 @@ private function build_by_compose_file()
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} build"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), "hidden" => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
); );
} }
$this->application_deployment_queue->addLogEntry("New images built."); $this->application_deployment_queue->addLogEntry("New images built.");
@ -1783,16 +1839,16 @@ private function start_by_compose_file()
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} else { } else {
if ($this->use_build_server) { if ($this->use_build_server) {
$this->execute_remote_command( $this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true], ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
); );
} }
} }

View File

@ -37,336 +37,5 @@ public function uniqueId(): int
public function handle() public function handle()
{ {
GetContainersStatus::run($this->server); GetContainersStatus::run($this->server);
return;
// if (!$this->server->isFunctional()) {
// return 'Server is not ready.';
// };
// $applications = $this->server->applications();
// $skip_these_applications = collect([]);
// foreach ($applications as $application) {
// if ($application->additional_servers->count() > 0) {
// $skip_these_applications->push($application);
// ComplexStatusCheck::run($application);
// $applications = $applications->filter(function ($value, $key) use ($application) {
// return $value->id !== $application->id;
// });
// }
// }
// $applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
// return !$skip_these_applications->pluck('id')->contains($value->id);
// });
// try {
// if ($this->server->isSwarm()) {
// $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
// $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
// } else {
// // Precheck for containers
// $containers = instant_remote_process(["docker container ls -q"], $this->server, false);
// if (!$containers) {
// return;
// }
// $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
// $containerReplicates = null;
// }
// if (is_null($containers)) {
// return;
// }
// $containers = format_docker_command_output_to_json($containers);
// if ($containerReplicates) {
// $containerReplicates = format_docker_command_output_to_json($containerReplicates);
// foreach ($containerReplicates as $containerReplica) {
// $name = data_get($containerReplica, 'Name');
// $containers = $containers->map(function ($container) use ($name, $containerReplica) {
// if (data_get($container, 'Spec.Name') === $name) {
// $replicas = data_get($containerReplica, 'Replicas');
// $running = str($replicas)->explode('/')[0];
// $total = str($replicas)->explode('/')[1];
// if ($running === $total) {
// data_set($container, 'State.Status', 'running');
// data_set($container, 'State.Health.Status', 'healthy');
// } else {
// data_set($container, 'State.Status', 'starting');
// data_set($container, 'State.Health.Status', 'unhealthy');
// }
// }
// return $container;
// });
// }
// }
// $databases = $this->server->databases();
// $services = $this->server->services()->get();
// $previews = $this->server->previews();
// $foundApplications = [];
// $foundApplicationPreviews = [];
// $foundDatabases = [];
// $foundServices = [];
// foreach ($containers as $container) {
// if ($this->server->isSwarm()) {
// $labels = data_get($container, 'Spec.Labels');
// $uuid = data_get($labels, 'coolify.name');
// } else {
// $labels = data_get($container, 'Config.Labels');
// }
// $containerStatus = data_get($container, 'State.Status');
// $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
// $containerStatus = "$containerStatus ($containerHealth)";
// $labels = Arr::undot(format_docker_labels_to_json($labels));
// $applicationId = data_get($labels, 'coolify.applicationId');
// if ($applicationId) {
// $pullRequestId = data_get($labels, 'coolify.pullRequestId');
// if ($pullRequestId) {
// if (str($applicationId)->contains('-')) {
// $applicationId = str($applicationId)->before('-');
// }
// $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
// if ($preview) {
// $foundApplicationPreviews[] = $preview->id;
// $statusFromDb = $preview->status;
// if ($statusFromDb !== $containerStatus) {
// $preview->update(['status' => $containerStatus]);
// }
// } else {
// //Notify user that this container should not be there.
// }
// } else {
// $application = $applications->where('id', $applicationId)->first();
// if ($application) {
// $foundApplications[] = $application->id;
// $statusFromDb = $application->status;
// if ($statusFromDb !== $containerStatus) {
// $application->update(['status' => $containerStatus]);
// }
// } else {
// //Notify user that this container should not be there.
// }
// }
// } else {
// $uuid = data_get($labels, 'com.docker.compose.service');
// $type = data_get($labels, 'coolify.type');
// if ($uuid) {
// if ($type === 'service') {
// $database_id = data_get($labels, 'coolify.service.subId');
// if ($database_id) {
// $service_db = ServiceDatabase::where('id', $database_id)->first();
// if ($service_db) {
// $uuid = $service_db->service->uuid;
// $isPublic = data_get($service_db, 'is_public');
// if ($isPublic) {
// $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'Name') === "/$uuid-proxy";
// }
// })->first();
// if (!$foundTcpProxy) {
// StartDatabaseProxy::run($service_db);
// // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
// }
// }
// }
// }
// } else {
// $database = $databases->where('uuid', $uuid)->first();
// if ($database) {
// $isPublic = data_get($database, 'is_public');
// $foundDatabases[] = $database->id;
// $statusFromDb = $database->status;
// if ($statusFromDb !== $containerStatus) {
// $database->update(['status' => $containerStatus]);
// }
// if ($isPublic) {
// $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'Name') === "/$uuid-proxy";
// }
// })->first();
// if (!$foundTcpProxy) {
// StartDatabaseProxy::run($database);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// }
// }
// } else {
// // Notify user that this container should not be there.
// }
// }
// }
// if (data_get($container, 'Name') === '/coolify-db') {
// $foundDatabases[] = 0;
// }
// }
// $serviceLabelId = data_get($labels, 'coolify.serviceId');
// if ($serviceLabelId) {
// $subType = data_get($labels, 'coolify.service.subType');
// $subId = data_get($labels, 'coolify.service.subId');
// $service = $services->where('id', $serviceLabelId)->first();
// if (!$service) {
// continue;
// }
// if ($subType === 'application') {
// $service = $service->applications()->where('id', $subId)->first();
// } else {
// $service = $service->databases()->where('id', $subId)->first();
// }
// if ($service) {
// $foundServices[] = "$service->id-$service->name";
// $statusFromDb = $service->status;
// if ($statusFromDb !== $containerStatus) {
// // ray('Updating status: ' . $containerStatus);
// $service->update(['status' => $containerStatus]);
// }
// }
// }
// }
// $exitedServices = collect([]);
// foreach ($services as $service) {
// $apps = $service->applications()->get();
// $dbs = $service->databases()->get();
// foreach ($apps as $app) {
// if (in_array("$app->id-$app->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($app);
// }
// }
// foreach ($dbs as $db) {
// if (in_array("$db->id-$db->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($db);
// }
// }
// }
// $exitedServices = $exitedServices->unique('id');
// foreach ($exitedServices as $exitedService) {
// if (str($exitedService->status)->startsWith('exited')) {
// continue;
// }
// $name = data_get($exitedService, 'name');
// $fqdn = data_get($exitedService, 'fqdn');
// $containerName = $name ? "$name, available at $fqdn" : $fqdn;
// $projectUuid = data_get($service, 'environment.project.uuid');
// $serviceUuid = data_get($service, 'uuid');
// $environmentName = data_get($service, 'environment.name');
// if ($projectUuid && $serviceUuid && $environmentName) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// $exitedService->update(['status' => 'exited']);
// }
// $notRunningApplications = $applications->pluck('id')->diff($foundApplications);
// foreach ($notRunningApplications as $applicationId) {
// $application = $applications->where('id', $applicationId)->first();
// if (str($application->status)->startsWith('exited')) {
// continue;
// }
// $application->update(['status' => 'exited']);
// $name = data_get($application, 'name');
// $fqdn = data_get($application, 'fqdn');
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
// $projectUuid = data_get($application, 'environment.project.uuid');
// $applicationUuid = data_get($application, 'uuid');
// $environment = data_get($application, 'environment.name');
// if ($projectUuid && $applicationUuid && $environment) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
// foreach ($notRunningApplicationPreviews as $previewId) {
// $preview = $previews->where('id', $previewId)->first();
// if (str($preview->status)->startsWith('exited')) {
// continue;
// }
// $preview->update(['status' => 'exited']);
// $name = data_get($preview, 'name');
// $fqdn = data_get($preview, 'fqdn');
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
// $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;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
// foreach ($notRunningDatabases as $database) {
// $database = $databases->where('id', $database)->first();
// if (str($database->status)->startsWith('exited')) {
// continue;
// }
// $database->update(['status' => 'exited']);
// $name = data_get($database, 'name');
// $fqdn = data_get($database, 'fqdn');
// $containerName = $name;
// $projectUuid = data_get($database, 'environment.project.uuid');
// $environmentName = data_get($database, 'environment.name');
// $databaseUuid = data_get($database, 'uuid');
// if ($projectUuid && $databaseUuid && $environmentName) {
// $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// // Check if proxy is running
// $this->server->proxyType();
// $foundProxyContainer = $containers->filter(function ($value, $key) {
// if ($this->server->isSwarm()) {
// return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
// } else {
// return data_get($value, 'Name') === '/coolify-proxy';
// }
// })->first();
// if (!$foundProxyContainer) {
// try {
// $shouldStart = CheckProxy::run($this->server);
// if ($shouldStart) {
// StartProxy::run($this->server, false);
// $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
// }
// } catch (\Throwable $e) {
// ray($e);
// }
// } else {
// $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
// $this->server->save();
// $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
// instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
// }
// } catch (\Throwable $e) {
// send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
// ray($e->getMessage());
// return handleError($e);
// }
} }
} }

View File

@ -77,8 +77,12 @@ public function handle(): void
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'); $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
} }
}); });
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
$this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
} }
if (count($this->containers) == 0) { if (count($this->containers) == 0) {
throw new \Exception('ScheduledTaskJob failed: No containers running.'); throw new \Exception('ScheduledTaskJob failed: No containers running.');
} }
@ -89,7 +93,7 @@ public function handle(): void
foreach ($this->containers as $containerName) { foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) { if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
$cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"'; $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
$exec = "docker exec {$containerName} {$cmd}"; $exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true); $this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([ $this->task_log->update([

View File

@ -40,7 +40,7 @@ public function handle()
try { try {
$servers = $this->team->servers; $servers = $this->team->servers;
$servers_count = $servers->count(); $servers_count = $servers->count();
$limit = $this->team->limits['serverLimit']; $limit = data_get($this->team->limits, 'serverLimit', 2);
$number_of_servers_to_disable = $servers_count - $limit; $number_of_servers_to_disable = $servers_count - $limit;
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable); ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
if ($number_of_servers_to_disable > 0) { if ($number_of_servers_to_disable > 0) {

View File

@ -22,6 +22,7 @@ class General extends Component
public ?string $git_commit_sha = null; public ?string $git_commit_sha = null;
public string $build_pack; public string $build_pack;
public ?string $ports_exposes = null; public ?string $ports_exposes = null;
public bool $is_container_label_escape_enabled = true;
public $customLabels; public $customLabels;
public bool $labelsChanged = false; public bool $labelsChanged = false;
@ -74,6 +75,7 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable', 'application.post_deployment_command_container' => 'nullable',
'application.settings.is_static' => 'boolean|required', 'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable', 'application.watch_paths' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@ -109,6 +111,7 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_static' => 'Is static', 'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.watch_paths' => 'Watch paths', 'application.watch_paths' => 'Watch paths',
]; ];
public function mount() public function mount()
@ -124,6 +127,7 @@ public function mount()
} }
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
@ -145,7 +149,7 @@ public function instantSave()
$this->application->settings->save(); $this->application->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
$this->application->refresh(); $this->application->refresh();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
} }
} }
@ -204,6 +208,9 @@ public function generateDomain(string $serviceName)
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save(); $this->application->save();
$this->dispatch('success', 'Domain generated.'); $this->dispatch('success', 'Domain generated.');
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
return $domain; return $domain;
} }
public function updatedApplicationBaseDirectory() public function updatedApplicationBaseDirectory()
@ -257,9 +264,12 @@ public function resetDefaultLabels()
{ {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes; $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->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
} }
public function checkFqdns($showToaster = true) public function checkFqdns($showToaster = true)
@ -300,11 +310,11 @@ public function submit($showToaster = true)
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$compose_return = $this->loadComposeFile(); $compose_return = $this->loadComposeFile();
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return; return;
} }
} }
$this->validate(); $this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(); $this->resetDefaultLabels();
} }
if (data_get($this->application, 'build_pack') === 'dockerimage') { if (data_get($this->application, 'build_pack') === 'dockerimage') {

View File

@ -3,7 +3,6 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use Livewire\Component; use Livewire\Component;

View File

@ -12,6 +12,7 @@ class EditCompose extends Component
protected $rules = [ protected $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.is_container_label_escape_enabled' => 'required',
]; ];
public function mount() public function mount()
{ {
@ -23,6 +24,14 @@ public function saveEditedCompose()
$this->dispatch('info', "Saving new docker compose..."); $this->dispatch('info', "Saving new docker compose...");
$this->dispatch('saveCompose', $this->service->docker_compose_raw); $this->dispatch('saveCompose', $this->service->docker_compose_raw);
} }
public function instantSave()
{
$this->validate([
'service.is_container_label_escape_enabled' => 'required',
]);
$this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
$this->dispatch('success', "Service updated successfully");
}
public function render() public function render()
{ {
return view('livewire.project.service.edit-compose'); return view('livewire.project.service.edit-compose');

View File

@ -2,11 +2,14 @@
namespace App\Livewire\Project\Shared\ScheduledTask; namespace App\Livewire\Project\Shared\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Add extends Component class Add extends Component
{ {
public $parameters; public $parameters;
public string $type;
public Collection $containerNames;
public string $name; public string $name;
public string $command; public string $command;
public string $frequency; public string $frequency;
@ -29,6 +32,9 @@ class Add extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if ($this->containerNames->count() > 0) {
$this->container = $this->containerNames->first();
}
} }
public function submit() public function submit()
@ -40,6 +46,11 @@ public function submit()
$this->dispatch('error', 'Invalid Cron / Human expression.'); $this->dispatch('error', 'Invalid Cron / Human expression.');
return; return;
} }
if (empty($this->container) || $this->container == 'null') {
if ($this->type == 'service') {
$this->container = $this->subServiceName;
}
}
$this->dispatch('saveScheduledTask', [ $this->dispatch('saveScheduledTask', [
'name' => $this->name, 'name' => $this->name,
'command' => $this->command, 'command' => $this->command,

View File

@ -3,14 +3,13 @@
namespace App\Livewire\Project\Shared\ScheduledTask; namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class All extends Component class All extends Component
{ {
public $resource; public $resource;
public string|null $modalId = null; public Collection $containerNames;
public ?string $variables = null; public ?string $variables = null;
public array $parameters; public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit']; protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
@ -18,7 +17,18 @@ class All extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->modalId = new Cuid2(7); if ($this->resource->type() == 'service') {
$this->containerNames = $this->resource->applications()->pluck('name');
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
} elseif ($this->resource->type() == 'application') {
if ($this->resource->build_pack === 'dockercompose') {
$parsed = $this->resource->parseCompose();
$containers = collect($parsed['services'])->keys();
$this->containerNames = $containers;
} else {
$this->containerNames = collect([]);
}
}
} }
public function refreshTasks() public function refreshTasks()
{ {

View File

@ -17,6 +17,7 @@ class Show extends Component
public string $type; public string $type;
protected $rules = [ protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string', 'task.name' => 'required|string',
'task.command' => 'required|string', 'task.command' => 'required|string',
'task.frequency' => 'required|string', 'task.frequency' => 'required|string',
@ -45,9 +46,18 @@ public function mount()
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
} }
public function instantSave()
{
$this->validateOnly('task.enabled');
$this->task->save(['enabled' => $this->task->enabled]);
$this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks');
}
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
$this->task->name = str($this->task->name)->trim()->value();
$this->task->container = str($this->task->container)->trim()->value();
$this->task->save(); $this->task->save();
$this->dispatch('success', 'Scheduled task updated.'); $this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks'); $this->dispatch('refreshTasks');
@ -60,11 +70,9 @@ public function delete()
if ($this->type == 'application') { if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters); return redirect()->route('project.application.configuration', $this->parameters);
} } else {
else {
return redirect()->route('project.service.configuration', $this->parameters); return redirect()->route('project.service.configuration', $this->parameters);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e); return handleError($e);
} }

View File

@ -146,9 +146,13 @@ public function gitBranchLocation(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
} }
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/tree/{$this->git_branch}";
}
return $this->git_repository; return $this->git_repository;
} }
); );
} }
@ -159,6 +163,11 @@ public function gitWebhook(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
} }
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/settings/hooks";
}
return $this->git_repository; return $this->git_repository;
} }
); );
@ -171,10 +180,26 @@ public function gitCommits(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}"; return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
} }
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commits/{$this->git_branch}";
}
return $this->git_repository; return $this->git_repository;
} }
); );
} }
public function gitCommitLink($link): string
{
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
}
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commit/{$link}";
}
return $this->git_repository;
}
public function dockerfileLocation(): Attribute public function dockerfileLocation(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function setStatus(string $status) { public function setStatus(string $status)
{
$this->update([ $this->update([
'status' => $status, 'status' => $status,
]); ]);
@ -21,7 +22,13 @@ public function getOutput($name)
} }
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null; return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
} }
public function commitMessage()
{
if (empty($this->commit_message) || is_null($this->commit_message)) {
return null;
}
return str($this->commit_message)->trim()->limit(50)->value();
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false) public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{ {
if ($type === 'error') { if ($type === 'error') {

View File

@ -450,14 +450,16 @@ public function extraFields()
$data = collect([]); $data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
$data = $data->merge([ if ($admin_user) {
'User' => [ $data = $data->merge([
'key' => 'SERVICE_USER_ADMIN', 'User' => [
'value' => data_get($admin_user, 'value', 'admin'), 'key' => 'SERVICE_USER_ADMIN',
'readonly' => true, 'value' => data_get($admin_user, 'value', 'admin'),
'rules' => 'required', 'readonly' => true,
], 'rules' => 'required',
]); ],
]);
}
if ($admin_password) { if ($admin_password) {
$data = $data->merge([ $data = $data->merge([
'Password' => [ 'Password' => [
@ -677,6 +679,17 @@ public function server()
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
} }
public function byUuid(string $uuid) {
$app = $this->applications()->whereUuid($uuid)->first();
if ($app) {
return $app;
}
$db = $this->databases()->whereUuid($uuid)->first();
if ($db) {
return $db;
}
return null;
}
public function byName(string $name) public function byName(string $name)
{ {
$app = $this->applications()->whereName($name)->first(); $app = $this->applications()->whereName($name)->first();

View File

@ -183,6 +183,7 @@ public function role()
if (data_get($this, 'pivot')) { if (data_get($this, 'pivot')) {
return $this->pivot->role; return $this->pivot->role;
} }
return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role; $user = auth()->user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
} }
} }

View File

@ -6,7 +6,6 @@
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Spatie\Url\Url; use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)

View File

@ -1174,6 +1174,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
] ]
]); ]);
} }
if ($serviceLabels->count() > 0) {
if ($resource->is_container_label_escape_enabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
if (!data_get($service, 'restart')) { if (!data_get($service, 'restart')) {
@ -1259,6 +1266,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$servicePorts = collect(data_get($service, 'ports', [])); $servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', [])); $serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', [])); $serviceVariables = collect(data_get($service, 'environment', []));
$serviceDependencies = collect(data_get($service, 'depends_on', []));
$serviceLabels = collect(data_get($service, 'labels', [])); $serviceLabels = collect(data_get($service, 'labels', []));
$serviceBuildVariables = collect(data_get($service, 'build.args', [])); $serviceBuildVariables = collect(data_get($service, 'build.args', []));
$serviceVariables = $serviceVariables->merge($serviceBuildVariables); $serviceVariables = $serviceVariables->merge($serviceBuildVariables);
@ -1275,11 +1283,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels->push("$removedLabelName=$removedLabel"); $serviceLabels->push("$removedLabelName=$removedLabel");
} }
} }
if ($serviceLabels->count() > 0) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
$baseName = generateApplicationContainerName($resource, $pull_request_id); $baseName = generateApplicationContainerName($resource, $pull_request_id);
$containerName = "$serviceName-$baseName"; $containerName = "$serviceName-$baseName";
if (count($serviceVolumes) > 0) { if (count($serviceVolumes) > 0) {
@ -1370,6 +1374,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray()); data_set($service, 'volumes', $serviceVolumes->toArray());
} }
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
$serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
return $dependency . "-pr-$pull_request_id";
});
data_set($service, 'depends_on', $serviceDependencies->toArray());
}
// Decide if the service is a database // Decide if the service is a database
$isDatabase = isDatabaseImage(data_get_str($service, 'image')); $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
data_set($service, 'is_database', $isDatabase); data_set($service, 'is_database', $isDatabase);
@ -1652,6 +1663,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
] ]
]); ]);
} }
if ($serviceLabels->count() > 0) {
if ($resource->settings->is_container_label_escape_enabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
if (!data_get($service, 'restart')) { if (!data_get($service, 'restart')) {

View File

@ -7,7 +7,7 @@
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.277', 'release' => '4.0.0-beta.278',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.277'; return '4.0.0-beta.278';

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->longText('stripe_comment')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_comment')->change();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->string('commit_message', 50)->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('commit_message');
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_container_label_escape_enabled')->default(true);
});
Schema::table('services', function (Blueprint $table) {
$table->boolean('is_container_label_escape_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_container_label_escape_enabled');
});
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('is_container_label_escape_enabled');
});
}
};

View File

@ -2,15 +2,15 @@ FROM alpine:3.17
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=24.0.9 ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.25.0 ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.13.1 ARG DOCKER_BUILDX_VERSION=0.14.0
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.33.2 ARG PACK_VERSION=0.33.2
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.21.2 ARG NIXPACKS_VERSION=1.21.3
USER root USER root
WORKDIR /artifacts WORKDIR /artifacts

View File

@ -2,7 +2,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases # https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.2.1 ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15 ARG POSTGRES_VERSION=15
RUN apt-get update RUN apt-get update

View File

@ -15,7 +15,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases # https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.2.1 ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15 ARG POSTGRES_VERSION=15
WORKDIR /var/www/html WORKDIR /var/www/html

View File

@ -2,11 +2,11 @@ FROM debian:12-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=24.0.5 ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.21.0 ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.11.2 ARG DOCKER_BUILDX_VERSION=0.14.0
USER root USER root
WORKDIR /root WORKDIR /root

1
public/svgs/twenty.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" height="136" viewBox="0 0 136 136" width="136" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><rect height="136" rx="16" width="136"/></clipPath><g clip-path="url(#a)"><path d="m136 .00002289h-136l.00014448 135.99997711h135.99985552zm-108.73 50.64007711c0-7.43 6.03-13.46 13.46-13.46h25.91c.38 0 .73.23.89.58s.09.76-.17 1.05l-5.68 6.17c-.99 1.07-2.38 1.69-3.84 1.69h-17.04c-2.23 0-4.04 1.81-4.04 4.04v10.18c0 1.31-1.06 2.37-2.37 2.37h-4.74c-1.31 0-2.37-1.06-2.37-2.37v-10.25zm80.61 34.72c0 7.43-6.03 13.4599-13.46 13.4599h-11.01c-7.43 0-13.46-6.0299-13.46-13.4599v-19.27c0-1.31.49-2.57 1.38-3.54l6.42-6.97c.27-.29.69-.39 1.07-.25.37.15.62.4999.62.8999v29.0701c0 2.23 1.81 4.04 4.04 4.04h10.88c2.23 0 4.04-1.81 4.04-4.04v-34.59c0-2.23-1.81-4.04-4.04-4.04h-12.65c-1.45 0-2.83.61-3.82 1.67l-37.73 41h22.67c1.31 0 2.37 1.06 2.37 2.37v4.74c0 1.31-1.06 2.3699-2.37 2.3699h-30.55c-2.77 0-5.02-2.2499-5.02-5.0199v-2.5101c0-1.26.47-2.4699 1.33-3.3999l42.3-45.95c2.8-3.04 6.73-4.76 10.86-4.76h12.66c7.43 0 13.46 6.03 13.46 13.46v34.72z" fill="#000"/><g fill="#fff"><path d="m27.27 50.6401c0-7.43 6.03-13.46 13.46-13.46h25.91c.38 0 .73.23.89.58s.09.76-.17 1.05l-5.68 6.17c-.99 1.07-2.38 1.69-3.84 1.69h-17.04c-2.23 0-4.04 1.81-4.04 4.04v10.18c0 1.31-1.06 2.37-2.37 2.37h-4.74c-1.31 0-2.37-1.06-2.37-2.37v-10.25z"/><path d="m107.88 85.3601c0 7.43-6.03 13.4599-13.46 13.4599h-11.01c-7.43 0-13.46-6.0299-13.46-13.4599v-19.27c0-1.31.49-2.57 1.38-3.54l6.42-6.97c.27-.29.69-.39 1.07-.25.37.15.62.4999.62.8999v29.0701c0 2.23 1.81 4.04 4.04 4.04h10.88c2.23 0 4.04-1.81 4.04-4.04v-34.59c0-2.23-1.81-4.04-4.04-4.04h-12.65c-1.45 0-2.83.61-3.82 1.67l-37.73 41h22.67c1.31 0 2.37 1.06 2.37 2.37v4.74c0 1.31-1.06 2.3699-2.37 2.3699h-30.55c-2.77 0-5.02-2.2499-5.02-5.0199v-2.5101c0-1.26.47-2.4699 1.33-3.3999l42.3-45.95c2.8-3.04 6.73-4.76 10.86-4.76h12.66c7.43 0 13.46 6.03 13.46 13.46v34.72z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -11,22 +11,22 @@
<div class="grid gap-2 lg:grid-cols-1"> <div class="grid gap-2 lg:grid-cols-1">
@forelse ($destinations as $destination) @forelse ($destinations as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="box group"> <a class="box group"
<a class="flex flex-col mx-6"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"> href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="box-title">{{ $destination->name }}</div> <div class="flex flex-col mx-6">
<div class="box-description">server: {{ $destination->server->name }}</div> <div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a> </a>
</div>
@endif @endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker') @if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<div class="box group"> <a class="box group"
<a class="flex flex-col mx-6"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"> href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="box-title">{{ $destination->name }}</div> <div class="flex flex-col mx-6">
<div class="box-description">server: {{ $destination->server->name }}</div> <div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a> </a>
</div>
@endif @endif
@empty @empty
<div> <div>

View File

@ -57,8 +57,7 @@ class="flex flex-col items-center justify-center p-10 mx-2 mt-10 bg-white border
Localhost is not reachable with the following public key. Localhost is not reachable with the following public key.
<br /> <br /> <br /> <br />
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for
user user or skip the boarding process and add a new private key manually to Coolify and to the
'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<br /> <br />
Check this <a target="_blank" class="underline" Check this <a target="_blank" class="underline"
@ -146,9 +145,12 @@ class="flex flex-col items-center justify-center p-10 mx-2 mt-10 bg-white border
This server is not reachable with the following public key. This server is not reachable with the following public key.
<br /> <br /> <br /> <br />
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for
user user or skip the boarding process and add a new private key manually to Coolify and to the
'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<br />
Check this <a target="_blank" class="underline"
href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further
help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="w-64 box-boarding" wire:target="validateServer" <x-forms.button class="w-64 box-boarding" wire:target="validateServer"
wire:click="validateServer">Check wire:click="validateServer">Check
@ -229,10 +231,6 @@ class="font-bold underline" target="_blank"
<x-forms.button type="submit">Continue</x-forms.button> <x-forms.button type="submit">Continue</x-forms.button>
</form> </form>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation>
<p>Username should be <x-highlighted text="root" /> for now. We are working on to use
non-root users.</p>
</x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'validate-server') @elseif ($currentState === 'validate-server')
<x-boarding-step title="Validate & Configure Server"> <x-boarding-step title="Validate & Configure Server">

View File

@ -15,7 +15,7 @@
checkPusherInterval = setInterval(() => { checkPusherInterval = setInterval(() => {
if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') { if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') {
checkNumber++; checkNumber++;
if (checkNumber > 4) { if (checkNumber > 5) {
this.popups.realtime = true; this.popups.realtime = true;
console.error( console.error(
'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)' 'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)'
@ -23,31 +23,36 @@
clearInterval(checkPusherInterval); clearInterval(checkPusherInterval);
} }
} }
}, 1000); }, 2000);
} }
} }
}"> }">
@auth @auth
<span x-show="popups.realtime === true"> <span x-show="popups.realtime === true">
<x-popup> @if (!isCloud())
<x-slot:title> <x-popup>
<span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?! <x-slot:title>
</x-slot:title> <span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?!
<x-slot:description> </x-slot:title>
<span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the UI <x-slot:description>
if <span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the
not fixed! <br><br> UI
Please ensure that you have opened the if
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall' target='_blank'>required ports</a>, not fixed! <br><br>
check the Please ensure that you have opened the
related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels' <a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>documentation</a> or get target='_blank'>required ports</a>,
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>. </span> check the
</x-slot:description> related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels'
<x-slot:button-text @click="disableRealtime()"> target='_blank'>documentation</a> or get
Acknowledge & Disable This Popup help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
</x-slot:button-text> </span>
</x-popup> </x-slot:description>
<x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup
</x-slot:button-text>
</x-popup>
@endif
</span> </span>
@endauth @endauth
<span x-show="popups.sponsorship"> <span x-show="popups.sponsorship">

View File

@ -27,18 +27,15 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
</form> </form>
@endif @endif
@forelse ($deployments as $deployment) @forelse ($deployments as $deployment)
<a @class([ <div @class([
'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col', 'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200',
'dark:hover:bg-coolgray-200' => 'border-warning' =>
data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' =>
data_get($deployment, 'status') === 'in_progress' || data_get($deployment, 'status') === 'in_progress' ||
data_get($deployment, 'status') === 'cancelled-by-user', data_get($deployment, 'status') === 'cancelled-by-user',
'border-error dark:hover:bg-error hover:bg-neutral-200' => 'border-error' => data_get($deployment, 'status') === 'failed',
data_get($deployment, 'status') === 'failed', 'border-success' => data_get($deployment, 'status') === 'finished',
'border-success dark:hover:bg-success hover:bg-neutral-200' => ])
data_get($deployment, 'status') === 'finished', x-on:click.stop="goto('{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}')">
]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}">
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<div class="flex gap-1"> <div class="flex gap-1">
{{ $deployment->created_at }} UTC {{ $deployment->created_at }} UTC
@ -64,11 +61,27 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@endif @endif
</div> </div>
@else @else
<div class="flex gap-1"> <div class="flex items-center gap-1">
Manual @if (data_get($deployment, 'rollback') === true)
Rollback
@else
Manual
@endif
@if (data_get($deployment, 'commit'))
<div class="dark:hover:text-white"
x-on:click.stop="goto('{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}')">
<div class="text-xs underline">
@if ($deployment->commitMessage())
({{data_get_str($deployment, 'commit')->limit(7)}} - {{ $deployment->commitMessage() }})
@else
{{ data_get_str($deployment, 'commit')->limit(7) }}
@endif
</div>
</div>
@endif
</div> </div>
@endif @endif
@if (data_get($deployment, 'server_name')) @if (data_get($deployment, 'server_name') && $application->additional_servers->count() > 0)
<div class="flex gap-1"> <div class="flex gap-1">
Server: {{ data_get($deployment, 'server_name') }} Server: {{ data_get($deployment, 'server_name') }}
</div> </div>
@ -85,15 +98,19 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<span class="font-bold" x-text="measure_finished_time()">0s</span> <span class="font-bold" x-text="measure_finished_time()">0s</span>
</div> </div>
</div> </div>
</a> </div>
@empty @empty
<div class="">No deployments found</div> <div class="">No deployments found</div>
@endforelse @endforelse
@if ($deployments_count > 0) @if ($deployments_count > 0)
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.js"></script> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script>
<script> <script>
function goto(url) {
window.location.href = url;
};
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
let timers = {}; let timers = {};

View File

@ -219,6 +219,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
<x-forms.textarea rows="10" readonly id="application.docker_compose" <x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." /> label="Docker Compose Content" helper="You need to modify the docker compose file." />
@endif @endif
<div class="w-72">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="application.settings.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr" {{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}} label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif @endif
@ -242,8 +247,15 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif @endif
</div> </div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea> <x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-modal-confirmation buttonFullWidth action="resetDefaultLabels" buttonTitle="Reset to Coolify Generated Labels"> <div class="w-72">
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy configuration after you restart the container. <x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="application.settings.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
<x-modal-confirmation buttonFullWidth action="resetDefaultLabels"
buttonTitle="Reset to Coolify Generated Labels">
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy
configuration after you restart the container.
</x-modal-confirmation> </x-modal-confirmation>
@endif @endif

View File

@ -19,22 +19,25 @@
<h2 class="pt-4 pb-4">Select a Github App</h2> <h2 class="pt-4 pb-4">Select a Github App</h2>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@if ($current_step === 'github_apps') @if ($current_step === 'github_apps')
<div class="flex flex-row justify-center gap-2 text-left"> <div class="flex flex-col justify-center gap-2 text-left">
@foreach ($github_apps as $ghapp) @foreach ($github_apps as $ghapp)
<div class="w-full gap-2 py-4 bg-white cursor-pointer group hover:bg-coollabs dark:bg-coolgray-200 box" <div class="flex">
wire:click.prevent="loadRepositories({{ $ghapp->id }})" wire:key="{{ $ghapp->id }}"> <div class="w-full gap-2 py-4 bg-white cursor-pointer group hover:bg-coollabs dark:bg-coolgray-200 box"
<div class="flex mr-4"> wire:click.prevent="loadRepositories({{ $ghapp->id }})"
<div class="flex flex-col mx-6"> wire:key="{{ $ghapp->id }}">
<div class="box-title"> <div class="flex mr-4">
{{ data_get($ghapp, 'name') }} <div class="flex flex-col mx-6">
<div class="box-title">
{{ data_get($ghapp, 'name') }}
</div>
<div class="box-description">
{{ data_get($ghapp, 'html_url') }}</div>
</div> </div>
<div class="box-description">
{{ data_get($ghapp, 'html_url') }}</div>
</div> </div>
</div> </div>
</div> <div class="flex flex-col items-center justify-center">
<div class="flex flex-col items-center justify-center"> <x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" />
<x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" /> </div>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@ -16,6 +16,10 @@
@click.prevent="activeTab = 'storages'; @click.prevent="activeTab = 'storages';
window.location.hash = 'storages'" window.location.hash = 'storages'"
href="#">Storages</a> href="#">Storages</a>
<a class="menu-item" :class="activeTab === 'scheduled-tasks' && 'menu-item-active'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks
</a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'execute-command' && 'menu-item-active'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'execute-command' && 'menu-item-active'"
@click.prevent="activeTab = 'execute-command'; @click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'" window.location.hash = 'execute-command'"
@ -103,10 +107,13 @@ class="w-4 h-4 dark:text-warning text-coollabs"
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $application->uuid]) }}"> href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $application->uuid]) }}">
Settings Settings
</a> </a>
<x-modal-confirmation action="restartApplication({{ $application->id }})" @if (str($application->status)->contains('running'))
isErrorButton buttonTitle="Restart"> <x-modal-confirmation action="restartApplication({{ $application->id }})"
This application will be unavailable during the restart. <br>Please think again. isErrorButton buttonTitle="Restart">
</x-modal-confirmation> This application will be unavailable during the restart. <br>Please think
again.
</x-modal-confirmation>
@endif
</div> </div>
</div> </div>
</div> </div>
@ -169,6 +176,9 @@ class="w-4 h-4 dark:text-warning text-coollabs"
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" /> <livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" />
@endforeach @endforeach
</div> </div>
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'webhooks'"> <div x-cloak x-show="activeTab === 'webhooks'">
<livewire:project.shared.webhooks :resource="$service" /> <livewire:project.shared.webhooks :resource="$service" />
</div> </div>

View File

@ -3,6 +3,11 @@
prevent prevent
name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage
menu.</div> menu.</div>
<div class="pb-2 w-72">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="service.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
</div>
<div x-cloak x-show="raw" class="font-mono"> <div x-cloak x-show="raw" class="font-mono">
<x-forms.textarea allowTab rows="20" id="service.docker_compose_raw"> <x-forms.textarea allowTab rows="20" id="service.docker_compose_raw">
</x-forms.textarea> </x-forms.textarea>
@ -14,10 +19,10 @@
<div class="flex justify-end w-full gap-2 pt-4"> <div class="flex justify-end w-full gap-2 pt-4">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<div x-cloak x-show="raw"> <div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button> <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div> </div>
<div x-cloak x-show="raw === false"> <div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button> Compose</x-forms.button>
</div> </div>
</div> </div>

View File

@ -10,14 +10,6 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
<a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages
</a>
<a class="menu-item" :class="activeTab === 'scheduled-tasks' && 'menu-item-active'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks
</a>
@if (str($serviceDatabase?->databaseType())->contains('mysql') || @if (str($serviceDatabase?->databaseType())->contains('mysql') ||
str($serviceDatabase?->databaseType())->contains('postgres') || str($serviceDatabase?->databaseType())->contains('postgres') ||
str($serviceDatabase?->databaseType())->contains('mariadb')) str($serviceDatabase?->databaseType())->contains('mariadb'))
@ -30,28 +22,12 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.service-application-view :application="$serviceApplication" /> <livewire:project.service.service-application-view :application="$serviceApplication" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="dark:text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceApplication->id }}"
:resource="$serviceApplication" />
</div>
@endisset @endisset
@isset($serviceDatabase) @isset($serviceDatabase)
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="dark:text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}" :resource="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'backups'"> <div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 "> <div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2> <h2 class="pb-4">Scheduled Backups</h2>
@ -62,9 +38,6 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
<livewire:project.database.scheduled-backups :database="$serviceDatabase" /> <livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div> </div>
@endisset @endisset
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,27 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Run cron" id="name" label="Name" /> <x-forms.input autofocus placeholder="Run cron" id="name" label="Name" />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" /> <x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" />
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" /> <x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" />
<x-forms.input placeholder="php" id="container" @if ($type === 'application')
helper="You can leave it empty if your resource only have one container." label="Container name" /> @if ($containerNames->count() > 1)
<x-forms.button @click="slideOverOpen=false" type="submit"> <x-forms.select id="container" label="Container name">
@foreach ($containerNames as $containerName)
<option value="{{ $containerName }}">{{ $containerName }}</option>
@endforeach
</x-forms.select>
@else
<x-forms.input placeholder="php" id="container"
helper="You can leave it empty if your resource only have one container." label="Container name" />
@endif
@elseif ($type === 'service')
<x-forms.select id="container" label="Container name">
@foreach ($containerNames as $containerName)
<option value="{{ $containerName }}">{{ $containerName }}</option>
@endforeach
</x-forms.select>
@endif
<x-forms.button @click="modalOpen=false" type="submit">
Save Save
</x-forms.button> </x-forms.button>
</form> </form>

View File

@ -1,23 +1,48 @@
<div> <div>
<div class="flex gap-2"> <div class="flex gap-2">
<h2>Scheduled Tasks</h2> <h2>Scheduled Tasks</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Task"> <x-modal-input buttonTitle="+ Add" title="New Scheduled Task" :closeOutside=false>
<livewire:project.shared.scheduled-task.add /> @if ($resource->type() == 'application')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames"/>
@elseif ($resource->type() == 'service')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames"/>
@endif
</x-modal-input> </x-modal-input>
</div> </div>
<div class="flex flex-wrap gap-2 pt-4"> <div class="flex flex-wrap gap-2 pt-4">
@forelse($resource->scheduled_tasks as $task) @forelse($resource->scheduled_tasks as $task)
<a class="flex flex-col box" @if ($resource->type() == 'application')
@if ($resource->type() == 'application') href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}"> <a class="box"
@elseif ($resource->type() == 'service') href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}"> @endif <span class="flex flex-col">
<div><span class="font-bold dark:text-warning">{{ $task->name }}<span> <span class="text-lg font-bold">{{ $task->name }}
@if ($task->container)
<span class="text-xs font-normal">({{ $task->container }})</span>
@endif
</span>
<span>Frequency: {{ $task->frequency }}</span>
<span>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}
</span>
</span>
</a>
@elseif ($resource->type() == 'service')
<a class="box"
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
<span class="flex flex-col">
<span class="text-lg font-bold">{{ $task->name }}
@if ($task->container)
<span class="text-xs font-normal">({{ $task->container }})</span>
@endif
</span>
<span>Frequency: {{ $task->frequency }}</span>
<span>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}
</span>
</span>
</a>
@endif
@empty
<div>No scheduled tasks configured.</div>
@endforelse
</div> </div>
<div>Frequency: {{ $task->frequency }}</div>
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
</a>
@empty
<div>No scheduled tasks configured.</div>
@endforelse
</div>
</div> </div>

View File

@ -12,7 +12,7 @@
</div> </div>
@endif @endif
<a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([ <a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([
'flex flex-col border-l border-dashed transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-100', 'flex flex-col border-l transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-200 cursor-pointer',
'bg-coolgray-200 dark:text-white hover:bg-coolgray-200' => 'bg-coolgray-200 dark:text-white hover:bg-coolgray-200' =>
data_get($execution, 'id') == $selectedKey, data_get($execution, 'id') == $selectedKey,
'border-green-500' => data_get($execution, 'status') === 'success', 'border-green-500' => data_get($execution, 'status') === 'success',

View File

@ -1,13 +1,13 @@
<div> <div>
<h1>Scheduled Task</h1>
@if ($type === 'application') @if ($type === 'application')
<h1>Scheduled Task</h1>
<livewire:project.application.heading :application="$resource" /> <livewire:project.application.heading :application="$resource" />
@elseif ($type === 'service') @elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" /> <livewire:project.service.navbar :service="$resource" :parameters="$parameters" />
@endif @endif
<form wire:submit="submit" class="w-full"> <form wire:submit="submit" class="w-full">
<div class="flex flex-col gap-2 pb-4"> <div class="flex flex-col gap-2 pb-2">
<div class="flex items-end gap-2 pt-4"> <div class="flex items-end gap-2 pt-4">
<h2>Scheduled Task</h2> <h2>Scheduled Task</h2>
<x-forms.button type="submit"> <x-forms.button type="submit">
@ -17,13 +17,23 @@
You will delete scheduled task <span class="font-bold dark:text-warning">{{ $task->name }}</span>. You will delete scheduled task <span class="font-bold dark:text-warning">{{ $task->name }}</span>.
</x-modal-confirmation> </x-modal-confirmation>
</div> </div>
<div class="w-48">
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" />
</div>
</div> </div>
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<x-forms.input placeholder="Name" id="task.name" label="Name" required /> <x-forms.input placeholder="Name" id="task.name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required /> <x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required /> <x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
<x-forms.input placeholder="php" helper="You can leave it empty if your resource only have one container." @if ($type === 'application')
id="task.container" label="Container name" /> <x-forms.input placeholder="php"
helper="You can leave it empty if your resource only have one container." id="task.container"
label="Container name" />
@elseif ($type === 'service')
<x-forms.input placeholder="php"
helper="You can leave it empty if your resource only have one service in your stack. Otherwise use the stack name, without the random generated id. So if you have a mysql service in your stack, use mysql."
id="task.container" label="Service name" />
@endif
</div> </div>
</form> </form>

View File

@ -1,7 +1,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<h3 class="dark:text-white">File: {{ str_replace('|', '.', $fileName) }}</h3> <h3 class="dark:text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
<div class="flex gap-2"> <div class="flex gap-2">
<x-modal-input buttonTitle="+ Add" title="Edit Configuration"> <x-modal-input buttonTitle="Edit" title="Edit Configuration">
<livewire:server.proxy.new-dynamic-configuration :server_id="$server_id" :fileName="$fileName" :value="$value" <livewire:server.proxy.new-dynamic-configuration :server_id="$server_id" :fileName="$fileName" :value="$value"
:newFile="$newFile" wire:key="{{ $fileName }}" /> :newFile="$newFile" wire:key="{{ $fileName }}" />
</x-modal-input> </x-modal-input>

View File

@ -1,12 +1,11 @@
<div> <div class="w-full">
<div class="subtitle">S3 Storage used to save backups / files.</div>
<form class="flex flex-col gap-2" wire:submit='submit'> <form class="flex flex-col gap-2" wire:submit='submit'>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required label="Name" id="name" /> <x-forms.input required label="Name" id="name" />
<x-forms.input label="Description" id="description" /> <x-forms.input label="Description" id="description" />
</div> </div>
<x-forms.input required type="url" label="Endpoint" id="endpoint" />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required type="url" label="Endpoint" id="endpoint" />
<x-forms.input required label="Bucket" id="bucket" /> <x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" /> <x-forms.input required label="Region" id="region" />
</div> </div>

View File

@ -1,7 +1,7 @@
<div> <div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<h1>S3 Storages</h1> <h1>S3 Storages</h1>
<x-modal-input buttonTitle="+ Add" title="New S3 Storage"> <x-modal-input buttonTitle="+ Add" title="New S3 Storage" :closeOutside="false">
<livewire:storage.create /> <livewire:storage.create />
</x-modal-input> </x-modal-input>
</div> </div>

View File

@ -1,5 +1,5 @@
<div x-data="{ selected: 'monthly' }" class="w-full pb-20"> <div x-data="{ selected: 'monthly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8"> <div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center"> <div class="flex justify-center">
<fieldset <fieldset
class="grid grid-cols-2 p-1 text-xs font-semibold leading-5 text-center rounded dark:text-white gap-x-1 dark:bg-white/5 bg-black/5"> class="grid grid-cols-2 p-1 text-xs font-semibold leading-5 text-center rounded dark:text-white gap-x-1 dark:bg-white/5 bg-black/5">
@ -62,7 +62,7 @@ class="flex-none w-8 h-8 mr-3 text-warning"
></path></svg ></path></svg
> >
<div class="flex flex-col text-sm text-white"> <div class="flex flex-col text-sm dark:text-white">
<div> <div>
You need to bring your own servers from any cloud provider (such as <a You need to bring your own servers from any cloud provider (such as <a
class="underline" class="underline"

View File

@ -7,7 +7,7 @@ set -e # Exit immediately if a command exits with a non-zero status
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.3.1" VERSION="1.3.1"
DOCKER_VERSION="24.0" DOCKER_VERSION="26.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')

View File

@ -0,0 +1,71 @@
# documentation: https://docs.twenty.com
# slogan: Twenty is a CRM designed to fit your unique business needs.
# tags: crm, self-hosted, dashboard
# logo: svgs/twenty.svg
# port: 3000
services:
twenty:
image: 'twentycrm/twenty:latest'
environment:
- SERVICE_FQDN_TRIGGER_3000
- SERVER_URL=$SERVICE_FQDN_TWENTY
- FRONT_BASE_URL=$SERVICE_FQDN_TWENTY
- ENABLE_DB_MIGRATIONS=true
- SIGN_IN_PREFILLED=false
- STORAGE_TYPE=${STORAGE_TYPE:-local}
- STORAGE_S3_REGION=$STORAGE_S3_REGION
- STORAGE_S3_NAME=$STORAGE_S3_NAME
- STORAGE_S3_ENDPOINT=$STORAGE_S3_ENDPOINT
- ACCESS_TOKEN_SECRET=$SERVICE_BASE64_32_ACCESS
- LOGIN_TOKEN_SECRET=$SERVICE_BASE64_32_LOGIN
- REFRESH_TOKEN_SECRET=$SERVICE_BASE64_32_REFRESH
- FILE_TOKEN_SECRET=$SERVICE_BASE64_32_FILE
- POSTGRES_ADMIN_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- PG_DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@postgres:5432/default
- EMAIL_FROM_ADDRESS=$EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME=$EMAIL_FROM_NAME
- EMAIL_SYSTEM_ADDRESS=$EMAIL_SYSTEM_ADDRESS
- EMAIL_DRIVER=${EMAIL_DRIVER:-logger}
- EMAIL_SMTP_HOST=$EMAIL_SMTP_HOST
- EMAIL_SMTP_PORT=$EMAIL_SMTP_PORT
- EMAIL_SMTP_USER=$EMAIL_SMTP_USER
- EMAIL_SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-false}
- CACHE_STORAGE_TYPE=${CACHE_STORAGE_TYPE:-redis}
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 2s
timeout: 10s
retries: 15
postgres:
image: "twentycrm/twenty-postgres:latest"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_DB=default
volumes:
- pg-data:/bitnami/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10
redis:
image: "redis:latest"
volumes:
- "redis-data:/data"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 20s
retries: 10

View File

@ -948,6 +948,19 @@
"minversion": "0.0.0", "minversion": "0.0.0",
"port": "3000" "port": "3000"
}, },
"twenty": {
"documentation": "https:\/\/docs.twenty.com",
"slogan": "Twenty is a CRM designed to fit your unique business needs.",
"compose": "c2VydmljZXM6CiAgdHdlbnR5OgogICAgaW1hZ2U6ICd0d2VudHljcm0vdHdlbnR5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBTRVJWRVJfVVJMPSRTRVJWSUNFX0ZRRE5fVFdFTlRZCiAgICAgIC0gRlJPTlRfQkFTRV9VUkw9JFNFUlZJQ0VfRlFETl9UV0VOVFkKICAgICAgLSBFTkFCTEVfREJfTUlHUkFUSU9OUz10cnVlCiAgICAgIC0gU0lHTl9JTl9QUkVGSUxMRUQ9ZmFsc2UKICAgICAgLSAnU1RPUkFHRV9UWVBFPSR7U1RPUkFHRV9UWVBFOi1sb2NhbH0nCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049JFNUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gU1RPUkFHRV9TM19OQU1FPSRTVE9SQUdFX1MzX05BTUUKICAgICAgLSBTVE9SQUdFX1MzX0VORFBPSU5UPSRTVE9SQUdFX1MzX0VORFBPSU5UCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfMzJfQUNDRVNTCiAgICAgIC0gTE9HSU5fVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9MT0dJTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9SRUZSRVNICiAgICAgIC0gRklMRV9UT0tFTl9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzMyX0ZJTEUKICAgICAgLSBQT1NUR1JFU19BRE1JTl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQR19EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0Z3JlczokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlczo1NDMyL2RlZmF1bHQnCiAgICAgIC0gRU1BSUxfRlJPTV9BRERSRVNTPSRFTUFJTF9GUk9NX0FERFJFU1MKICAgICAgLSBFTUFJTF9GUk9NX05BTUU9JEVNQUlMX0ZST01fTkFNRQogICAgICAtIEVNQUlMX1NZU1RFTV9BRERSRVNTPSRFTUFJTF9TWVNURU1fQUREUkVTUwogICAgICAtICdFTUFJTF9EUklWRVI9JHtFTUFJTF9EUklWRVI6LWxvZ2dlcn0nCiAgICAgIC0gRU1BSUxfU01UUF9IT1NUPSRFTUFJTF9TTVRQX0hPU1QKICAgICAgLSBFTUFJTF9TTVRQX1BPUlQ9JEVNQUlMX1NNVFBfUE9SVAogICAgICAtIEVNQUlMX1NNVFBfVVNFUj0kRU1BSUxfU01UUF9VU0VSCiAgICAgIC0gRU1BSUxfU01UUF9QQVNTV09SRD0kRU1BSUxfU01UUF9QQVNTV09SRAogICAgICAtICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ0NBQ0hFX1NUT1JBR0VfVFlQRT0ke0NBQ0hFX1NUT1JBR0VfVFlQRTotcmVkaXN9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9oZWFsdGh6JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3R3ZW50eWNybS90d2VudHktcG9zdGdyZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZGVmYXVsdAogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovYml0bmFtaS9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
"tags": [
"crm",
"self-hosted",
"dashboard"
],
"logo": "svgs\/twenty.svg",
"minversion": "0.0.0",
"port": "3000"
},
"umami": { "umami": {
"documentation": "https:\/\/umami.is", "documentation": "https:\/\/umami.is",
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",

View File

@ -1,7 +1,7 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.277" "version": "4.0.0-beta.278"
}, },
"sentinel": { "sentinel": {
"version": "0.0.4" "version": "0.0.4"