mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-07 12:34:18 +00:00
Compare commits
44 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d87a88d3d | ||
|
|
10f9e22a8e | ||
|
|
8edda0cdda | ||
|
|
21047afc02 | ||
|
|
2e9793ffb2 | ||
|
|
fcd100df39 | ||
|
|
dfd564a3a4 | ||
|
|
a43c916009 | ||
|
|
c8332ca9bf | ||
|
|
e98170f921 | ||
|
|
b8f25406cd | ||
|
|
76dcc12b13 | ||
|
|
baa2228c9b | ||
|
|
5275ae8e9c | ||
|
|
c71e1e107e | ||
|
|
8ab72c7e10 | ||
|
|
a8970df91b | ||
|
|
2468251f56 | ||
|
|
6e74f3e40e | ||
|
|
407f84a4bb | ||
|
|
91632f0adb | ||
|
|
af3c575d84 | ||
|
|
bf1475441d | ||
|
|
9268f9db1d | ||
|
|
600c43827a | ||
|
|
74092ea95b | ||
|
|
b67abe58e8 | ||
|
|
678647f39a | ||
|
|
453956172b | ||
|
|
b550c32f9b | ||
|
|
f6b886adbc | ||
|
|
9642453052 | ||
|
|
64fca99c26 | ||
|
|
c7da43f50d | ||
|
|
6efa2dd9ba | ||
|
|
5e980c5fe0 | ||
|
|
c8c7a415ea | ||
|
|
c3cfb8d23b | ||
|
|
1b055f0316 | ||
|
|
1fcbf0b363 | ||
|
|
61dbc81765 | ||
|
|
b8b76dfa40 | ||
|
|
297b314904 | ||
|
|
55dd1ab0a1 |
@@ -33,7 +33,9 @@ Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and
|
||||
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
|
||||
|
||||
## Github Sponsors ($15+)
|
||||
## Github Sponsors ($40+)
|
||||
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></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://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://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a>
|
||||
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
|
||||
|
||||
@@ -16,7 +16,7 @@ class StartService
|
||||
$commands[] = "cd " . $service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
$commands[] = "echo Starting service.";
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
|
||||
25
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
25
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupApplicationDeploymentQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
|
||||
protected $description = 'CleanupApplicationDeploymentQueue';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$team_id = $this->option('team-id');
|
||||
$servers = \App\Models\Server::where('team_id', $team_id)->get();
|
||||
foreach ($servers as $server) {
|
||||
$deployments = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->where("server_id", $server->id)->get();
|
||||
foreach ($deployments as $deployment) {
|
||||
$deployment->update(['status' => 'failed']);
|
||||
instant_remote_process(['docker rm -f ' . $deployment->deployment_uuid], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/Console/Commands/CleanupDatabase.php
Normal file
59
app/Console/Commands/CleanupDatabase.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CleanupDatabase extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:database {--yes}';
|
||||
protected $description = 'Cleanup database';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running database cleanup...\n";
|
||||
$keep_days = 60;
|
||||
|
||||
// Cleanup failed jobs table
|
||||
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7));
|
||||
$count = $failed_jobs->count();
|
||||
echo "Delete $count entries from failed_jobs.\n";
|
||||
if ($this->option('yes')) {
|
||||
$failed_jobs->delete();
|
||||
}
|
||||
|
||||
// Cleanup sessions table
|
||||
$sessions = DB::table('sessions')->where('last_activity', '<', now()->subDays($keep_days)->timestamp);
|
||||
$count = $sessions->count();
|
||||
echo "Delete $count entries from sessions.\n";
|
||||
if ($this->option('yes')) {
|
||||
$sessions->delete();
|
||||
}
|
||||
|
||||
// Cleanup activity_log table
|
||||
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $activity_log->count();
|
||||
echo "Delete $count entries from activity_log.\n";
|
||||
if ($this->option('yes')) {
|
||||
$activity_log->delete();
|
||||
}
|
||||
|
||||
// Cleanup application_deployment_queues table
|
||||
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $application_deployment_queues->count();
|
||||
echo "Delete $count entries from application_deployment_queues.\n";
|
||||
if ($this->option('yes')) {
|
||||
$application_deployment_queues->delete();
|
||||
}
|
||||
|
||||
// Cleanup webhooks table
|
||||
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $webhooks->count();
|
||||
echo "Delete $count entries from webhooks.\n";
|
||||
if ($this->option('yes')) {
|
||||
$webhooks->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,16 @@ use Illuminate\Console\Command;
|
||||
class CleanupUnreachableServers extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:unreachable-servers';
|
||||
protected $description = 'Cleanup Unreachable Servers (3 days)';
|
||||
protected $description = 'Cleanup Unreachable Servers (7 days)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running unreachable server cleanup...\n";
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4'
|
||||
]);
|
||||
|
||||
@@ -69,12 +69,34 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
foreach ($containerServers as $server) {
|
||||
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new ContainerStatusJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// ray('dispatching container status job in ' . $randomSeconds . ' seconds');
|
||||
// dispatch($job);
|
||||
// })->name('container-status-' . $server->id)->everyMinute()->onOneServer();
|
||||
if ($server->isLogDrainEnabled()) {
|
||||
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new CheckLogDrainContainerJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// dispatch($job);
|
||||
// })->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
|
||||
}
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule
|
||||
// ->call(function () use ($server) {
|
||||
// $randomSeconds = rand(1, 40);
|
||||
// $job = new ServerStatusJob($server);
|
||||
// $job->delay($randomSeconds);
|
||||
// dispatch($job);
|
||||
// })->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
|
||||
}
|
||||
}
|
||||
private function instance_auto_update($schedule)
|
||||
|
||||
@@ -167,65 +167,71 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
|
||||
}
|
||||
if ($this->application->build_pack === 'dockerfile') {
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
$this->dockerfile_location = $this->application->dockerfile_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
$allContainers = collect($allContainers)->sort()->values();
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/-(\d{12})/', $containerName)) {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
try {
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
$allContainers = collect($allContainers)->sort()->values();
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/-(\d{12})/', $containerName)) {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Check custom port
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
// Check custom port
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$teamId = data_get($this->application, 'environment.project.team.id');
|
||||
$buildServers = Server::buildServers($teamId)->get();
|
||||
if ($buildServers->count() === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$teamId = data_get($this->application, 'environment.project.team.id');
|
||||
$buildServers = Server::buildServers($teamId)->get();
|
||||
if ($buildServers->count() === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->original_server = $this->server;
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
} else {
|
||||
// Set build server & original_server to the same as deployment server
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->original_server = $this->server;
|
||||
$this->use_build_server = true;
|
||||
}
|
||||
} else {
|
||||
// Set build server & original_server to the same as deployment server
|
||||
$this->build_server = $this->server;
|
||||
$this->original_server = $this->server;
|
||||
}
|
||||
try {
|
||||
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
|
||||
$this->just_restart();
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
@@ -1223,6 +1229,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$ipv4 = data_get($custom_compose, 'ip.0');
|
||||
$ipv6 = data_get($custom_compose, 'ip6.0');
|
||||
data_forget($custom_compose, 'ip');
|
||||
data_forget($custom_compose, 'ip6');
|
||||
if ($ipv4 || $ipv6) {
|
||||
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
|
||||
}
|
||||
if ($ipv4) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
|
||||
}
|
||||
if ($ipv6) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
|
||||
}
|
||||
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
|
||||
}
|
||||
} else {
|
||||
@@ -1230,6 +1249,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
data_forget($docker_compose, 'services.' . $this->container_name);
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$ipv4 = data_get($custom_compose, 'ip.0');
|
||||
$ipv6 = data_get($custom_compose, 'ip6.0');
|
||||
data_forget($custom_compose, 'ip');
|
||||
data_forget($custom_compose, 'ip6');
|
||||
if ($ipv4 || $ipv6) {
|
||||
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
|
||||
}
|
||||
if ($ipv4) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
|
||||
}
|
||||
if ($ipv6) {
|
||||
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
|
||||
}
|
||||
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
|
||||
}
|
||||
}
|
||||
@@ -1634,6 +1666,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
|
||||
if (str($exception->getMessage())->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
@@ -1641,6 +1675,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
$code = $exception->getCode();
|
||||
ray($code);
|
||||
if ($code !== 69420) {
|
||||
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
|
||||
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
|
||||
@@ -1649,7 +1684,5 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
};
|
||||
|
||||
$applications = $this->server->applications();
|
||||
$skip_these_applications = collect([]);
|
||||
foreach ($applications as $application) {
|
||||
@@ -57,10 +61,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
|
||||
return !$skip_these_applications->pluck('id')->contains($value->id);
|
||||
});
|
||||
|
||||
if (!$this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
};
|
||||
try {
|
||||
if ($this->server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
|
||||
68
app/Jobs/ServerLimitCheckJob.php
Normal file
68
app/Jobs/ServerLimitCheckJob.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Server\ForceDisabled;
|
||||
use App\Notifications\Server\ForceEnabled;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 4;
|
||||
public function backoff(): int
|
||||
{
|
||||
return isDev() ? 1 : 3;
|
||||
}
|
||||
public function __construct(public Team $team)
|
||||
{
|
||||
}
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->team->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->team->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$servers = $this->team->servers;
|
||||
$servers_count = $servers->count();
|
||||
$limit = $this->team->limits['serverLimit'];
|
||||
$number_of_servers_to_disable = $servers_count - $limit;
|
||||
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
|
||||
if ($number_of_servers_to_disable > 0) {
|
||||
ray('Disabling servers');
|
||||
$servers = $servers->sortbyDesc('created_at');
|
||||
$servers_to_disable = $servers->take($number_of_servers_to_disable);
|
||||
$servers_to_disable->each(function ($server) {
|
||||
$server->forceDisableServer();
|
||||
$this->team->notify(new ForceDisabled($server));
|
||||
});
|
||||
} else if ($number_of_servers_to_disable === 0) {
|
||||
$servers->each(function ($server) {
|
||||
if ($server->isForceDisabled()) {
|
||||
$server->forceEnableServer();
|
||||
$this->team->notify(new ForceEnabled($server));
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,15 +41,6 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
throw new \RuntimeException('Server is not ready.');
|
||||
};
|
||||
try {
|
||||
// $this->server->validateConnection();
|
||||
// $this->server->validateOS();
|
||||
// $docker_installed = $this->server->validateDockerEngine();
|
||||
// if (!$docker_installed) {
|
||||
// $this->server->installDocker();
|
||||
// $this->server->validateDockerEngine();
|
||||
// }
|
||||
|
||||
// $this->server->validateDockerEngineVersion();
|
||||
if ($this->server->isFunctional()) {
|
||||
$this->cleanup(notify: false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,28 +15,26 @@ class Index extends Component
|
||||
if (!isCloud()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (auth()->user()->id !== 0 && session('adminToken') === null) {
|
||||
if (auth()->user()->id !== 0) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->users = User::whereHas('teams', function ($query) {
|
||||
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
|
||||
})->get();
|
||||
})->get()->filter(function ($user) {
|
||||
return $user->id !== 0;
|
||||
});
|
||||
}
|
||||
public function switchUser(int $user_id)
|
||||
{
|
||||
$user = User::find($user_id);
|
||||
auth()->login($user);
|
||||
|
||||
if ($user_id === 0) {
|
||||
session()->forget('adminToken');
|
||||
} else {
|
||||
$token_payload = [
|
||||
'valid' => true,
|
||||
];
|
||||
$token = Crypt::encrypt($token_payload);
|
||||
session(['adminToken' => $token]);
|
||||
if (auth()->user()->id !== 0) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
return refreshSession();
|
||||
$user = User::find($user_id);
|
||||
$team_to_switch_to = $user->teams->first();
|
||||
Cache::forget("team:{$user->id}");
|
||||
auth()->login($user);
|
||||
refreshSession($team_to_switch_to);
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -23,8 +23,8 @@ class Dashboard extends Component
|
||||
public function cleanup_queue()
|
||||
{
|
||||
$this->dispatch('success', 'Cleanup started.');
|
||||
Artisan::queue('app:init', [
|
||||
'--cleanup-deployments' => 'true'
|
||||
Artisan::queue('cleanup:application-deployment-queue', [
|
||||
'--team-id' => currentTeam()->id
|
||||
]);
|
||||
}
|
||||
public function get_deployments()
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Sponsorship extends Component
|
||||
class LayoutPopups extends Component
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
@@ -23,6 +23,6 @@ class Sponsorship extends Component
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.sponsorship');
|
||||
return view('livewire.layout-popups');
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ class Import extends Component
|
||||
public string $container;
|
||||
public array $importCommands = [];
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
|
||||
@@ -10,7 +10,8 @@ use Livewire\Component;
|
||||
class Create extends Component
|
||||
{
|
||||
public $type;
|
||||
public function mount() {
|
||||
public function mount()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
$type = str(request()->query('type'));
|
||||
$destination_uuid = request()->query('destination');
|
||||
@@ -70,7 +71,7 @@ class Create extends Component
|
||||
$generatedValue = $value;
|
||||
if ($value->contains('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$generatedValue = generateEnvValue($command->value());
|
||||
$generatedValue = generateEnvValue($command->value(), $service);
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
|
||||
@@ -64,7 +64,7 @@ class Navbar extends Component
|
||||
StopService::run($this->service);
|
||||
$this->service->refresh();
|
||||
if ($forceCleanup) {
|
||||
$this->dispatch('success', 'Force cleanup service.');
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ class ExecuteContainerCommand extends Component
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
if ($this->server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
// Wrap command to prevent escaped execution in the host.
|
||||
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"';
|
||||
if (!empty($this->workDir)) {
|
||||
|
||||
@@ -23,6 +23,7 @@ class GetLogs extends Component
|
||||
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
|
||||
public Server $server;
|
||||
public ?string $container = null;
|
||||
public ?string $pull_request = null;
|
||||
public ?bool $streamLogs = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
public int $numberOfLines = 100;
|
||||
@@ -70,7 +71,14 @@ class GetLogs extends Component
|
||||
}
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (!$refresh && $this->resource?->getMorphClass() === 'App\Models\Service') return;
|
||||
if ($this->resource?->getMorphClass() === 'App\Models\Application') {
|
||||
if (str($this->container)->contains('-pr-')) {
|
||||
$this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
|
||||
} else {
|
||||
$this->pull_request = 'branch';
|
||||
}
|
||||
}
|
||||
if (!$refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) return;
|
||||
if ($this->container) {
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
|
||||
@@ -41,13 +41,19 @@ class Logs extends Component
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, includePullrequests: true);
|
||||
}
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
}
|
||||
$this->containers = $this->containers->sortByDesc(function ($container) {
|
||||
if (str_contains($container, '-pr-')) {
|
||||
return explode('-pr-', $container)[1];
|
||||
}
|
||||
return $container;
|
||||
});
|
||||
} else if (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->type = 'database';
|
||||
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
@@ -70,21 +76,15 @@ class Logs extends Component
|
||||
$this->status = $this->resource->status;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$this->container = $this->resource->uuid;
|
||||
// if (str(data_get($this, 'resource.status'))->startsWith('running')) {
|
||||
$this->containers->push($this->container);
|
||||
// }
|
||||
$this->containers->push($this->container);
|
||||
} else if (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
// if (str(data_get($application, 'status'))->contains('running')) {
|
||||
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
// }
|
||||
$this->containers->push(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->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
// }
|
||||
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
});
|
||||
|
||||
$this->server = $this->resource->server;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use Livewire\Component;
|
||||
|
||||
class Create extends Component
|
||||
@@ -16,11 +17,7 @@ class Create extends Component
|
||||
$this->limit_reached = false;
|
||||
return;
|
||||
}
|
||||
$team = currentTeam();
|
||||
$servers = $team->servers->count();
|
||||
['serverLimit' => $serverLimit] = $team->limits;
|
||||
|
||||
$this->limit_reached = $servers >= $serverLimit;
|
||||
$this->limit_reached = Team::serverLimitReached();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Server\New;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Livewire\Component;
|
||||
|
||||
class ByIp extends Component
|
||||
@@ -76,6 +77,9 @@ class ByIp extends Component
|
||||
if (is_null($this->private_key_id)) {
|
||||
return $this->dispatch('error', 'You must select a private key');
|
||||
}
|
||||
if (Team::serverLimitReached()) {
|
||||
return $this->dispatch('error', 'You have reached the server limit for your subscription.');
|
||||
}
|
||||
$payload = [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
|
||||
@@ -55,7 +55,7 @@ class Resources extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->loadUnmanagedContainers();
|
||||
// $this->loadUnmanagedContainers();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
namespace App\Livewire\Subscription;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Actions extends Component
|
||||
{
|
||||
public $server_limits = 0;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$limits = currentTeam()->limits;
|
||||
$this->server_limits = data_get($limits, 'serverLimit', 0);
|
||||
|
||||
$this->server_limits = Team::serverLimit();
|
||||
}
|
||||
public function cancel()
|
||||
{
|
||||
|
||||
@@ -9,8 +9,9 @@ use Stripe\Checkout\Session;
|
||||
class PricingPlans extends Component
|
||||
{
|
||||
public bool $isTrial = false;
|
||||
public function mount() {
|
||||
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
|
||||
public function mount()
|
||||
{
|
||||
$this->isTrial = !data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
|
||||
if (config('constants.limits.trial_period') == 0) {
|
||||
$this->isTrial = false;
|
||||
}
|
||||
@@ -26,15 +27,15 @@ class PricingPlans extends Component
|
||||
case 'basic-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_basic_yearly');
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||
break;
|
||||
case 'pro-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_monthly');
|
||||
break;
|
||||
case 'pro-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_yearly');
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||
break;
|
||||
case 'ultimate-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
|
||||
break;
|
||||
@@ -64,18 +65,25 @@ class PricingPlans extends Component
|
||||
'success_url' => route('dashboard', ['success' => true]),
|
||||
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
||||
];
|
||||
|
||||
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
|
||||
if (config('constants.limits.trial_period') > 0) {
|
||||
$payload['subscription_data'] = [
|
||||
'trial_period_days' => config('constants.limits.trial_period'),
|
||||
'trial_settings' => [
|
||||
'end_behavior' => [
|
||||
'missing_payment_method' => 'cancel',
|
||||
]
|
||||
],
|
||||
if (str($type)->contains('ultimate')) {
|
||||
$payload['line_items'][0]['adjustable_quantity'] = [
|
||||
'enabled' => true,
|
||||
'minimum' => 10,
|
||||
];
|
||||
$payload['line_items'][0]['quantity'] = 10;
|
||||
}
|
||||
|
||||
if (!data_get($team, 'subscription.stripe_trial_already_ended')) {
|
||||
if (config('constants.limits.trial_period') > 0) {
|
||||
$payload['subscription_data'] = [
|
||||
'trial_period_days' => config('constants.limits.trial_period'),
|
||||
'trial_settings' => [
|
||||
'end_behavior' => [
|
||||
'missing_payment_method' => 'cancel',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
$payload['payment_method_collection'] = 'if_required';
|
||||
}
|
||||
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -69,7 +70,7 @@ class Server extends BaseModel
|
||||
|
||||
static public function isUsable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false);
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
|
||||
}
|
||||
|
||||
static public function destinationsByServer(string $server_id)
|
||||
@@ -146,11 +147,34 @@ class Server extends BaseModel
|
||||
public function skipServer()
|
||||
{
|
||||
if ($this->ip === '1.2.3.4') {
|
||||
ray('skipping 1.2.3.4');
|
||||
// ray('skipping 1.2.3.4');
|
||||
return true;
|
||||
}
|
||||
if ($this->settings->force_disabled === true) {
|
||||
// ray('force_disabled');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public function isForceDisabled()
|
||||
{
|
||||
return $this->settings->force_disabled;
|
||||
}
|
||||
public function forceEnableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
}
|
||||
public function forceDisableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => true,
|
||||
]);
|
||||
$sshKeyFileLocation = "id.root@{$this->uuid}";
|
||||
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
public function isServerReady(int $tries = 3)
|
||||
{
|
||||
if ($this->skipServer()) {
|
||||
@@ -239,17 +263,19 @@ class Server extends BaseModel
|
||||
}
|
||||
public function loadUnmanagedContainers()
|
||||
{
|
||||
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.managed")) {
|
||||
return $container;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$containers = $containers->filter();
|
||||
return collect($containers);
|
||||
if ($this->isFunctional()) {
|
||||
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.managed")) {
|
||||
return $container;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$containers = $containers->filter();
|
||||
return collect($containers);
|
||||
}
|
||||
}
|
||||
public function hasDefinedResources()
|
||||
{
|
||||
@@ -374,7 +400,7 @@ class Server extends BaseModel
|
||||
}
|
||||
public function isFunctional()
|
||||
{
|
||||
return $this->settings->is_reachable && $this->settings->is_usable;
|
||||
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
|
||||
@@ -102,6 +102,30 @@ class Service extends BaseModel
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)?->contains('kong'):
|
||||
$data = collect([]);
|
||||
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
|
||||
if ($dashboard_user) {
|
||||
$data = $data->merge([
|
||||
'Dashboard User' => [
|
||||
'key' => data_get($dashboard_user, 'key'),
|
||||
'value' => data_get($dashboard_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($dashboard_password) {
|
||||
$data = $data->merge([
|
||||
'Dashboard Password' => [
|
||||
'key' => data_get($dashboard_password, 'key'),
|
||||
'value' => data_get($dashboard_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Supabase', $data->toArray());
|
||||
case str($image)?->contains('minio'):
|
||||
$data = collect([]);
|
||||
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
|
||||
@@ -48,7 +48,22 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
}
|
||||
return explode(',', $recipients);
|
||||
}
|
||||
|
||||
static public function serverLimitReached() {
|
||||
$serverLimit = Team::serverLimit();
|
||||
$team = currentTeam();
|
||||
$servers = $team->servers->count();
|
||||
return $servers >= $serverLimit;
|
||||
}
|
||||
public function serverOverflow() {
|
||||
if ($this->serverLimit() < $this->servers->count()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static public function serverLimit()
|
||||
{
|
||||
return Team::find(currentTeam()->id)->limits['serverLimit'];
|
||||
}
|
||||
public function limits(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -63,14 +78,19 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
$subscription = $subscription->type();
|
||||
}
|
||||
}
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
if ($this->custom_server_limit) {
|
||||
$serverLimit = $this->custom_server_limit;
|
||||
} else {
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
}
|
||||
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
|
||||
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
public function environment_variables() {
|
||||
public function environment_variables()
|
||||
{
|
||||
return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id');
|
||||
}
|
||||
public function members()
|
||||
@@ -130,7 +150,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
{
|
||||
return $this->hasMany(S3Storage::class)->where('is_usable', true);
|
||||
}
|
||||
public function trialEnded() {
|
||||
public function trialEnded()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => false,
|
||||
@@ -138,7 +159,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
]);
|
||||
}
|
||||
}
|
||||
public function trialEndedButSubscribed() {
|
||||
public function trialEndedButSubscribed()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => true,
|
||||
|
||||
63
app/Notifications/Server/ForceDisabled.php
Normal file
63
app/Notifications/Server/ForceDisabled.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ForceDisabled extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = isEmailEnabled($notifiable);
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
|
||||
|
||||
if ($isDiscordEnabled) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
if ($isEmailEnabled) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isTelegramEnabled) {
|
||||
$channels[] = TelegramChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
|
||||
$mail->view('emails.server-force-disabled', [
|
||||
'name' => $this->server->name,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
$message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
|
||||
return $message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)."
|
||||
];
|
||||
}
|
||||
}
|
||||
63
app/Notifications/Server/ForceEnabled.php
Normal file
63
app/Notifications/Server/ForceEnabled.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ForceEnabled extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = isEmailEnabled($notifiable);
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
|
||||
|
||||
if ($isDiscordEnabled) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
if ($isEmailEnabled) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isTelegramEnabled) {
|
||||
$channels[] = TelegramChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
|
||||
$mail->view('emails.server-force-enabled', [
|
||||
'name' => $this->server->name,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
$message = "Coolify: Server ({$this->server->name}) enabled again!";
|
||||
return $message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => "Coolify: Server ({$this->server->name}) enabled again!"
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,21 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
|
||||
{
|
||||
$containers = collect([]);
|
||||
if (!$server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containers = $containers->map(function ($container) use ($pullRequestId) {
|
||||
$containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) {
|
||||
$labels = data_get($container, 'Labels');
|
||||
if (!str($labels)->contains("coolify.pullRequestId=")) {
|
||||
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
|
||||
return $container;
|
||||
}
|
||||
if ($includePullrequests) {
|
||||
return $container;
|
||||
}
|
||||
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
|
||||
return $container;
|
||||
}
|
||||
@@ -423,7 +426,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
|
||||
'--security-opt',
|
||||
'--sysctl',
|
||||
'--ulimit',
|
||||
'--device'
|
||||
'--device',
|
||||
]);
|
||||
$mapping = collect([
|
||||
'--cap-add' => 'cap_add',
|
||||
@@ -435,6 +438,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
|
||||
'--init' => 'init',
|
||||
'--ulimit' => 'ulimits',
|
||||
'--privileged' => 'privileged',
|
||||
'--ip' => 'ip',
|
||||
]);
|
||||
foreach ($matches as $match) {
|
||||
$option = $match[1];
|
||||
|
||||
@@ -110,6 +110,9 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
|
||||
}
|
||||
function generateSshCommand(Server $server, string $command)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$user = $server->user;
|
||||
$port = $server->port;
|
||||
$privateKeyLocation = savePrivateKeyToFs($server);
|
||||
|
||||
@@ -33,6 +33,11 @@ use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use Lcobucci\JWT\Encoding\ChainedFormatter;
|
||||
use Lcobucci\JWT\Encoding\JoseEncoder;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Lcobucci\JWT\Token\Builder;
|
||||
use Poliander\Cron\CronExpression;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
@@ -625,7 +630,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
}
|
||||
}
|
||||
$definedNetwork = collect([$resource->uuid]);
|
||||
|
||||
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) {
|
||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||
$servicePorts = collect(data_get($service, 'ports', []));
|
||||
@@ -927,6 +931,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $fqdn,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $resource->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
// data_forget($service, "environment.$variableName");
|
||||
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
|
||||
@@ -978,7 +989,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$generatedValue = generateEnvValue($command);
|
||||
$generatedValue = generateEnvValue($command, $resource);
|
||||
if (!$foundEnv) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
@@ -1394,7 +1405,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$generatedValue = generateEnvValue($command);
|
||||
$generatedValue = generateEnvValue($command, $service);
|
||||
if (!$foundEnv) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
@@ -1570,7 +1581,7 @@ function parseEnvVariable(Str|string $value)
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
function generateEnvValue(string $command)
|
||||
function generateEnvValue(string $command, Service $service)
|
||||
{
|
||||
switch ($command) {
|
||||
case 'PASSWORD':
|
||||
@@ -1591,6 +1602,46 @@ function generateEnvValue(string $command)
|
||||
case 'USER':
|
||||
$generatedValue = Str::random(16);
|
||||
break;
|
||||
case 'SUPABASEANON':
|
||||
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
|
||||
if (is_null($signingKey)) {
|
||||
return;
|
||||
} else {
|
||||
$signingKey = $signingKey->value;
|
||||
}
|
||||
$key = InMemory::plainText($signingKey);
|
||||
$algorithm = new Sha256();
|
||||
$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
|
||||
$now = new DateTimeImmutable();
|
||||
$now = $now->setTime($now->format('H'), $now->format('i'));
|
||||
$token = $tokenBuilder
|
||||
->issuedBy('supabase')
|
||||
->issuedAt($now)
|
||||
->expiresAt($now->modify('+100 year'))
|
||||
->withClaim('role', 'anon')
|
||||
->getToken($algorithm, $key);
|
||||
$generatedValue = $token->toString();
|
||||
break;
|
||||
case 'SUPABASESERVICE':
|
||||
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
|
||||
if (is_null($signingKey)) {
|
||||
return;
|
||||
} else {
|
||||
$signingKey = $signingKey->value;
|
||||
}
|
||||
$key = InMemory::plainText($signingKey);
|
||||
$algorithm = new Sha256();
|
||||
$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
|
||||
$now = new DateTimeImmutable();
|
||||
$now = $now->setTime($now->format('H'), $now->format('i'));
|
||||
$token = $tokenBuilder
|
||||
->issuedBy('supabase')
|
||||
->issuedAt($now)
|
||||
->expiresAt($now->modify('+100 year'))
|
||||
->withClaim('role', 'service_role')
|
||||
->getToken($algorithm, $key);
|
||||
$generatedValue = $token->toString();
|
||||
break;
|
||||
default:
|
||||
$generatedValue = Str::random(16);
|
||||
break;
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.224',
|
||||
'release' => '4.0.0-beta.228',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.224';
|
||||
return '4.0.0-beta.228';
|
||||
|
||||
@@ -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('teams', function (Blueprint $table) {
|
||||
$table->integer('custom_server_limit')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('custom_server_limit');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('force_disabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('force_disabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
3
public/svgs/internal-link.svg
Normal file
3
public/svgs/internal-link.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6 6l6-6m-6-6l6 6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 203 B |
15
public/svgs/supabase.svg
Normal file
15
public/svgs/supabase.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
22
resources/views/components/banner.blade.php
Normal file
22
resources/views/components/banner.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@props(['closable' => true])
|
||||
<div x-data="{
|
||||
bannerVisible: false,
|
||||
bannerVisibleAfter: 100,
|
||||
}" x-show="bannerVisible" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="-translate-y-10" x-transition:enter-end="translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-100" x-transition:leave-start="translate-y-0"
|
||||
x-transition:leave-end="-translate-y-10" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
|
||||
class="relative z-50 w-full py-2 mx-auto duration-100 ease-out shadow-sm bg-coolgray-100 sm:py-0 sm:h-14" x-cloak>
|
||||
<div class="flex items-center justify-between h-full px-3">
|
||||
{{ $slot }}
|
||||
@if ($closable)
|
||||
<button @click="bannerVisible=false"
|
||||
class="flex items-center flex-shrink-0 translate-x-1 ease-out duration-150 justify-center w-6 h-6 p-1.5 text-neutral-200 rounded-full hover:bg-coolgray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-full h-full">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
1
resources/views/components/internal-link.blade.php
Normal file
1
resources/views/components/internal-link.blade.php
Normal file
@@ -0,0 +1 @@
|
||||
<img class="inline-flex w-4 h-4" src="{{ asset('svgs/internal-link.svg') }}">
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="flex flex-col items-center justify-center h-screen">
|
||||
<span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span>
|
||||
<span>Please <a class="text-white underline "href="{{ route('team.index') }}">upgrade your
|
||||
<span>Please <a class="text-white underline "href="{{ route('subscription.show') }}">upgrade your
|
||||
subscription</a> to create more
|
||||
{{ $name }}.</span>
|
||||
</div>
|
||||
|
||||
@@ -188,21 +188,21 @@
|
||||
</div>
|
||||
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
||||
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3>
|
||||
<p class="flex items-baseline mt-6 gap-x-1">
|
||||
<p class="flex items-baseline mt-6 gap-x-1">
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">$?</span>
|
||||
<span class="text-sm font-semibold leading-6 ">/month + VAT</span>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">Custom</span>
|
||||
{{-- <span class="text-sm font-semibold leading-6 ">pay-as-you-go</span> --}}
|
||||
</span>
|
||||
<span x-show="selected === 'yearly'" x-cloak>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">$?</span>
|
||||
<span class="text-sm font-semibold leading-6 ">/month + VAT</span>
|
||||
<span class="text-4xl font-bold tracking-tight text-white">Custom</span>
|
||||
{{-- <span class="text-sm font-semibold leading-6 ">/month + VAT</span> --}}
|
||||
</span>
|
||||
</p>
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span>billed monthly</span>
|
||||
<span x-show="selected === 'monthly'" x-cloak>
|
||||
<span>pay-as-you-go</span>
|
||||
</span>
|
||||
<span x-show="selected === 'yearly'" x-cloak>
|
||||
<span>billed annually</span>
|
||||
<span>pay-as-you-go</span>
|
||||
</span>
|
||||
@if ($showSubscribeButtons)
|
||||
@isset($ultimate)
|
||||
@@ -219,7 +219,7 @@
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Connect <span class="px-1 font-bold text-white">unlimited</span> servers
|
||||
Connect <span class="px-1 font-bold text-white">10+</span> servers
|
||||
</li>
|
||||
|
||||
<li class="flex gap-x-3">
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
<div>
|
||||
@if ($server->isFunctional())
|
||||
<div class="flex h-full pr-4">
|
||||
<div class="flex flex-col w-48 gap-4 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if (data_get($server, 'proxy.type') !== 'NONE')
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>Server is not validated. Validate first.</div>
|
||||
@endif
|
||||
<div class="flex h-full pr-4">
|
||||
<div class="flex flex-col w-48 gap-4 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if (data_get($server, 'proxy.type') !== 'NONE')
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
5
resources/views/emails/server-force-disabled.blade.php
Normal file
5
resources/views/emails/server-force-disabled.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-emails.layout>
|
||||
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
|
||||
|
||||
Please update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).
|
||||
</x-emails.layout>
|
||||
3
resources/views/emails/server-force-enabled.blade.php
Normal file
3
resources/views/emails/server-force-enabled.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<x-emails.layout>
|
||||
Your server ({{ $name }}) is enabled again!
|
||||
</x-emails.layout>
|
||||
@@ -1,5 +1,5 @@
|
||||
<x-emails.layout>
|
||||
Your trial ends soon. Please update payment details [here]({{ $stripeCustomerPortal }}),
|
||||
|
||||
Your servers & deployed resources will be untouched, but you won't be able to deploy new resources and lost all automations and integrations.
|
||||
Your servers & deployed resources will be untouched, but you won't be able to deploy new resources and lose all automations and integrations.
|
||||
</x-emails.layout>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
@endpersist
|
||||
<livewire:sponsorship />
|
||||
<livewire:layout-popups />
|
||||
@auth
|
||||
<livewire:realtime-connection />
|
||||
@endauth
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{ auth()->user()->name }}
|
||||
<h3 class="pt-4">Users</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="w-96 box" wire:click="switchUser('0')">
|
||||
<div class="text-white cursor-pointer w-96 box-without-bg bg-coollabs-100" wire:click="switchUser('0')">
|
||||
Root
|
||||
</div>
|
||||
@foreach ($users as $user)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
@forelse ($server->swarmDockers as $docker)
|
||||
<a
|
||||
@@ -24,7 +23,6 @@
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
|
||||
@@ -11,4 +11,13 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if (currentTeam()->serverOverflow())
|
||||
<x-banner :closable=false>
|
||||
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
|
||||
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
|
||||
class="text-white underline">/subscription</a> to update your subscription or remove some servers.
|
||||
</div>
|
||||
</x-banner>
|
||||
@endif
|
||||
</div>
|
||||
@@ -52,8 +52,8 @@
|
||||
@foreach (decode_remote_command_output($application_deployment_queue) as $line)
|
||||
<div @class([
|
||||
'font-mono',
|
||||
'text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['type'] == 'stderr',
|
||||
'text-warning whitespace-pre-line' => $line['hidden'],
|
||||
'text-red-500 whitespace-pre-line' => $line['type'] == 'stderr',
|
||||
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
|
||||
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
|
||||
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))
|
||||
|
||||
@@ -86,15 +86,21 @@
|
||||
Redeploy
|
||||
@endif
|
||||
</x-forms.button>
|
||||
<x-forms.button class="bg-coolgray-500"
|
||||
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove Preview
|
||||
</x-forms.button>
|
||||
<a
|
||||
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button class="bg-coolgray-500">
|
||||
Get Deployment Logs
|
||||
Deployment Logs
|
||||
</x-forms.button>
|
||||
</a>
|
||||
<a
|
||||
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button class="bg-coolgray-500">
|
||||
Application Logs
|
||||
</x-forms.button>
|
||||
</a>
|
||||
<x-forms.button isError class="bg-coolgray-500"
|
||||
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Delete
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<template x-for="item in filteredApplications" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col w-full px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -66,8 +66,8 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="description break-all" x-text="item.fqdn"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.fqdn"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -83,7 +83,7 @@
|
||||
<template x-for="item in filteredPostgresqls" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -99,7 +99,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -115,7 +115,7 @@
|
||||
<template x-for="item in filteredRedis" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -131,7 +131,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -147,7 +147,7 @@
|
||||
<template x-for="item in filteredMongodbs" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -163,7 +163,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -179,7 +179,7 @@
|
||||
<template x-for="item in filteredMysqls" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -195,7 +195,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -211,7 +211,7 @@
|
||||
<template x-for="item in filteredMariadbs" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -227,7 +227,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
@@ -243,7 +243,7 @@
|
||||
<template x-for="item in filteredServices" :key="item.id">
|
||||
<span>
|
||||
<a class="h-24 box group" :href="item.hrefLink">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="flex flex-col px-4 mx-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
@@ -259,7 +259,7 @@
|
||||
<div title="degraded" class="mt-1 bg-warning badge badge-xs"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="description" x-text="item.description"></div>
|
||||
<div class="max-w-full truncate description" x-text="item.description"></div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<div>
|
||||
<div x-init="$wire.getLogs">
|
||||
<div class="flex gap-2">
|
||||
<h4>Container: {{ $container }}</h4>
|
||||
<div x-init="$wire.getLogs" id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{{ str($container)->beforeLast('-')->headline() }}</h3>
|
||||
@if ($pull_request)
|
||||
<div>({{ $pull_request }})</div>
|
||||
@endif
|
||||
@if ($streamLogs)
|
||||
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<form wire:submit='getLogs(true)' class="flex items-end gap-2 pt-2 ">
|
||||
<div class="w-96">
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required
|
||||
id="numberOfLines"></x-forms.input>
|
||||
</div>
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
|
||||
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
|
||||
</div>
|
||||
<form wire:submit='getLogs(true)' class="flex items-end gap-2">
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required
|
||||
id="numberOfLines"></x-forms.input>
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
</form>
|
||||
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'w-full py-4 mx-auto'">
|
||||
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
|
||||
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
|
||||
<div :class="fullscreen ? 'fullscreen' : 'relative w-full py-4 mx-auto'">
|
||||
<div class="flex flex-col-reverse w-full px-4 py-2 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
|
||||
:class="fullscreen ? '' : 'max-h-96 border border-solid rounded'">
|
||||
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4"
|
||||
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -36,8 +39,8 @@
|
||||
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg></button>
|
||||
|
||||
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-2"
|
||||
x-on:click="makeFullscreen"><svg class=" icon" viewBox="0 0 24 24"
|
||||
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-6 right-4"
|
||||
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
@@ -46,7 +49,11 @@
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg></button>
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
|
||||
@if ($outputs)
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
|
||||
@else
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap">Refresh to get the logs...</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
back!
|
||||
</div>
|
||||
@if ($server->definedResources()->count() > 0)
|
||||
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
|
||||
<x-new-modal disabled isErrorButton buttonTitle="Delete">
|
||||
This server will be deleted. It is not reversible. <br>Please think again.
|
||||
</x-new-modal>
|
||||
<div>You need to delete all resources before deleting this server.</div>
|
||||
@else
|
||||
<x-new-modal isErrorButton buttonTitle="Delete">
|
||||
This server will be deleted. It is not reversible. <br>Please think again.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2>General</h2>
|
||||
@if ($server->id === 0)
|
||||
<x-new-modal buttonTitle="Save" title="Change Localhost" action="submit">
|
||||
You could lost a lot of functionalities if you change the server details of the server where Coolify
|
||||
You could lose a lot of functionalities if you change the server details of the server where Coolify
|
||||
is
|
||||
running on.<br>Please think again.
|
||||
</x-new-modal>
|
||||
@@ -47,6 +47,10 @@
|
||||
Validate Server
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server->isForceDisabled() && isCloud())
|
||||
<div class="pt-4 font-bold text-red-500">The system has disabled the server because you have exceeded the
|
||||
number of servers for which you have paid.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||
<x-forms.input id="server.name" label="Name" required />
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<div>
|
||||
<div class="flex items-start gap-2">
|
||||
<h1>Servers</h1>
|
||||
<a class="text-white hover:no-underline" href="{{ route('server.create') }}">
|
||||
<a class="text-white hover:no-underline" href="{{ route('server.create') }}">
|
||||
<x-forms.button class="btn">+ Add</x-forms.button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="subtitle ">All Servers</div>
|
||||
<div class="grid gap-2 lg:grid-cols-2">
|
||||
@forelse ($servers as $server)
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
|
||||
@class([
|
||||
'gap-2 border cursor-pointer box group',
|
||||
'border-transparent' => $server->settings->is_reachable,
|
||||
'border-red-500' => !$server->settings->is_reachable,
|
||||
'border-transparent' => $server->settings->is_reachable && $server->settings->is_usable && !$server->settings->force_disabled,
|
||||
'border-red-500' => !$server->settings->is_reachable || $server->settings->force_disabled,
|
||||
])>
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="font-bold text-white">
|
||||
@@ -30,6 +30,9 @@
|
||||
@if (!$server->settings->is_usable)
|
||||
<span>Not usable by Coolify</span>
|
||||
@endif
|
||||
@if ($server->settings->force_disabled)
|
||||
<span>Disabled by the system</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
<div wire:loading.remove> No dynamic configurations found.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<div>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<div class="flex gap-2">
|
||||
<x-server.sidebar :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
@if ($server->isFunctional())
|
||||
@if ($server->isFunctional())
|
||||
<div class="flex gap-2">
|
||||
<x-server.sidebar :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<livewire:server.proxy :server="$server" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>Server is not validated. Validate first.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{{ data_get($resource, 'environment.name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap"><a class=""
|
||||
href="{{ $resource->link() }}">{{ $resource->name }} </a>
|
||||
href="{{ $resource->link() }}">{{ $resource->name }} <x-internal-link/></a>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ str($resource->type())->headline() }}</td>
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
|
||||
<div class="flex gap-2">
|
||||
<h2>Resources</h2>
|
||||
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div>
|
||||
@if (subscriptionProvider() === 'stripe')
|
||||
<div class="pt-4">
|
||||
<div class="pb-4">Your current Plan is: <strong
|
||||
<h2>Your current plan</h2>
|
||||
<div class="pb-4">Tier: <strong
|
||||
class="text-warning">{{ data_get(currentTeam(), 'subscription')->type() }}</strong></div>
|
||||
|
||||
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
|
||||
@@ -10,13 +11,19 @@
|
||||
<div>Subscription is active. Last invoice is
|
||||
{{ currentTeam()->subscription->stripe_invoice_paid ? 'paid' : 'not paid' }}.</div>
|
||||
@endif
|
||||
<h3 class="pt-4">Limits</h3>
|
||||
<div>Server: {{ $server_limits }}</div>
|
||||
<h3 class="pt-4">Actions</h3>
|
||||
<div>Number of paid servers: {{ $server_limits }}</div>
|
||||
<div>Currently active servers: {{ currentTeam()->servers->count() }}</div>
|
||||
@if (currentTeam()->serverOverflow())
|
||||
<div class="py-4"><span class="font-bold text-red-500">WARNING:</span> You must delete
|
||||
{{ currentTeam()->servers->count() - $server_limits }} servers,
|
||||
or upgrade your subscription. {{ currentTeam()->servers->count() - $server_limits }} servers will be
|
||||
deactivated.</div>
|
||||
@endif
|
||||
<h2 class="pt-4">Manage your subscription</h2>
|
||||
<div class="pb-4">Cancel, upgrade or downgrade your subscription.</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button wire:click='stripeCustomerPortal'>Manage your subscription on <svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="w-12" viewBox="0 0 512 214">
|
||||
<x-forms.button wire:click='stripeCustomerPortal'>Go to <svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-12" viewBox="0 0 512 214">
|
||||
<path fill="#635BFF"
|
||||
d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138c-33.85 0-54.33 28.73-54.33 64.854c0 42.808 24.179 64.426 58.88 64.426c16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823c-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658Zm-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756c8.675 0 17.92 6.685 17.92 22.756h-36.694Zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537l.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96c25.458 0 48.64-20.48 48.64-65.564c-.142-41.245-23.609-63.716-48.498-63.716Zm-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968c12.942 0 21.902 14.506 21.902 33.137c0 19.058-8.818 33.28-21.902 33.28ZM241.493 36.551l35.698-7.68V0l-35.698 7.538V36.55Zm0 10.809h35.698v124.444h-35.698V47.36Zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524Zm-71.112-41.386l-34.702 7.395l-.142 113.92c0 21.05 15.787 36.551 36.836 36.551c11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022l.142-30.862ZM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68c10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92c0 6.541-5.688 8.675-13.653 8.675c-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106c29.582 0 49.92-14.648 49.92-40.106c-.142-42.382-54.329-34.845-54.329-50.774Z" />
|
||||
</svg></x-forms.button>
|
||||
|
||||
@@ -22,16 +22,13 @@
|
||||
</x-forms.button>
|
||||
</x-slot:pro>
|
||||
<x-slot:ultimate>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme"><a class="text-white hover:no-underline" href="{{ config('coolify.contact') }}"
|
||||
target="_blank">
|
||||
Contact Us</a>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||
wire:click="subscribeStripe('ultimate-monthly')">
|
||||
{{ $isTrial ? 'Start Trial' : 'Subscribe' }}
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme"><a class="text-white hover:no-underline" href="{{ config('coolify.contact') }}"
|
||||
target="_blank">
|
||||
Contact Us</a>
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||
wire:click="subscribeStripe('ultimate-yearly')"> {{ $isTrial ? 'Start Trial' : 'Subscribe' }}
|
||||
</x-forms.button>
|
||||
</x-slot:ultimate>
|
||||
@endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\Server as ApiServer;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Jobs\ApplicationPullRequestUpdateJob;
|
||||
use App\Jobs\GithubAppPermissionJob;
|
||||
use App\Jobs\ServerLimitCheckJob;
|
||||
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||
use App\Jobs\SubscriptionTrialEndedJob;
|
||||
use App\Jobs\SubscriptionTrialEndsSoonJob;
|
||||
@@ -875,6 +876,15 @@ Route::post('/payments/stripe/events', function () {
|
||||
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
|
||||
$feedback = data_get($data, 'cancellation_details.feedback');
|
||||
$comment = data_get($data, 'cancellation_details.comment');
|
||||
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
|
||||
if (str($lookup_key)->contains('ultimate')) {
|
||||
$quantity = data_get($data, 'items.data.0.quantity', 10);
|
||||
$team = data_get($subscription, 'team');
|
||||
$team->update([
|
||||
'custom_server_limit' => $quantity,
|
||||
]);
|
||||
ServerLimitCheckJob::dispatch($team);
|
||||
}
|
||||
$subscription->update([
|
||||
'stripe_feedback' => $feedback,
|
||||
'stripe_comment' => $comment,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
## Do not modify this file. You will lost the ability to autoupdate!
|
||||
## Do not modify this file. You will lose the ability to autoupdate!
|
||||
|
||||
VERSION="1.0.4"
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
- DB_USERNAME=$SERVICE_USER_MYSQL
|
||||
- DB_PASSWORD=$SERVICE_PASSWORD_MYSQL
|
||||
- STATIC_CRON_TOKEN=$SERVICE_BASE64_CRONTOKEN
|
||||
- TRUSTED_PROXIES=*
|
||||
volumes:
|
||||
- firefly-upload:/var/www/html/storage/upload
|
||||
healthcheck:
|
||||
@@ -49,8 +50,20 @@ services:
|
||||
retries: 10
|
||||
volumes:
|
||||
- firefly-mysql-data:/var/lib/mysql
|
||||
# cron:
|
||||
# image: alpine
|
||||
# command: sh -c "echo \"0 3 * * * wget -qO- http://app:8080/api/v1/cron/$STATIC_CRON_TOKEN\" | crontab - && crond -f -L /dev/stdout"
|
||||
# environment:
|
||||
# - STATIC_CRON_TOKEN=$SERVICE_PASSWORD_32_CRONTOKEN
|
||||
cron:
|
||||
image: alpine
|
||||
entrypoint: ["/entrypoint.sh"]
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./entrypoint.sh
|
||||
target: /entrypoint.sh
|
||||
content: |
|
||||
#!/bin/sh
|
||||
# Substitute the environment variable into the cron command
|
||||
CRON_COMMAND="0 3 * * * wget -qO- http://firefly:8080/api/v1/cron/${STATIC_CRON_TOKEN}"
|
||||
# Add the cron command to the crontab
|
||||
echo "$CRON_COMMAND" | crontab -
|
||||
# Start the cron daemon in the foreground with logging to stdout
|
||||
crond -f -L /dev/stdout
|
||||
environment:
|
||||
- STATIC_CRON_TOKEN=$SERVICE_BASE64_CRONTOKEN
|
||||
|
||||
100
templates/compose/invoice-ninja.yaml
Normal file
100
templates/compose/invoice-ninja.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
# ignore: true
|
||||
# documentation: https://invoiceninja.github.io/selfhost.html
|
||||
# slogan: The leading open-source invoicing platform
|
||||
# tags: invoicing, billing, accounting, finance, self-hosted
|
||||
|
||||
services:
|
||||
invoice-ninja:
|
||||
image: invoiceninja/invoiceninja:5
|
||||
environment:
|
||||
- SERVICE_FQDN_INVOICENINJA
|
||||
- APP_ENV=production
|
||||
- APP_URL=${SERVICE_FQDN_INVOICENINJA}
|
||||
- APP_KEY=${SERVICE_BASE64_INVOICENINJA}
|
||||
- APP_DEBUG=false
|
||||
- REQUIRE_HTTPS=false
|
||||
- PHANTOMJS_PDF_GENERATION=false
|
||||
- PDF_GENERATOR=snappdf
|
||||
- TRUSTED_PROXIES=*
|
||||
- QUEUE_CONNECTION=database
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=${MYSQL_DATABASE:-invoice_ninja}
|
||||
- DB_USERNAME=${SERVICE_USER_MYSQL}
|
||||
- DB_PASSWORD=${SERVICE_PASSWORD_MYSQL}
|
||||
volumes:
|
||||
- invoice-ninja-public:/var/www/app/public
|
||||
- invoice-ninja-storage:/var/www/app/storage
|
||||
- type: bind
|
||||
source: ./php.ini
|
||||
target: /usr/local/etc/php/php.ini
|
||||
content: |
|
||||
session.auto_start = Off
|
||||
short_open_tag = Off
|
||||
|
||||
error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED
|
||||
|
||||
; opcache.enable=1
|
||||
; opcache.preload=/srv/www/invoiceninja/current/preload.php
|
||||
; opcache.preload_user=www-data
|
||||
|
||||
; ; The OPcache shared memory storage size.
|
||||
; opcache.max_accelerated_files=300000
|
||||
; opcache.validate_timestamps=1
|
||||
; opcache.revalidate_freq=30
|
||||
; opcache.jit_buffer_size=256M
|
||||
; opcache.jit=1205
|
||||
; opcache.memory_consumption=1024M
|
||||
|
||||
|
||||
post_max_size = 60M
|
||||
upload_max_filesize = 50M
|
||||
memory_limit=512M
|
||||
- type: bind
|
||||
source: ./php-cli.ini
|
||||
target: /usr/local/etc/php/php-cli.ini
|
||||
content: |
|
||||
session.auto_start = Off
|
||||
short_open_tag = Off
|
||||
|
||||
error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED
|
||||
|
||||
; opcache.enable_cli=1
|
||||
; opcache.fast_shutdown=1
|
||||
; opcache.memory_consumption=256
|
||||
; opcache.interned_strings_buffer=8
|
||||
; opcache.max_accelerated_files=4000
|
||||
; opcache.revalidate_freq=60
|
||||
; # http://symfony.com/doc/current/performance.html
|
||||
; realpath_cache_size = 4096K
|
||||
; realpath_cache_ttl = 600
|
||||
|
||||
memory_limit = 2G
|
||||
post_max_size = 60M
|
||||
upload_max_filesize = 50M
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
image: mariadb:lts
|
||||
environment:
|
||||
- MYSQL_USER=${SERVICE_USER_MYSQL}
|
||||
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-invoice_ninja}
|
||||
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"mysqladmin",
|
||||
"ping",
|
||||
"-h",
|
||||
"localhost",
|
||||
"-uroot",
|
||||
"-p${SERVICE_PASSWORD_MYSQLROOT}",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
volumes:
|
||||
- invoice-ninja-mysql-data:/var/lib/mysql
|
||||
1246
templates/compose/supabase.yaml
Normal file
1246
templates/compose/supabase.yaml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -8,6 +8,15 @@ test('ConvertCapAdd', function () {
|
||||
])->ray();
|
||||
});
|
||||
|
||||
test('ConvertIp', function () {
|
||||
$input = '--cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add SYS_ADMIN --ip 127.0.0.1 --ip 127.0.0.2';
|
||||
$output = convert_docker_run_to_compose($input);
|
||||
expect($output)->toBe([
|
||||
'cap_add' => ['NET_ADMIN', 'NET_RAW', 'SYS_ADMIN'],
|
||||
'ip' => ['127.0.0.1', '127.0.0.2']
|
||||
])->ray();
|
||||
});
|
||||
|
||||
test('ConvertPrivilegedAndInit', function () {
|
||||
$input = '---privileged --init';
|
||||
$output = convert_docker_run_to_compose($input);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "3.12.36"
|
||||
},
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.224"
|
||||
"version": "4.0.0-beta.228"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user