Compare commits

...

115 Commits

Author SHA1 Message Date
Andras Bacsai
1bfedf69f2 Merge pull request #1288 from coollabsio/next
v4.0.0-beta.67
2023-10-06 10:24:13 +02:00
Andras Bacsai
208fe7d87b revert 2023-10-06 10:11:04 +02:00
Andras Bacsai
a6d58b5d72 fix: decrease max horizon processes to get lower memory usage 2023-10-06 10:10:47 +02:00
Andras Bacsai
277b4276e6 small fixes 2023-10-06 10:08:50 +02:00
Andras Bacsai
f6adc9285a fix: services - do not remove unnecessary things for now 2023-10-06 10:08:46 +02:00
Andras Bacsai
f03bbe0e95 feat: basedir / monorepo initial support 2023-10-06 10:07:25 +02:00
Andras Bacsai
535375193c cloud: add shared email option to everyone 2023-10-05 14:46:39 +02:00
Andras Bacsai
d79c063fd6 ui: notifications 2023-10-05 14:44:17 +02:00
Andras Bacsai
35f45492e3 fix: email notifications subscription fixed 2023-10-05 14:37:16 +02:00
Andras Bacsai
ab8a7893d9 composer update 2023-10-05 13:57:59 +02:00
Andras Bacsai
76b8d048d4 fix: PR deployments use the first fqdn as base 2023-10-05 13:35:16 +02:00
Andras Bacsai
0e583334e7 version++ 2023-10-05 11:39:02 +02:00
Andras Bacsai
0ad8ca224f Merge pull request #1287 from coollabsio/next
v4.0.0-beta.66
2023-10-05 11:28:50 +02:00
Andras Bacsai
050e56f69a fix: traefik labelling in case of several http and https domain added 2023-10-05 11:27:50 +02:00
Andras Bacsai
6099ac11d9 version++ 2023-10-05 11:26:45 +02:00
Andras Bacsai
adac728a60 Merge pull request #1286 from coollabsio/next
v4.0.0-beta.65
2023-10-05 11:00:56 +02:00
Andras Bacsai
e2e64e36a0 feat: disable service, required version 2023-10-05 10:58:08 +02:00
Andras Bacsai
762af66cbf add deprecated templates in dev 2023-10-05 10:48:26 +02:00
Andras Bacsai
b08f525bd4 fix: move dev data to volumes to prevent permission issues 2023-10-05 08:54:03 +02:00
Andras Bacsai
5ae16b195c remove ray 2023-10-05 08:50:01 +02:00
Andras Bacsai
91db1953ff fix: delete event to deleting 2023-10-05 08:46:26 +02:00
Andras Bacsai
1c8f92d3b7 fix: remove SERVICE_ from deployable compose 2023-10-05 08:40:17 +02:00
Andras Bacsai
4075572dbc fix: visible version number 2023-10-05 08:32:20 +02:00
Andras Bacsai
2971e360d7 revert last commits 2023-10-04 18:06:25 +02:00
Andras Bacsai
32bb2780f2 rename composes 2023-10-04 15:43:29 +02:00
Andras Bacsai
af69575b29 fix: remove SERVICE_ stuff from raw compose
feat: multiport predefined compose
fix: use predefined name as prefix for fqdn
2023-10-04 15:40:08 +02:00
Andras Bacsai
d4a7d0d25f fix: traefik labels for multiport deployments 2023-10-04 15:39:23 +02:00
Andras Bacsai
45f9def0f6 fix: dev compose files 2023-10-04 14:40:33 +02:00
Andras Bacsai
5a90eed7ef fix: compose parser updated 2023-10-04 14:40:26 +02:00
Andras Bacsai
38e1f17edf feat: multiselect removable resources 2023-10-04 14:40:04 +02:00
Andras Bacsai
1651845e20 version++ 2023-10-04 11:54:56 +02:00
Andras Bacsai
38e96548b5 Merge pull request #1285 from coollabsio/next
v4.0.0-beta.64
2023-10-04 11:33:21 +02:00
Andras Bacsai
47e4126dca fix: compose magic 2023-10-04 11:32:27 +02:00
Andras Bacsai
e0b175ab07 version++ 2023-10-04 11:05:40 +02:00
Andras Bacsai
93ec785f4f Merge pull request #1284 from coollabsio/next
v4.0.0-beta.63
2023-10-04 11:01:05 +02:00
Andras Bacsai
e849addab8 dev: switch back to /data (volume errors) 2023-10-04 10:57:44 +02:00
Andras Bacsai
a5e6975dac fix: ui 2023-10-04 10:34:44 +02:00
Andras Bacsai
4ac8e1cc67 fix: services file/dir read from server
ui: fix storages layout
2023-10-04 09:58:39 +02:00
Andras Bacsai
1a5e3a7836 sync volume script 2023-10-03 15:48:07 +02:00
Andras Bacsai
4498d1ed4b fix: service logs visible if the whole service stack is not running 2023-10-03 15:08:23 +02:00
Andras Bacsai
c8b974820b Merge pull request #1282 from coollabsio/next
fix: volume names
2023-10-03 13:26:02 +02:00
Andras Bacsai
527373e297 fix: volume names 2023-10-03 13:25:41 +02:00
Andras Bacsai
a84be8dc33 Merge pull request #1281 from coollabsio/next
v4.0.0-beta.61
2023-10-03 13:14:39 +02:00
Andras Bacsai
bd856f7f67 fix: volume names in services 2023-10-03 13:14:11 +02:00
Andras Bacsai
8ff216e5fb revert 2023-10-03 12:20:09 +02:00
Andras Bacsai
5255311a2e hmm 2023-10-03 12:14:58 +02:00
Andras Bacsai
774a245e84 Merge pull request #1280 from coollabsio/next
v4.0.0-beta.60
2023-10-03 12:12:17 +02:00
Andras Bacsai
194675c838 update commands 2023-10-03 12:08:57 +02:00
Andras Bacsai
75862ca8de rename resource delete 2023-10-03 11:59:30 +02:00
Andras Bacsai
5580a4e704 feat: delete resource command 2023-10-03 11:56:56 +02:00
Andras Bacsai
51e601a303 fix: only use _ in volume names for services 2023-10-03 11:22:35 +02:00
Andras Bacsai
734e9fd68d more explanation 2023-10-03 11:01:01 +02:00
Andras Bacsai
09fc950ae8 fix: add _data to vite ignore 2023-10-03 11:00:52 +02:00
Andras Bacsai
cf6caa279d fix: ui 2023-10-03 09:02:36 +02:00
Andras Bacsai
68c976ab70 fix: show all storages in one place for services 2023-10-03 08:48:07 +02:00
Andras Bacsai
1560ab2a50 fix: UI 2023-10-03 08:22:03 +02:00
Andras Bacsai
e3a6458506 version++ 2023-10-03 08:16:19 +02:00
Andras Bacsai
1768b9374f fix: move /data to ./_data in dev 2023-10-03 08:14:49 +02:00
Andras Bacsai
51d0a30a6c Merge pull request #1279 from coollabsio/next
v4.0.0-beta.59
2023-10-02 18:03:07 +02:00
Andras Bacsai
9701c65297 fix: predefined content for files 2023-10-02 18:02:32 +02:00
Andras Bacsai
58e3bb2571 version++ 2023-10-02 18:01:24 +02:00
Andras Bacsai
31cbd1602d Merge pull request #1278 from coollabsio/next
v4.0.0-beta.58
2023-10-02 17:21:07 +02:00
Andras Bacsai
dd5723d596 service: uptime kume hc updated 2023-10-02 17:17:49 +02:00
Andras Bacsai
620f26a6f1 fix: add destination to new services 2023-10-02 17:12:50 +02:00
Andras Bacsai
540717e809 feat: attach Coolify defined networks to services 2023-10-02 16:57:55 +02:00
Andras Bacsai
d446cd4103 feat: reset root password 2023-10-02 16:38:05 +02:00
Andras Bacsai
7d1a76570c wip 2023-10-02 15:51:06 +02:00
Andras Bacsai
e18766ec21 fix: if waitlist is disabled, redirect to register 2023-10-02 15:09:57 +02:00
Andras Bacsai
3d0354cf7e add contribution guide 2023-10-02 14:58:29 +02:00
Andras Bacsai
af5b9fced1 Merge pull request #1277 from coollabsio/next
v4.0.0-beta.57
2023-10-02 14:19:24 +02:00
Andras Bacsai
ab5202515e fix: service status 2023-10-02 14:12:19 +02:00
Andras Bacsai
f863db7ea5 only 100 2023-10-02 14:01:54 +02:00
Andras Bacsai
3adc0bdd6e fix: only show last 1000 lines 2023-10-02 14:01:16 +02:00
Andras Bacsai
97027875bf feat: container logs 2023-10-02 13:38:16 +02:00
Andras Bacsai
fd9c13009f fix: always pull helper image in dev 2023-10-02 09:37:55 +02:00
Andras Bacsai
46a72fac47 Merge pull request #1276 from alexjustesen/patch-1
wee little missing spacing
2023-10-02 09:32:33 +02:00
Andras Bacsai
5d6ee04991 fix: new volumes for services should have - instead of _ 2023-10-02 09:18:32 +02:00
Andras Bacsai
d523becb29 fix: add uuid to volume names 2023-10-02 09:08:41 +02:00
Andras Bacsai
41672f75d0 fix: only parse expose in dockerfiles if ports_exposes is empty 2023-10-02 09:08:33 +02:00
Alex Justesen
acd8541e68 wee little spacing
noticed some missing spacing during the onboarding process.
2023-10-01 19:16:37 -04:00
Andras Bacsai
ed6af777a4 fix: show real volume names 2023-10-01 20:46:49 +02:00
Andras Bacsai
05f162f4e8 version++ 2023-10-01 20:29:35 +02:00
Andras Bacsai
5e0adc3777 Merge pull request #1275 from coollabsio/next
v4.0.0-beta.56
2023-10-01 18:16:24 +02:00
Andras Bacsai
7d06fc4403 fix: do not show subscription cancelled noti 2023-10-01 18:14:24 +02:00
Andras Bacsai
0e1bcceb8e feat: able to disable container healthchecks
fix: dockerfile based deployments have hc off by default
2023-10-01 18:14:13 +02:00
Andras Bacsai
a922f2fedf version++ 2023-10-01 18:13:34 +02:00
Andras Bacsai
1e0226c8ed Merge pull request #1274 from coollabsio/next
v4.0.0-beta.55
2023-10-01 17:40:31 +02:00
Andras Bacsai
5c45908087 fix: if app settings is not saved to db 2023-10-01 17:39:57 +02:00
Andras Bacsai
aefdc76805 fix: dockerfile expose is not overwritten 2023-10-01 17:27:12 +02:00
Andras Bacsai
b3c8c881b7 Revert "fix: services should have destination as well"
This reverts commit 9ab5a1f7bd.
2023-10-01 15:31:25 +02:00
Andras Bacsai
9ab5a1f7bd fix: services should have destination as well 2023-10-01 13:59:22 +02:00
Andras Bacsai
23968e7886 version++ 2023-10-01 13:28:33 +02:00
Andras Bacsai
390d24b6d7 Merge pull request #1273 from coollabsio/next
v4.0.0-beta.54
2023-10-01 12:36:10 +02:00
Andras Bacsai
e4296345b3 fix: public repo branch selection
fix: commit sha selection in source tabs
2023-10-01 12:29:50 +02:00
Andras Bacsai
bcffbe418b fix: preview deployments name, status etc 2023-10-01 12:02:44 +02:00
Andras Bacsai
bfbee4e78f version+ 2023-10-01 11:33:15 +02:00
Andras Bacsai
2bdb44dac3 Merge pull request #1272 from coollabsio/next
v4.0.0-beta.52
2023-09-30 20:55:24 +02:00
Andras Bacsai
4daa1b8c16 fix: coolify db backup 2023-09-30 20:54:05 +02:00
Andras Bacsai
febc399568 fix: not found base_branch in git webhooks 2023-09-30 20:47:07 +02:00
Andras Bacsai
9b9d4f9941 Merge pull request #1271 from coollabsio/next
v4.0.0-beta.52
2023-09-30 20:27:12 +02:00
Andras Bacsai
f49b87870c fix: backups are now working again 2023-09-30 20:26:42 +02:00
Andras Bacsai
ed49d4e3a0 Merge pull request #1264 from coollabsio/next
v4.0.0-beta.51
2023-09-30 16:02:09 +02:00
Andras Bacsai
7811c75139 fix: deploykey branch 2023-09-30 15:59:39 +02:00
Andras Bacsai
068a1b4bc4 fix: preselect branc on private repos 2023-09-30 15:57:30 +02:00
Andras Bacsai
c618d912db fix: if public repository does not have a main branch 2023-09-30 15:51:01 +02:00
Andras Bacsai
3d43f2127a fix: respect server fqdn 2023-09-30 15:39:40 +02:00
Andras Bacsai
79fde593a9 fix: service volume read from filesystem
fix: edit compose moved to dialog
2023-09-30 15:08:40 +02:00
Andras Bacsai
64ce41df0f fix: file/dir based volumes are now read from the server 2023-09-29 21:38:11 +02:00
Andras Bacsai
23e205b6cd fix: docker cleanup should be a job by server 2023-09-29 14:26:42 +02:00
Andras Bacsai
4161ea7eb6 fix: show source on all type of applications 2023-09-29 14:26:31 +02:00
Andras Bacsai
77037f8933 ui: fix previews to preview 2023-09-29 14:26:19 +02:00
Andras Bacsai
bdcc0c8de5 fix: only show manually added private keys on server view 2023-09-29 11:01:58 +02:00
Andras Bacsai
ac133875fa fix: remove private key in case you removed a github app 2023-09-29 11:01:40 +02:00
Andras Bacsai
f4819a849b remove plausible for now 2023-09-29 10:53:33 +02:00
Andras Bacsai
2cd1785a92 version++ 2023-09-29 10:53:28 +02:00
103 changed files with 2417 additions and 755 deletions

28
CONTRIBUTION.md Normal file
View File

@@ -0,0 +1,28 @@
# Contributing
> "First, thanks for considering to contribute to my project.
It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
## 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
- If necessary, set `USERID` & `GROUPID` accordingly (read in .env file).
## 3) Start & setup Coolify
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
## 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.

View File

@@ -4,12 +4,14 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
use Symfony\Component\Yaml\Yaml;
class StartService
{
use AsAction;
public function handle(Service $service)
{
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
@@ -21,6 +23,11 @@ class StartService
$commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} 2>/dev/null || true";
}
$activity = remote_process($commands, $service->server);
return $activity;
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Command;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
class ResourcesDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'resources:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete a resource from the database';
/**
* Execute the console command.
*/
public function handle()
{
$resource = select(
'What resource do you want to delete?',
['Application', 'Database', 'Service'],
);
if ($resource === 'Application') {
$this->deleteApplication();
} elseif ($resource === 'Database') {
$this->deleteDatabase();
} elseif ($resource === 'Service') {
$this->deleteService();
}
}
private function deleteApplication()
{
$applications = Application::all();
if ($applications->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$applicationsToDelete = multiselect(
'What application do you want to delete?',
$applications->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($applicationsToDelete as $application) {
$toDelete = $applications->where('name', $application)->first();
$toDelete->delete();
}
}
private function deleteDatabase()
{
$databases = StandalonePostgresql::all();
if ($databases->count() === 0) {
$this->error('There are no databases to delete.');
return;
}
$databasesToDelete = multiselect(
'What database do you want to delete?',
$databases->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first();
$toDelete->delete();
}
}
private function deleteService()
{
$services = Service::all();
if ($services->count() === 0) {
$this->error('There are no services to delete.');
return;
}
$servicesToDelete = multiselect(
'What service do you want to delete?',
$services->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first();
$toDelete->delete();
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password;
class UsersResetRoot extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:reset-root';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset Root Password';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: ');
$passwordAgain = password('Again');
if ($password != $passwordAgain) {
$this->error('Passwords do not match.');
return;
}
$this->info('Updating root password...');
try {
User::find(0)->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root password.');
return;
}
}
}

View File

@@ -27,14 +27,23 @@ class Kernel extends ConsoleKernel
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->cleanup_servers($schedule);
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
// $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->cleanup_servers($schedule);
}
}
private function cleanup_servers($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
}
}
private function check_resources($schedule)

View File

@@ -6,7 +6,7 @@ use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Cache;
use App\Models\StandaloneDocker;
use Illuminate\Support\Str;
class ProjectController extends Controller
@@ -69,17 +69,19 @@ class ProjectController extends Controller
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
ray($oneClickServiceName);
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
@@ -90,6 +92,7 @@ class ProjectController extends Controller
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
// TODO: make it shared with Service.php
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);

View File

@@ -76,7 +76,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
Team::find(currentTeam()->id)->update([
'show_boarding' => false
]);
ray(currentTeam());
refreshSession();
return redirect()->route('dashboard');
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Dev;
use Livewire\Component;
class Compose extends Component
{
public string $compose = '';
public string $base64 = '';
public $services;
public function mount() {
$this->services = getServiceTemplates();
}
public function setService(string $selected) {
$this->base64 = data_get($this->services, $selected . '.compose');
if ($this->base64) {
$this->compose = base64_decode($this->base64);
}
}
public function updatedCompose($value) {
$this->base64 = base64_encode($value);
}
public function render()
{
return view('livewire.dev.compose');
}
}

View File

@@ -22,9 +22,6 @@ class General extends Component
public string $git_branch;
public string|null $git_commit_sha;
public string $build_pack;
public string|null $wildcard_domain = null;
public string|null $server_wildcard_domain = null;
public string|null $global_wildcard_domain = null;
public bool $is_static;
public bool $is_git_submodules_enabled;
@@ -91,52 +88,31 @@ class General extends Component
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->checkWildCardDomain();
$this->emit('success', 'Application settings updated!');
}
protected function checkWildCardDomain()
{
$coolify_instance_settings = InstanceSettings::get();
$this->server_wildcard_domain = data_get($this->application, 'destination.server.settings.wildcard_domain');
$this->global_wildcard_domain = data_get($coolify_instance_settings, 'wildcard_domain');
$this->wildcard_domain = $this->server_wildcard_domain ?? $this->global_wildcard_domain ?? null;
}
public function getWildcardDomain() {
$server = data_get($this->application, 'destination.server');
if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
}
public function mount()
{
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->checkWildCardDomain();
}
public function generateGlobalRandomDomain()
{
// Set wildcard domain based on Global wildcard domain
$url = Url::fromString($this->global_wildcard_domain);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$this->application->fqdn = $scheme . '://' . $this->application->uuid . '.' . $host . $path;
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
public function generateServerRandomDomain()
{
// Set wildcard domain based on Server wildcard domain
$url = Url::fromString($this->server_wildcard_domain);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$this->application->fqdn = $scheme . '://' . $this->application->uuid . '.' . $host . $path;
$this->application->save();
$this->emit('success', 'Application settings updated!');
if (data_get($this->application,'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
}
public function submit()
@@ -151,7 +127,7 @@ class General extends Component
}
if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
if ($port && !$this->application->ports_exposes) {
$this->application->ports_exposes = $port;
}
}

View File

@@ -29,7 +29,8 @@ class Form extends Component
public function generate_real_url()
{
if (data_get($this->application, 'fqdn')) {
$url = Url::fromString($this->application->fqdn);
$firstFqdn = Str::of($this->application->fqdn)->before(',');
$url = Url::fromString($firstFqdn);
$host = $url->getHost();
$this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host);
}

View File

@@ -72,8 +72,7 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generateApplicationContainerName($this->application);
ray('Stopping container: ' . $container_name);
$container_name = generateApplicationContainerName($this->application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();

View File

@@ -10,7 +10,7 @@ class CreateScheduledBackup extends Component
public $database;
public $frequency;
public bool $enabled = true;
public bool $save_s3 = true;
public bool $save_s3 = false;
public $s3_storage_id;
public $s3s;

View File

@@ -50,8 +50,9 @@ class General extends Component
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}

View File

@@ -32,8 +32,6 @@ class DockerCompose extends Component
- type: volume
source: mydata
target: /data
volume:
nocopy: true
- type: bind
source: ./var/lib/ghost/data
target: /data

View File

@@ -11,6 +11,7 @@ use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Spatie\Url\Url;
class GithubPrivateRepository extends Component
{
@@ -95,6 +96,7 @@ class GithubPrivateRepository extends Component
$this->loadBranchByPage();
}
}
$this->selected_branch_name = data_get($this->branches,'0.name');
}
protected function loadBranchByPage()
@@ -144,8 +146,9 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save();

View File

@@ -46,7 +46,6 @@ class GithubPrivateRepositoryDeployKey extends Component
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
private string $git_branch;
public function mount()
{
@@ -96,7 +95,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$application_init = [
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->git_branch,
'git_branch' => $this->branch,
'git_full_url' => "git@$this->git_host:$this->git_repository.git",
'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
@@ -112,8 +111,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->name = generate_random_name($application->uuid);
$application->save();
@@ -132,11 +131,6 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
if ($this->branch) {
$this->git_branch = $this->branch;
} else {
$this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
}
if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();

View File

@@ -26,6 +26,10 @@ class PublicGitRepository extends Component
public string $git_branch = 'main';
public int $rate_limit_remaining = 0;
public $rate_limit_reset = 0;
private object $repository_url_parsed;
public GithubApp|GitlabApp|null $git_source = null;
public string $git_host;
public string $git_repository;
protected $rules = [
'repository_url' => 'required|url',
'port' => 'required|numeric',
@@ -38,10 +42,6 @@ class PublicGitRepository extends Component
'is_static' => 'static',
'publish_directory' => 'publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
public function mount()
{
@@ -76,13 +76,15 @@ class PublicGitRepository extends Component
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Throwable $e) {
return handleError($e, $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
$this->get_branch();
} catch (\Throwable $e) {
ray($e->getMessage());
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
$this->get_branch();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} else {
return handleError($e, $this);
}
}
@@ -122,9 +124,6 @@ class PublicGitRepository extends Component
$project_uuid = $this->parameters['project_uuid'];
$environment_name = $this->parameters['environment_name'];
$this->get_git_source();
$this->git_branch = $this->selected_branch ?? $this->git_branch;
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
@@ -156,8 +155,8 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save();

View File

@@ -45,6 +45,9 @@ CMD ["nginx", "-g", "daemon off;"]
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$port = get_port_from_dockerfile($this->dockerfile);
if (!$port) {
$port = 80;
}
$application = Application::create([
'name' => 'dockerfile-' . new Cuid2(7),
'repository_project_id' => 0,
@@ -56,14 +59,15 @@ CMD ["nginx", "-g", "daemon off;"]
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'health_check_enabled' => false,
'source_id' => 0,
'source_type' => GithubApp::class
]);
$sslip = sslip($destination->server);
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->update([
'name' => 'dockerfile-' . $application->uuid,
'fqdn' => "http://{$application->uuid}.$sslip"
'fqdn' => $fqdn
]);
redirect()->route('project.application.configuration', [

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class ComposeModal extends Component
{
public string $raw;
public string $actual;
public function render()
{
return view('livewire.project.service.compose-modal');
}
public function submit() {
$this->emit('warning', "Saving new docker compose...");
$this->emit('saveCompose', $this->raw);
}
}

View File

@@ -13,6 +13,7 @@ class FileStorage extends Component
public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service;
public string $fs_path;
public ?string $workdir = null;
protected $rules = [
'fileStorage.is_directory' => 'required',
@@ -23,22 +24,28 @@ class FileStorage extends Component
public function mount()
{
$this->service = $this->fileStorage->service;
$this->fs_path = Str::of($this->fileStorage->fs_path)->beforeLast('/');
$file = Str::of($this->fileStorage->fs_path)->afterLast('/');
if (Str::of($this->fs_path)->startsWith('.')) {
$this->fs_path = Str::of($this->fs_path)->after('.');
$this->fs_path = $this->service->service->workdir() . $this->fs_path . "/" . $file;
}
if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
$this->workdir = $this->service->service->workdir();
$this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
} else {
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
}
public function submit()
{
$original = $this->fileStorage->getOriginal();
try {
$this->validate();
if ($this->fileStorage->is_directory) {
$this->fileStorage->content = null;
}
$this->fileStorage->save();
$this->service->saveFileVolumes();
$this->fileStorage->saveStorageOnServer($this->service);
$this->emit('success', 'File updated successfully.');
} catch (\Throwable $e) {
$this->fileStorage->setRawAttributes($original);
$this->fileStorage->save();
return handleError($e, $this);
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Http\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
class Index extends Component
@@ -20,7 +19,26 @@ class Index extends Component
'service.name' => 'required',
'service.description' => 'nullable',
];
public function checkStatus() {
protected $listeners = ["saveCompose"];
public function render()
{
return view('livewire.project.service.index');
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack();
}
@@ -35,17 +53,8 @@ class Index extends Component
$database->refresh();
});
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
}
public function render()
{
return view('livewire.project.service.index');
}
public function submit()
{
try {

View File

@@ -21,7 +21,6 @@ class Navbar extends Component
}
public function serviceStatusUpdated()
{
ray('serviceStatusUpdated');
$this->check_status();
}
public function check_status()

View File

@@ -20,16 +20,23 @@ class Show extends Component
public function mount()
{
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
try {
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
$this->serviceApplication->getFilesFromServer();
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer();
}
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function generateDockerCompose()
{

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
class Storage extends Component
{
protected $listeners = ['addNewVolume'];
public $resource;
public function render()
{
return view('livewire.project.service.storage');
}
public function addNewVolume($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
$this->emit('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use App\Models\Server;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class GetLogs extends Component
{
public string $outputs = '';
public string $errors = '';
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= $output;
}
public function instantSave()
{
}
public function getLogs($refresh = false)
{
if ($this->container) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($refresh) {
$this->outputs = '';
}
Process::run($sshCommand, function (string $type, string $output) {
$this->doSomethingWithThisChunkOfOutput($output);
});
}
}
public function render()
{
return view('livewire.project.shared.get-logs');
}
}

View File

@@ -9,6 +9,7 @@ class HealthChecks extends Component
public $resource;
protected $rules = [
'resource.health_check_enabled' => 'boolean',
'resource.health_check_path' => 'string',
'resource.health_check_port' => 'nullable|string',
'resource.health_check_host' => 'string',
@@ -22,12 +23,19 @@ class HealthChecks extends Component
'resource.health_check_start_period' => 'integer',
];
public function instantSave()
{
$this->resource->save();
$this->emit('success', 'Health check updated.');
}
public function submit()
{
try {
$this->validate();
$this->resource->save();
$this->emit('saved');
$this->emit('success', 'Health check updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|StandalonePostgresql|Service $resource;
public Server $server;
public ?string $container = null;
public $parameters;
public $query;
public $status;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id);
if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names');
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->server;
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid;
}
}
public function render()
{
return view('livewire.project.shared.logs');
}
}

View File

@@ -6,6 +6,7 @@ use Livewire\Component;
class Add extends Component
{
public $uuid;
public $parameters;
public string $name;
public string $mount_path;
@@ -31,8 +32,9 @@ class Add extends Component
public function submit()
{
$this->validate();
$this->emitUp('submit', [
'name' => $this->name,
$name = $this->uuid . '-' . $this->name;
$this->emit('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);

View File

@@ -8,28 +8,10 @@ use Livewire\Component;
class All extends Component
{
public $resource;
protected $listeners = ['refreshStorages', 'submit'];
protected $listeners = ['refreshStorages'];
public function refreshStorages()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -11,7 +11,7 @@ class Show extends Component
public LocalPersistentVolume $storage;
public bool $isReadOnly = false;
public ?string $modalId = null;
public ?string $realName = null;
public bool $isFirst = true;
protected $rules = [
'storage.name' => 'required|string',
@@ -26,11 +26,6 @@ class Show extends Component
public function mount()
{
if ($this->storage->resource_type === 'App\Models\ServiceApplication' || $this->storage->resource_type === 'App\Models\ServiceDatabase') {
$this->realName = "{$this->storage->service->service->uuid}_{$this->storage->name}";
} else {
$this->realName = $this->storage->name;
}
$this->modalId = new Cuid2(7);
}

View File

@@ -23,6 +23,9 @@ class Index extends Component
}
public function mount()
{
if (config('coolify.waitlist') == false) {
return redirect()->route('register');
}
$this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count();
if (isDev()) {

View File

@@ -52,9 +52,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $container_name;
private string|null $currently_running_container_name = null;
private string $basedir;
private string $workdir;
private string $configuration_dir;
private string $build_workdir;
private string $build_image_name;
private string $production_image_name;
private bool $is_debug_enabled;
@@ -84,12 +84,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server;
$this->workdir = "/artifacts/{$this->deployment_uuid}";
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/');
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application);
ray($this->basedir,$this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -97,7 +98,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
$template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn);
$host = $url->getHost();
@@ -159,12 +162,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
);
}
$this->execute_remote_command(
[
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
"hidden" => true,
]
);
// $this->execute_remote_command(
// [
// "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
// "hidden" => true,
// ]
// );
}
}
private function deploy_docker_compose()
@@ -231,7 +234,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
@@ -256,7 +259,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) {
$this->execute_remote_command([
@@ -284,7 +287,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update()
{
if (count($this->application->ports_mappings_array) > 0){
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"],
);
@@ -301,7 +304,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function health_check()
{
ray('New container name: ', $this->container_name);
if ($this->application->isHealthcheckDisabled()) {
$this->newVersionIsHealthy = true;
return;
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
$this->execute_remote_command(
@@ -348,7 +355,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
]);
@@ -373,9 +380,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image()
{
$pull = "--pull=always";
if (isDev()) {
$pull = "--pull=never";
}
$helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
@@ -388,7 +392,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"hidden" => true,
],
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
],
);
}
@@ -399,13 +403,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$this->execute_remote_command(
[
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}. '"
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[
$this->importing_git_repository()
$this->importing_git_repository(), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git rev-parse HEAD"),
executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git rev-parse HEAD"),
"hidden" => true,
"save" => "git_commit_sha"
],
@@ -429,16 +433,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else {
$github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}"));
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}"));
}
if ($this->pull_request_id !== 0) {
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
@@ -460,13 +464,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function set_git_import_settings($git_clone_command)
{
if ($this->application->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
}
if ($this->application->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive";
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
}
if ($this->application->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull";
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
}
return $git_clone_command;
}
@@ -474,17 +478,24 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function cleanup_git()
{
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "rm -fr {$this->workdir}/.git")],
[executeInDocker($this->deployment_uuid, "rm -fr {$this->basedir}/.git")],
);
}
private function generate_nixpacks_confs()
{
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command(
[
"echo -n Running: $nixpacks_command",
],
[$this->nixpacks_build_cmd()],
[executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
[executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")]
);
@@ -493,7 +504,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd()
{
$this->generate_env_variables();
$nixpacks_command = "nixpacks build -o {$this->workdir} {$this->env_args} --no-error-without-start";
$nixpacks_command = "nixpacks build --no-cache -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@@ -504,7 +515,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$nixpacks_command .= " --install-cmd \"{$this->application->install_command}\"";
}
$nixpacks_command .= " {$this->workdir}";
return executeInDocker($this->deployment_uuid, $nixpacks_command);
return $nixpacks_command;
}
private function generate_env_variables()
@@ -571,6 +582,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
}
@@ -622,14 +636,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_environment_variables($ports)
{
$environment_variables = collect();
ray('Generate Environment Variables')->green();
// ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) {
ray($this->application->runtime_environment_variables)->green();
// ray($this->application->runtime_environment_variables)->green();
foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
ray($this->application->runtime_environment_variables_preview)->green();
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
@@ -667,7 +681,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function build_image()
{
$this->execute_remote_command([
"echo -n 'Building docker image for your application.'",
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
if ($this->application->settings->is_static) {

View File

@@ -108,9 +108,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId) {
if (str_contains($labelId, '-pr-')) {
$previewId = (int) Str::after($labelId, '-pr-');
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
$applicationId = (int) Str::before($labelId, '-pr-');
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $previewId)->first();
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;

View File

@@ -61,7 +61,8 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
try {
if (data_get($this->database, 'status') !== 'running') {
$status = Str::of(data_get($this->database, 'status'));
if (!$status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
return;
}
@@ -89,7 +90,6 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$this->upload_to_s3();
}
$this->save_backup_logs();
// TODO: Notify user
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());

View File

@@ -16,65 +16,66 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500;
public $timeout = 1000;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
public function middleware(): array
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct()
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server)
{
}
public function handle(): void
{
$queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
if ($queue->count() > 0) {
$queuedCount = 0;
$this->server->applications()->each(function ($application) use ($queuedCount) {
$count = data_get($application->deployments(), 'count', 0);
$queuedCount += $count;
});
if ($queuedCount > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
return;
}
try {
// ray()->showQueries()->color('orange');
$servers = Server::all();
foreach ($servers as $server) {
if (
!$server->isFunctional()
) {
continue;
}
if (isDev()) {
$this->dockerRootFilesystem = "/";
if (!$this->server->isFunctional()) {
return;
}
if (isDev()) {
$this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$this->server,
false
);
}
if (!$this->dockerRootFilesystem) {
return;
}
$this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $this->server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
instant_remote_process(['docker builder prune -af'], $this->server);
$usageAfter = $this->getFilesystemUsage();
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$server,
false
);
}
if (!$this->dockerRootFilesystem) {
continue;
}
$this->usageBefore = $this->getFilesystemUsage($server);
if ($this->usageBefore >= $server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server);
$usageAfter = $this->getFilesystemUsage($server);
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
} else {
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
}
} else {
ray('No need to clean up ' . $server->name)->color('orange');
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange');
}
} else {
ray('No need to clean up ' . $this->server->name)->color('orange');
}
} catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
@@ -83,8 +84,8 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
}
}
private function getFilesystemUsage(Server $server)
private function getFilesystemUsage()
{
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this->server, false);
}
}

View File

@@ -18,10 +18,16 @@ class Application extends BaseModel
]);
});
static::deleting(function ($application) {
// Stop Container
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server);
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
}
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
@@ -38,6 +44,10 @@ class Application extends BaseModel
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function type()
{
@@ -226,9 +236,17 @@ class Application extends BaseModel
}
public function git_based(): bool
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->dockercompose || $this->build_pack === 'dockercompose') {
if ($this->dockerfile) {
return false;
}
return true;
}
public function isHealthcheckDisabled(): bool
{
if (data_get($this, 'dockerfile') || data_get($this, 'build_pack') === 'dockerfile' || data_get($this, 'health_check_enabled') === false) {
ray('dockerfile');
return true;
}
return false;
}
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
class GithubApp extends BaseModel
{
protected $guarded = [];
protected $appends = ['type'];
protected $casts = [
@@ -17,6 +18,7 @@ class GithubApp extends BaseModel
'webhook_secret',
];
static public function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@@ -34,6 +36,7 @@ class GithubApp extends BaseModel
if ($applications_count > 0) {
throw new \Exception('You cannot delete this GitHub App because it is in use by ' . $applications_count . ' application(s). Delete them first.');
}
$github_app->privateKey()->delete();
});
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class LocalFileVolume extends BaseModel
{
@@ -13,4 +14,35 @@ class LocalFileVolume extends BaseModel
{
return $this->morphTo('resource');
}
public function saveStorageOnServer(ServiceApplication|ServiceDatabase $service)
{
$workdir = $service->service->workdir();
$server = $service->service->server;
$commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
]);
$fileVolume = $this;
$path = Str::of(data_get($fileVolume, 'fs_path'));
$content = data_get($fileVolume, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$path = $workdir . $path;
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
if ($isFile == 'OK' && $fileVolume->is_directory) {
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
} else if ($isDir == 'OK' && !$fileVolume->is_directory) {
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.");
}
if (!$fileVolume->is_directory && $isDir == 'NOK') {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path");
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
ray($commands->toArray());
return instant_remote_process($commands, $server);
}
}

View File

@@ -22,7 +22,7 @@ class Project extends BaseModel
'project_id' => $project->id,
]);
});
static::deleted(function ($project) {
static::deleting(function ($project) {
$project->environments()->delete();
$project->settings()->delete();
});

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
@@ -120,7 +121,20 @@ class Server extends BaseModel
{
return $this->hasMany(Service::class);
}
public function getIp(): Attribute
{
return Attribute::make(
get: function () {
if (isDev()) {
return '127.0.0.1';
}
if ($this->ip === 'host.docker.internal') {
return base_ip();
}
return $this->ip;
}
);
}
public function previews()
{
return $this->destinations()->map(function ($standaloneDocker) {

View File

@@ -8,7 +8,6 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
use Spatie\Url\Url;
class Service extends BaseModel
{
@@ -17,9 +16,10 @@ class Service extends BaseModel
protected static function booted()
{
static::deleted(function ($service) {
static::deleting(function ($service) {
$storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
@@ -27,6 +27,7 @@ class Service extends BaseModel
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
@@ -63,6 +64,10 @@ class Service extends BaseModel
{
return $this->hasMany(ServiceDatabase::class);
}
public function destination()
{
return $this->morphTo();
}
public function environment()
{
return $this->belongsTo(Environment::class);
@@ -112,7 +117,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
// ray()->clearAll();
ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -124,9 +129,16 @@ class Service extends BaseModel
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$definedNetwork = $this->uuid;
$generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -196,6 +208,12 @@ class Service extends BaseModel
}
}
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
@@ -231,18 +249,24 @@ class Service extends BaseModel
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
$topLevelNetworks->put($definedNetwork, [
'name' => $definedNetwork,
'external' => true
]);
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
'external' => true
]);
}
}
$networks = $serviceNetworks->toArray();
$networks = array_merge($networks, [$definedNetwork]);
foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
$network
]);
}
data_set($service, 'networks', $networks);
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
foreach ($serviceVolumes as $volume) {
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
$type = null;
$source = null;
$target = null;
@@ -264,16 +288,19 @@ class Service extends BaseModel
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$content = data_get($foundConfig, 'content');
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
continue;
return $volume;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
continue;
return $volume;
}
LocalFileVolume::updateOrCreate(
[
@@ -291,7 +318,19 @@ class Service extends BaseModel
]
);
} else if ($type->value() === 'volume') {
$topLevelVolumes->put($source->value(), null);
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} else if (is_array($volume)) {
data_set($volume, 'source', $name);
}
$topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
@@ -299,15 +338,17 @@ class Service extends BaseModel
'resource_type' => get_class($savedService)
],
[
'name' => Str::slug($source, '-'),
'name' => $name,
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
$savedService->saveFileVolumes();
}
$savedService->getFilesFromServer(isInit: true);
return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
}
// Add env_file with at least .env to the service
@@ -343,10 +384,23 @@ class Service extends BaseModel
$value = Str::of($variable);
}
if ($key->startsWith('SERVICE_FQDN')) {
if (is_null(data_get($savedService, 'fqdn'))) {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) {
if ($isNew) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
@@ -358,13 +412,24 @@ class Service extends BaseModel
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
$fqdn = "http://$containerName.$sslip$path";
$fqdn = "$fqdn$path";
}
if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue;
}
if ($value?->startsWith('$')) {
@@ -379,11 +444,17 @@ class Service extends BaseModel
$forService = $value->afterLast('_');
$generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
@@ -392,10 +463,11 @@ class Service extends BaseModel
'is_preview' => false,
]);
}
if (!$isDatabase) {
$savedService->fqdn = $fqdn;
$savedService->save();
if ($command->value() === 'FQDN') {
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
} else {
switch ($command) {
@@ -468,25 +540,27 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, $containerName, true));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true));
}
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment
$withoutServiceEnvs = collect([]);
collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
if (!Str::of($key)->startsWith('$SERVICE_')) {
$withoutServiceEnvs->put($key, $value);
}
});
data_set($service, 'environment', $withoutServiceEnvs->toArray());
// $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
// ray($key, $value);
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
// $k = Str::of($value)->before("=");
// $v = Str::of($value)->after("=");
// $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service;
});
$finalServices = [
@@ -496,7 +570,7 @@ class Service extends BaseModel
'networks' => $topLevelNetworks->toArray(),
];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$this->saveComposeConfigs();
return collect([]);

View File

@@ -36,8 +36,8 @@ class ServiceApplication extends BaseModel
);
}
public function saveFileVolumes()
public function getFilesFromServer(bool $isInit = false)
{
saveFileVolumesHelper($this);
getFilesystemVolumesFromServer($this, $isInit);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceDatabase extends BaseModel
{
@@ -26,8 +25,8 @@ class ServiceDatabase extends BaseModel
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function saveFileVolumes()
public function getFilesFromServer(bool $isInit = false)
{
saveFileVolumesHelper($this);
getFilesystemVolumesFromServer($this, $isInit);
}
}

View File

@@ -28,10 +28,21 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true
]);
});
static::deleted(function ($database) {
static::deleting(function ($database) {
// Stop Container
instant_remote_process(
["docker rm -f {$database->uuid}"],
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
});
}
@@ -65,6 +76,11 @@ class StandalonePostgresql extends BaseModel
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();

View File

@@ -15,7 +15,6 @@ class EmailChannel
try {
$this->bootConfigs($notifiable);
$recepients = $notifiable->getRecepients($notification);
ray($recepients);
if (count($recepients) === 0) {
throw new Exception('No email recipients found');
}

View File

@@ -1,12 +1,12 @@
<?php
use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection
{
@@ -104,16 +104,16 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return data_get($container[0], 'State.Status', 'exited');
}
function generateApplicationContainerName(Application $application)
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
{
$now = now()->format('Hisu');
if ($application->pull_request_id !== 0 && $application->pull_request_id !== null) {
return $application->uuid . '-pr-' . $application->pull_request_id;
if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $application->uuid . '-pr-' . $pull_request_id;
} else {
return $application->uuid . '-' . $now;
}
}
function get_port_from_dockerfile($dockerfile): int
function get_port_from_dockerfile($dockerfile): int|null
{
$dockerfile_array = explode("\n", $dockerfile);
$found_exposed_port = null;
@@ -127,7 +127,7 @@ function get_port_from_dockerfile($dockerfile): int
if ($found_exposed_port) {
return (int)$found_exposed_port->value();
}
return 80;
return null;
}
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application', $subType = null, $subId = null)
@@ -147,20 +147,20 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
return $labels;
}
function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled)
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
foreach ($domains as $domain) {
$uuid = (string)new Cuid2(7);
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
$slug = Str::slug($host . $path);
$http_label = "{$container_name}-{$slug}-http";
$https_label = "{$container_name}-{$slug}-https";
$http_label = "{$uuid}-http";
$https_label = "{$uuid}-https";
if ($schema === 'https') {
// Set labels for https
@@ -207,10 +207,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
{
$pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application);
$container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id;
if ($pull_request_id !== 0) {
$appId = $appId . '-pr-' . $application->pull_request_id;
if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $pull_request_id;
}
$labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id));
@@ -221,7 +221,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled));
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled));
}
return $labels->all();
}

View File

@@ -64,40 +64,68 @@ function serviceStatus(Service $service)
}
return 'exited';
}
function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService)
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false)
{
// TODO: make this async
try {
$workdir = $oneService->service->workdir();
$server = $oneService->service->server;
$applicationFileVolume = $oneService->fileStorages()->get();
$fileVolumes = $oneService->fileStorages()->get();
$commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
"cd "
]);
foreach ($applicationFileVolume as $fileVolume) {
$path = Str::of($fileVolume->fs_path);
if ($fileVolume->is_directory) {
$commands->push("test -f $path && rm -f $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
continue;
instant_remote_process($commands, $server);
foreach ($fileVolumes as $fileVolume) {
$path = Str::of(data_get($fileVolume, 'fs_path'));
$content = data_get($fileVolume, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$fileLocation = $workdir . $path;
} else {
$fileLocation = $path;
}
$content = $fileVolume->content;
$dir = $path->beforeLast('/');
if ($dir->startsWith('.')) {
$dir = $dir->after('.');
$dir = $workdir . $dir;
ray($path,$fileLocation);
// Exists and is a file
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
// Exists and is a directory
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($isFile == 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
$fileVolume->content = $filesystemContent;
$fileVolume->is_directory = false;
$fileVolume->save();
} else if ($isDir == 'OK') {
// If its a directory & exists
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
} else if ($isFile == 'NOK' && $isDir == 'NOK' && !$fileVolume->is_directory && $isInit && $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false;
$fileVolume->save();
$content = base64_encode($content);
$dir = Str::of($fileLocation)->dirname();
instant_remote_process([
"mkdir -p $dir",
"echo '$content' | base64 -d > $fileLocation"
], $server);
} else if ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
}
$content = base64_encode($content);
$commands->push("test -d $path && rm -rf $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $dir > /dev/null 2>&1 || true");
$commands->push("echo '$content' | base64 -d > $path");
}
return instant_remote_process($commands, $server);
} catch (\Throwable $e) {
return handleError($e);
}
}
function updateCompose($resource) {
function updateCompose($resource)
{
try {
$name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
@@ -109,13 +137,18 @@ function updateCompose($resource) {
// Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
ray($variableName);
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv){
if ($generatedEnv) {
$generatedEnv->value = $resource->fqdn;
$generatedEnv->save();
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;

View File

@@ -14,6 +14,7 @@ use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;
@@ -352,13 +353,14 @@ function setNotificationChannels($notifiable, $event)
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
@@ -395,16 +397,29 @@ function data_get_str($data, $key, $default = null): Stringable
return Str::of($str);
}
function generateFqdn(Server $server, string $random)
{
$wildcard = data_get($server, 'settings.wildcard_domain');
if (is_null($wildcard) || $wildcard === '') {
$wildcard = sslip($server);
}
$url = Url::fromString($wildcard);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$finalFqdn = "$scheme://{$random}.$host$path";
return $finalFqdn;
}
function sslip(Server $server)
{
if (isDev()) {
return "127.0.0.1.sslip.io";
return "http://127.0.0.1.sslip.io";
}
if ($server->ip === 'host.docker.internal') {
$baseIp = base_ip();
return "$baseIp.sslip.io";
return "http://$baseIp.sslip.io";
}
return "{$server->ip}.sslip.io";
return "http://{$server->ip}.sslip.io";
}
function getServiceTemplates()
@@ -412,6 +427,16 @@ function getServiceTemplates()
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
$deprecated = File::get(base_path('templates/deprecated.json'));
$deprecated = collect(json_decode($deprecated))->sortKeys();
$services = $services->merge($deprecated);
$version = config('version');
$services = $services->map(function ($service) use ($version) {
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
$service->disabled = true;
}
return $service;
});
} else {
$services = Http::get(config('constants.services.official'));
if ($services->failed()) {

313
composer.lock generated
View File

@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.281.12",
"version": "3.283.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "22a92f08758db2b152843ea0875eeee5a467d8ff"
"reference": "5084c03431ecda0003e35d7fc7a12eeca4242685"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/22a92f08758db2b152843ea0875eeee5a467d8ff",
"reference": "22a92f08758db2b152843ea0875eeee5a467d8ff",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5084c03431ecda0003e35d7fc7a12eeca4242685",
"reference": "5084c03431ecda0003e35d7fc7a12eeca4242685",
"shasum": ""
},
"require": {
@@ -151,9 +151,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.281.12"
"source": "https://github.com/aws/aws-sdk-php/tree/3.283.0"
},
"time": "2023-09-22T18:12:27+00:00"
"time": "2023-10-04T18:08:32+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -603,16 +603,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.6.7",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "8e0e268052b4a8974cb00215bb2892787021614f"
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/8e0e268052b4a8974cb00215bb2892787021614f",
"reference": "8e0e268052b4a8974cb00215bb2892787021614f",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/00d03067f07482f025d41ab55e4ba0db5eca2cdf",
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf",
"shasum": ""
},
"require": {
@@ -628,9 +628,9 @@
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "1.10.34",
"phpstan/phpstan": "1.10.35",
"phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "9.6.12",
"phpunit/phpunit": "9.6.13",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.7.2",
@@ -696,7 +696,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.6.7"
"source": "https://github.com/doctrine/dbal/tree/3.7.0"
},
"funding": [
{
@@ -712,20 +712,20 @@
"type": "tidelift"
}
],
"time": "2023-09-19T20:15:41+00:00"
"time": "2023-09-26T20:56:55+00:00"
},
{
"name": "doctrine/deprecations",
"version": "v1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
"reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
"reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
"reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
"shasum": ""
},
"require": {
@@ -757,9 +757,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
"source": "https://github.com/doctrine/deprecations/tree/1.1.2"
},
"time": "2023-06-03T09:27:29+00:00"
"time": "2023-09-27T20:04:15+00:00"
},
{
"name": "doctrine/event-manager",
@@ -1869,16 +1869,16 @@
},
{
"name": "laravel/framework",
"version": "v10.24.0",
"version": "v10.26.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "bcebd0a4c015d5c38aeec299d355a42451dd3726"
"reference": "6e5440f7c518f26b4495e5d7e4796ec239e26df9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/bcebd0a4c015d5c38aeec299d355a42451dd3726",
"reference": "bcebd0a4c015d5c38aeec299d355a42451dd3726",
"url": "https://api.github.com/repos/laravel/framework/zipball/6e5440f7c518f26b4495e5d7e4796ec239e26df9",
"reference": "6e5440f7c518f26b4495e5d7e4796ec239e26df9",
"shasum": ""
},
"require": {
@@ -1896,7 +1896,7 @@
"ext-tokenizer": "*",
"fruitcake/php-cors": "^1.2",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.1",
"laravel/prompts": "^0.1.9",
"laravel/serializable-closure": "^1.3",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
@@ -1978,7 +1978,7 @@
"league/flysystem-read-only": "^3.3",
"league/flysystem-sftp-v3": "^3.0",
"mockery/mockery": "^1.5.1",
"orchestra/testbench-core": "^8.10",
"orchestra/testbench-core": "^8.12",
"pda/pheanstalk": "^4.0",
"phpstan/phpstan": "^1.4.7",
"phpunit/phpunit": "^10.0.7",
@@ -2065,7 +2065,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2023-09-19T15:25:04+00:00"
"time": "2023-10-03T14:24:20+00:00"
},
{
"name": "laravel/horizon",
@@ -2147,16 +2147,16 @@
},
{
"name": "laravel/prompts",
"version": "v0.1.8",
"version": "v0.1.11",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
"reference": "68dcc65babf92e1fb43cba0b3f78fc3d8002709c"
"reference": "cce65a90e64712909ea1adc033e1d88de8455ffd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/68dcc65babf92e1fb43cba0b3f78fc3d8002709c",
"reference": "68dcc65babf92e1fb43cba0b3f78fc3d8002709c",
"url": "https://api.github.com/repos/laravel/prompts/zipball/cce65a90e64712909ea1adc033e1d88de8455ffd",
"reference": "cce65a90e64712909ea1adc033e1d88de8455ffd",
"shasum": ""
},
"require": {
@@ -2165,6 +2165,10 @@
"php": "^8.1",
"symfony/console": "^6.2"
},
"conflict": {
"illuminate/console": ">=10.17.0 <10.25.0",
"laravel/framework": ">=10.17.0 <10.25.0"
},
"require-dev": {
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3",
@@ -2175,6 +2179,11 @@
"ext-pcntl": "Required for the spinner to be animated."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "0.1.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
@@ -2189,9 +2198,9 @@
],
"support": {
"issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.1.8"
"source": "https://github.com/laravel/prompts/tree/v0.1.11"
},
"time": "2023-09-19T15:33:56+00:00"
"time": "2023-10-03T01:07:35+00:00"
},
{
"name": "laravel/sanctum",
@@ -3442,16 +3451,16 @@
},
{
"name": "nesbot/carbon",
"version": "2.70.0",
"version": "2.71.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d"
"reference": "98276233188583f2ff845a0f992a235472d9466a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3298b38ea8612e5f77d38d1a99438e42f70341d",
"reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/98276233188583f2ff845a0f992a235472d9466a",
"reference": "98276233188583f2ff845a0f992a235472d9466a",
"shasum": ""
},
"require": {
@@ -3544,7 +3553,7 @@
"type": "tidelift"
}
],
"time": "2023-09-07T16:43:50+00:00"
"time": "2023-09-25T11:31:05+00:00"
},
{
"name": "nette/schema",
@@ -4767,16 +4776,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.24.1",
"version": "1.24.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01"
"reference": "bcad8d995980440892759db0c32acae7c8e79442"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
"reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442",
"reference": "bcad8d995980440892759db0c32acae7c8e79442",
"shasum": ""
},
"require": {
@@ -4808,9 +4817,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2"
},
"time": "2023-09-18T12:18:02+00:00"
"time": "2023-09-26T12:28:12+00:00"
},
{
"name": "pimple/pimple",
@@ -6011,16 +6020,16 @@
},
{
"name": "sentry/sentry-laravel",
"version": "3.8.0",
"version": "3.8.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
"reference": "c7e7611553f9f90af10ed98dde1a680220f02e4d"
"reference": "b6142a80fa9360a10b786d2da032339602d0e362"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/c7e7611553f9f90af10ed98dde1a680220f02e4d",
"reference": "c7e7611553f9f90af10ed98dde1a680220f02e4d",
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/b6142a80fa9360a10b786d2da032339602d0e362",
"reference": "b6142a80fa9360a10b786d2da032339602d0e362",
"shasum": ""
},
"require": {
@@ -6087,7 +6096,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-laravel/issues",
"source": "https://github.com/getsentry/sentry-laravel/tree/3.8.0"
"source": "https://github.com/getsentry/sentry-laravel/tree/3.8.1"
},
"funding": [
{
@@ -6099,7 +6108,7 @@
"type": "custom"
}
],
"time": "2023-09-05T11:02:34+00:00"
"time": "2023-10-04T10:21:16+00:00"
},
{
"name": "spatie/backtrace",
@@ -6748,16 +6757,16 @@
},
{
"name": "stripe/stripe-php",
"version": "v12.4.0",
"version": "v12.5.0",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "7d0a90772fc1c179e370971264318208533324b9"
"reference": "a4249b4a90437844f6c35e8701f8c68acd206f56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/7d0a90772fc1c179e370971264318208533324b9",
"reference": "7d0a90772fc1c179e370971264318208533324b9",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/a4249b4a90437844f6c35e8701f8c68acd206f56",
"reference": "a4249b4a90437844f6c35e8701f8c68acd206f56",
"shasum": ""
},
"require": {
@@ -6802,9 +6811,9 @@
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v12.4.0"
"source": "https://github.com/stripe/stripe-php/tree/v12.5.0"
},
"time": "2023-09-21T22:55:47+00:00"
"time": "2023-09-28T23:06:27+00:00"
},
{
"name": "symfony/console",
@@ -7030,16 +7039,16 @@
},
{
"name": "symfony/error-handler",
"version": "v6.3.2",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a"
"reference": "1f69476b64fb47105c06beef757766c376b548c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/85fd65ed295c4078367c784e8a5a6cee30348b7a",
"reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/1f69476b64fb47105c06beef757766c376b548c4",
"reference": "1f69476b64fb47105c06beef757766c376b548c4",
"shasum": ""
},
"require": {
@@ -7084,7 +7093,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v6.3.2"
"source": "https://github.com/symfony/error-handler/tree/v6.3.5"
},
"funding": [
{
@@ -7100,7 +7109,7 @@
"type": "tidelift"
}
],
"time": "2023-07-16T17:05:46+00:00"
"time": "2023-09-12T06:57:20+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -7260,16 +7269,16 @@
},
{
"name": "symfony/finder",
"version": "v6.3.3",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e"
"reference": "a1b31d88c0e998168ca7792f222cbecee47428c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e",
"reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e",
"url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4",
"reference": "a1b31d88c0e998168ca7792f222cbecee47428c4",
"shasum": ""
},
"require": {
@@ -7304,7 +7313,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v6.3.3"
"source": "https://github.com/symfony/finder/tree/v6.3.5"
},
"funding": [
{
@@ -7320,20 +7329,20 @@
"type": "tidelift"
}
],
"time": "2023-07-31T08:31:44+00:00"
"time": "2023-09-26T12:56:25+00:00"
},
{
"name": "symfony/http-client",
"version": "v6.3.2",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00"
"reference": "213e564da4cbf61acc9728d97e666bcdb868c10d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
"reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00",
"url": "https://api.github.com/repos/symfony/http-client/zipball/213e564da4cbf61acc9728d97e666bcdb868c10d",
"reference": "213e564da4cbf61acc9728d97e666bcdb868c10d",
"shasum": ""
},
"require": {
@@ -7396,7 +7405,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v6.3.2"
"source": "https://github.com/symfony/http-client/tree/v6.3.5"
},
"funding": [
{
@@ -7412,7 +7421,7 @@
"type": "tidelift"
}
],
"time": "2023-07-05T08:41:27+00:00"
"time": "2023-09-29T15:57:12+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -7494,16 +7503,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v6.3.4",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "cac1556fdfdf6719668181974104e6fcfa60e844"
"reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/cac1556fdfdf6719668181974104e6fcfa60e844",
"reference": "cac1556fdfdf6719668181974104e6fcfa60e844",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/b50f5e281d722cb0f4c296f908bacc3e2b721957",
"reference": "b50f5e281d722cb0f4c296f908bacc3e2b721957",
"shasum": ""
},
"require": {
@@ -7551,7 +7560,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v6.3.4"
"source": "https://github.com/symfony/http-foundation/tree/v6.3.5"
},
"funding": [
{
@@ -7567,20 +7576,20 @@
"type": "tidelift"
}
],
"time": "2023-08-22T08:20:46+00:00"
"time": "2023-09-04T21:33:54+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v6.3.4",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb"
"reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
"reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f991a964368bee8d883e8d57ced4fe9fff04dfc",
"reference": "9f991a964368bee8d883e8d57ced4fe9fff04dfc",
"shasum": ""
},
"require": {
@@ -7664,7 +7673,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v6.3.4"
"source": "https://github.com/symfony/http-kernel/tree/v6.3.5"
},
"funding": [
{
@@ -7680,20 +7689,20 @@
"type": "tidelift"
}
],
"time": "2023-08-26T13:54:49+00:00"
"time": "2023-09-30T06:37:04+00:00"
},
{
"name": "symfony/mailer",
"version": "v6.3.0",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435"
"reference": "d89611a7830d51b5e118bca38e390dea92f9ea06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
"reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435",
"url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06",
"reference": "d89611a7830d51b5e118bca38e390dea92f9ea06",
"shasum": ""
},
"require": {
@@ -7744,7 +7753,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v6.3.0"
"source": "https://github.com/symfony/mailer/tree/v6.3.5"
},
"funding": [
{
@@ -7760,20 +7769,20 @@
"type": "tidelift"
}
],
"time": "2023-05-29T12:49:39+00:00"
"time": "2023-09-06T09:47:15+00:00"
},
{
"name": "symfony/mime",
"version": "v6.3.3",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98"
"reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
"reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98",
"url": "https://api.github.com/repos/symfony/mime/zipball/d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
"reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e",
"shasum": ""
},
"require": {
@@ -7828,7 +7837,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v6.3.3"
"source": "https://github.com/symfony/mime/tree/v6.3.5"
},
"funding": [
{
@@ -7844,7 +7853,7 @@
"type": "tidelift"
}
],
"time": "2023-07-31T07:08:24+00:00"
"time": "2023-09-29T06:59:36+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8886,16 +8895,16 @@
},
{
"name": "symfony/routing",
"version": "v6.3.3",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a"
"reference": "82616e59acd3e3d9c916bba798326cb7796d7d31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/e7243039ab663822ff134fbc46099b5fdfa16f6a",
"reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a",
"url": "https://api.github.com/repos/symfony/routing/zipball/82616e59acd3e3d9c916bba798326cb7796d7d31",
"reference": "82616e59acd3e3d9c916bba798326cb7796d7d31",
"shasum": ""
},
"require": {
@@ -8949,7 +8958,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v6.3.3"
"source": "https://github.com/symfony/routing/tree/v6.3.5"
},
"funding": [
{
@@ -8965,7 +8974,7 @@
"type": "tidelift"
}
],
"time": "2023-07-31T07:08:24+00:00"
"time": "2023-09-20T16:05:51+00:00"
},
{
"name": "symfony/service-contracts",
@@ -9113,16 +9122,16 @@
},
{
"name": "symfony/string",
"version": "v6.3.2",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "53d1a83225002635bca3482fcbf963001313fb68"
"reference": "13d76d0fb049051ed12a04bef4f9de8715bea339"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68",
"reference": "53d1a83225002635bca3482fcbf963001313fb68",
"url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339",
"reference": "13d76d0fb049051ed12a04bef4f9de8715bea339",
"shasum": ""
},
"require": {
@@ -9179,7 +9188,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.3.2"
"source": "https://github.com/symfony/string/tree/v6.3.5"
},
"funding": [
{
@@ -9195,7 +9204,7 @@
"type": "tidelift"
}
],
"time": "2023-07-05T08:41:27+00:00"
"time": "2023-09-18T10:38:32+00:00"
},
{
"name": "symfony/translation",
@@ -9446,16 +9455,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v6.3.4",
"version": "v6.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45"
"reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/2027be14f8ae8eae999ceadebcda5b4909b81d45",
"reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/3d9999376be5fea8de47752837a3e1d1c5f69ef5",
"reference": "3d9999376be5fea8de47752837a3e1d1c5f69ef5",
"shasum": ""
},
"require": {
@@ -9510,7 +9519,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v6.3.4"
"source": "https://github.com/symfony/var-dumper/tree/v6.3.5"
},
"funding": [
{
@@ -9526,7 +9535,7 @@
"type": "tidelift"
}
],
"time": "2023-08-24T14:51:05+00:00"
"time": "2023-09-12T10:11:35+00:00"
},
{
"name": "symfony/yaml",
@@ -10249,16 +10258,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
"version": "v7.2.7",
"version": "v7.2.8",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "1526eb4fd195f65075456dee394d14742ae0a66c"
"reference": "882b02d197328138686bb06ce7d8cbb98fc0a16c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/1526eb4fd195f65075456dee394d14742ae0a66c",
"reference": "1526eb4fd195f65075456dee394d14742ae0a66c",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/882b02d197328138686bb06ce7d8cbb98fc0a16c",
"reference": "882b02d197328138686bb06ce7d8cbb98fc0a16c",
"shasum": ""
},
"require": {
@@ -10328,7 +10337,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.2.7"
"source": "https://github.com/paratestphp/paratest/tree/v7.2.8"
},
"funding": [
{
@@ -10340,7 +10349,7 @@
"type": "paypal"
}
],
"time": "2023-09-14T14:10:09+00:00"
"time": "2023-10-04T13:38:04+00:00"
},
{
"name": "fakerphp/faker",
@@ -10595,16 +10604,16 @@
},
{
"name": "laravel/dusk",
"version": "v7.11.0",
"version": "v7.11.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
"reference": "89ec34a35737303bf3e75f0e8b958d7fcd1cf486"
"reference": "076865d1057a4951f796342aa6a8f97a317e7638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/89ec34a35737303bf3e75f0e8b958d7fcd1cf486",
"reference": "89ec34a35737303bf3e75f0e8b958d7fcd1cf486",
"url": "https://api.github.com/repos/laravel/dusk/zipball/076865d1057a4951f796342aa6a8f97a317e7638",
"reference": "076865d1057a4951f796342aa6a8f97a317e7638",
"shasum": ""
},
"require": {
@@ -10665,9 +10674,9 @@
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
"source": "https://github.com/laravel/dusk/tree/v7.11.0"
"source": "https://github.com/laravel/dusk/tree/v7.11.1"
},
"time": "2023-09-12T11:13:00+00:00"
"time": "2023-09-26T13:23:43+00:00"
},
{
"name": "laravel/pint",
@@ -10974,16 +10983,16 @@
},
{
"name": "pestphp/pest",
"version": "v2.19.2",
"version": "v2.20.0",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
"reference": "6bc9da3fe1154d75a65262618b4a7032f267c04f"
"reference": "a8b785f69e44ae3f902cbf08fe6b79359ba46945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pestphp/pest/zipball/6bc9da3fe1154d75a65262618b4a7032f267c04f",
"reference": "6bc9da3fe1154d75a65262618b4a7032f267c04f",
"url": "https://api.github.com/repos/pestphp/pest/zipball/a8b785f69e44ae3f902cbf08fe6b79359ba46945",
"reference": "a8b785f69e44ae3f902cbf08fe6b79359ba46945",
"shasum": ""
},
"require": {
@@ -11061,7 +11070,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
"source": "https://github.com/pestphp/pest/tree/v2.19.2"
"source": "https://github.com/pestphp/pest/tree/v2.20.0"
},
"funding": [
{
@@ -11073,7 +11082,7 @@
"type": "github"
}
],
"time": "2023-09-19T10:48:16+00:00"
"time": "2023-09-29T18:05:52+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -11445,16 +11454,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.10.35",
"version": "1.10.37",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3"
"reference": "058ba07e92f744d4dcf6061ae75283d0c6456f2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e730e5facb75ffe09dfb229795e8c01a459f26c3",
"reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/058ba07e92f744d4dcf6061ae75283d0c6456f2e",
"reference": "058ba07e92f744d4dcf6061ae75283d0c6456f2e",
"shasum": ""
},
"require": {
@@ -11503,20 +11512,20 @@
"type": "tidelift"
}
],
"time": "2023-09-19T15:27:56+00:00"
"time": "2023-10-02T16:18:37+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "10.1.6",
"version": "10.1.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "56f33548fe522c8d82da7ff3824b42829d324364"
"reference": "355324ca4980b8916c18b9db29f3ef484078f26e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/56f33548fe522c8d82da7ff3824b42829d324364",
"reference": "56f33548fe522c8d82da7ff3824b42829d324364",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/355324ca4980b8916c18b9db29f3ef484078f26e",
"reference": "355324ca4980b8916c18b9db29f3ef484078f26e",
"shasum": ""
},
"require": {
@@ -11573,7 +11582,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.6"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.7"
},
"funding": [
{
@@ -11581,7 +11590,7 @@
"type": "github"
}
],
"time": "2023-09-19T04:59:03+00:00"
"time": "2023-10-04T15:34:17+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -12173,16 +12182,16 @@
},
{
"name": "sebastian/complexity",
"version": "3.0.1",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/complexity.git",
"reference": "c70b73893e10757af9c6a48929fa6a333b56a97a"
"reference": "68cfb347a44871f01e33ab0ef8215966432f6957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c70b73893e10757af9c6a48929fa6a333b56a97a",
"reference": "c70b73893e10757af9c6a48929fa6a333b56a97a",
"url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68cfb347a44871f01e33ab0ef8215966432f6957",
"reference": "68cfb347a44871f01e33ab0ef8215966432f6957",
"shasum": ""
},
"require": {
@@ -12195,7 +12204,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
"dev-main": "3.1-dev"
}
},
"autoload": {
@@ -12219,7 +12228,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/complexity/issues",
"security": "https://github.com/sebastianbergmann/complexity/security/policy",
"source": "https://github.com/sebastianbergmann/complexity/tree/3.0.1"
"source": "https://github.com/sebastianbergmann/complexity/tree/3.1.0"
},
"funding": [
{
@@ -12227,7 +12236,7 @@
"type": "github"
}
],
"time": "2023-08-31T09:55:53+00:00"
"time": "2023-09-28T11:50:59+00:00"
},
{
"name": "sebastian/diff",

View File

@@ -31,9 +31,9 @@ return [
'ultimate' => 25,
],
'email' => [
'zero' => false,
'zero' => true,
'self-hosted' => true,
'basic' => false,
'basic' => true,
'pro' => true,
'ultimate' => true,
],

View File

@@ -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.50',
'release' => '4.0.0-beta.67',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -30,7 +30,7 @@ return [
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 3000,
'duration' => 5000,
/**
* The horizontal position of each toast.

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.50';
return '4.0.0-beta.67';

View File

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

View File

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

View File

@@ -34,14 +34,14 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust"
volumes:
- /data/coolify/_volumes/database/:/var/lib/postgresql/data
- coolify-pg-data-dev:/var/lib/postgresql/data
redis:
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
- .env
volumes:
- /data/coolify/_volumes/redis/:/data
- coolify-redis-data-dev:/data
vite:
image: node:19
working_dir: /var/www/html
@@ -56,7 +56,7 @@ services:
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify
- coolify-data-dev:/data/coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail
@@ -76,6 +76,12 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- /data/coolify/_volumes/minio/:/data
- coolify-minio-data-dev:/data
networks:
- coolify
volumes:
coolify-data-dev:
coolify-pg-data-dev:
coolify-redis-data-dev:
coolify-minio-data-dev:

View File

@@ -0,0 +1,756 @@
x-logging: &x-logging
logging:
driver: 'json-file'
options:
max-file: '5'
max-size: '10m'
x-image: &x-image
image: appwrite/appwrite:1.4.3
x-image-assistant: &x-image-assistant
image: appwrite/assistant:0.2.1
version: '3'
services:
appwrite:
<<: *x-image
container_name: appwrite
<<: *x-logging
labels:
- traefik.constraint-label-stack=appwrite
- traefik.docker.network=appwrite
- traefik.http.services.appwrite_api.loadbalancer.server.port=80
#http
- traefik.http.routers.appwrite_api_http.entrypoints=web
- traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_http.service=appwrite_api
# https
- traefik.http.routers.appwrite_api_https.entrypoints=websecure
- traefik.http.routers.appwrite_api_https.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
- appwrite-functions:/storage/functions:rw
depends_on:
- mariadb
- redis
# - clamav
- influxdb
environment:
- SERVICE_FQDN_APPWRITE=/
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_LOCALE
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_IPS
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_SYSTEM_RESPONSE_FORMAT
- _APP_OPTIONS_ABUSE
- _APP_OPTIONS_FORCE_HTTPS
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_FUNCTIONS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
- _APP_SMTP_USERNAME
- _APP_SMTP_PASSWORD
- _APP_USAGE_STATS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_STORAGE_LIMIT
- _APP_STORAGE_PREVIEW_LIMIT
- _APP_STORAGE_ANTIVIRUS
- _APP_STORAGE_ANTIVIRUS_HOST
- _APP_STORAGE_ANTIVIRUS_PORT
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_FUNCTIONS_SIZE_LIMIT
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_BUILD_TIMEOUT
- _APP_FUNCTIONS_CPUS
- _APP_FUNCTIONS_MEMORY
- _APP_FUNCTIONS_RUNTIMES
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_STATSD_HOST
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
- _APP_MAINTENANCE_RETENTION_SCHEDULES
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_GRAPHQL_MAX_BATCH_SIZE
- _APP_GRAPHQL_MAX_COMPLEXITY
- _APP_GRAPHQL_MAX_DEPTH
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
- _APP_VCS_GITHUB_APP_ID
- _APP_VCS_GITHUB_WEBHOOK_SECRET
- _APP_VCS_GITHUB_CLIENT_SECRET
- _APP_VCS_GITHUB_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-realtime:
<<: *x-image
entrypoint: realtime
container_name: appwrite-realtime
<<: *x-logging
labels:
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=appwrite"
- "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80"
#ws
- traefik.http.routers.appwrite_realtime_ws.entrypoints=web
- traefik.http.routers.appwrite_realtime_ws.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_ws.service=appwrite_realtime
# wss
- traefik.http.routers.appwrite_realtime_wss.entrypoints=websecure
- traefik.http.routers.appwrite_realtime_wss.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_wss.service=appwrite_realtime
- traefik.http.routers.appwrite_realtime_wss.tls=true
- traefik.http.routers.appwrite_realtime_wss.tls.certresolver=dns
networks:
- appwrite
depends_on:
- mariadb
- redis
environment:
- SERVICE_FQDN_APPWRITE=/v1/realtime
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPTIONS_ABUSE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-audits:
<<: *x-image
entrypoint: worker-audits
<<: *x-logging
container_name: appwrite-worker-audits
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
<<: *x-image
entrypoint: worker-webhooks
<<: *x-logging
container_name: appwrite-worker-webhooks
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-deletes:
<<: *x-image
entrypoint: worker-deletes
<<: *x-logging
container_name: appwrite-worker-deletes
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
- appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw
- appwrite-certificates:/storage/certificates:rw
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
appwrite-worker-databases:
<<: *x-image
entrypoint: worker-databases
<<: *x-logging
container_name: appwrite-worker-databases
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
<<: *x-image
entrypoint: worker-builds
<<: *x-logging
container_name: appwrite-worker-builds
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
- _APP_VCS_GITHUB_APP_ID
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_BUILD_TIMEOUT
- _APP_FUNCTIONS_CPUS
- _APP_FUNCTIONS_MEMORY
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
appwrite-worker-certificates:
<<: *x-image
entrypoint: worker-certificates
<<: *x-logging
container_name: appwrite-worker-certificates
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_FUNCTIONS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-functions:
<<: *x-image
entrypoint: worker-functions
<<: *x-logging
container_name: appwrite-worker-functions
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- openruntimes-executor
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_BUILD_TIMEOUT
- _APP_FUNCTIONS_CPUS
- _APP_FUNCTIONS_MEMORY
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_USAGE_STATS
- _APP_DOCKER_HUB_USERNAME
- _APP_DOCKER_HUB_PASSWORD
- _APP_LOGGING_CONFIG
- _APP_LOGGING_PROVIDER
appwrite-worker-mails:
<<: *x-image
entrypoint: worker-mails
<<: *x-logging
container_name: appwrite-worker-mails
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
- _APP_SMTP_USERNAME
- _APP_SMTP_PASSWORD
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-messaging:
<<: *x-image
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-migrations:
<<: *x-image
entrypoint: worker-migrations
<<: *x-logging
container_name: appwrite-worker-migrations
restart: unless-stopped
networks:
- appwrite
depends_on:
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
appwrite-maintenance:
<<: *x-image
entrypoint: maintenance
<<: *x-logging
container_name: appwrite-maintenance
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_FUNCTIONS
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
- _APP_MAINTENANCE_RETENTION_SCHEDULES
appwrite-usage:
<<: *x-image
entrypoint: usage
container_name: appwrite-usage
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-schedule:
<<: *x-image
entrypoint: schedule
container_name: appwrite-schedule
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- mariadb
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
appwrite-assistant:
<<: *x-image-assistant
container_name: appwrite-assistant
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
openruntimes-executor:
container_name: openruntimes-executor
hostname: appwrite-executor
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.4.1
networks:
- appwrite
- runtimes
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-builds:/storage/builds:rw
- appwrite-functions:/storage/functions:rw
# Host mount nessessary to share files between executor and runtimes.
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
- /tmp:/tmp:rw
environment:
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL
- OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK
- OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME
- OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD
- OPR_EXECUTOR_ENV=$_APP_ENV
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES
- OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
- OPR_EXECUTOR_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER
- OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
- OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE
- OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET
- OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION
- OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET
- OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET
- OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION
- OPR_EXECUTOR_STORAGE_DO_SPACES_BUCKET=$_APP_STORAGE_DO_SPACES_BUCKET
- OPR_EXECUTOR_STORAGE_BACKBLAZE_ACCESS_KEY=$_APP_STORAGE_BACKBLAZE_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_BACKBLAZE_SECRET=$_APP_STORAGE_BACKBLAZE_SECRET
- OPR_EXECUTOR_STORAGE_BACKBLAZE_REGION=$_APP_STORAGE_BACKBLAZE_REGION
- OPR_EXECUTOR_STORAGE_BACKBLAZE_BUCKET=$_APP_STORAGE_BACKBLAZE_BUCKET
- OPR_EXECUTOR_STORAGE_LINODE_ACCESS_KEY=$_APP_STORAGE_LINODE_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_LINODE_SECRET=$_APP_STORAGE_LINODE_SECRET
- OPR_EXECUTOR_STORAGE_LINODE_REGION=$_APP_STORAGE_LINODE_REGION
- OPR_EXECUTOR_STORAGE_LINODE_BUCKET=$_APP_STORAGE_LINODE_BUCKET
- OPR_EXECUTOR_STORAGE_WASABI_ACCESS_KEY=$_APP_STORAGE_WASABI_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_WASABI_SECRET=$_APP_STORAGE_WASABI_SECRET
- OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
mariadb:
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-mariadb:/var/lib/mysql:rw
environment:
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
command: 'mysqld --innodb-flush-method=fsync'
redis:
image: redis:7.0.4-alpine
container_name: appwrite-redis
<<: *x-logging
restart: unless-stopped
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--maxmemory-samples 5
networks:
- appwrite
volumes:
- appwrite-redis:/data:rw
# clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# restart: unless-stopped
# networks:
# - appwrite
# volumes:
# - appwrite-uploads:/storage/uploads
influxdb:
image: appwrite/influxdb:1.5.0
container_name: appwrite-influxdb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-influxdb:/var/lib/influxdb:rw
telegraf:
image: appwrite/telegraf:1.4.0
container_name: appwrite-telegraf
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
environment:
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
networks:
gateway:
name: gateway
appwrite:
name: appwrite
runtimes:
name: runtimes
volumes:
appwrite-mariadb:
appwrite-redis:
appwrite-cache:
appwrite-uploads:
appwrite-certificates:
appwrite-functions:
appwrite-builds:
appwrite-influxdb:
appwrite-config:

View File

@@ -11,7 +11,8 @@ services:
- database__connection__password=$SERVICE_PASSWORD_MYSQL
- database__connection__database=${MYSQL_DATABASE-ghost}
depends_on:
- mysql
mysql:
condition: service_healthy
mysql:
image: mysql:8.0
volumes:
@@ -20,4 +21,9 @@ services:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10

View File

@@ -0,0 +1,16 @@
services:
minio:
image: quay.io/minio/minio:RELEASE.2023-09-30T07-02-29Z
command: server /data --console-address ":9001"
environment:
SERVICE_FQDN_MINIO_9000:
SERVICE_FQDN_CONSOLE_9001:
MINIO_ROOT_USER: $SERVICE_USER_MINIO
MINIO_ROOT_PASSWORD: $SERVICE_PASSWORD_MINIO
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -55,6 +55,9 @@ a {
.box {
@apply flex items-center p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
}
.box-without-bg {
@apply flex items-center p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];
}
.lds-heart {
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);

View File

@@ -3,7 +3,7 @@
<x-chevron-down />
</label>
<div class="absolute hidden group-hover:block">
<div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if (data_get($application, 'gitBrancLocation'))
<li>

View File

@@ -7,6 +7,10 @@
href="{{ route('project.application.deployments', $parameters) }}">
<button>Deployments</button>
</a>
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<x-applications.links :application="$application" />
<div class="flex-1"></div>
<x-applications.advanced :application="$application" />

View File

@@ -2,7 +2,7 @@
<div tabindex="0" x-data="{ open: false }"
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200"
:class="open ? 'collapse-open' : 'collapse-close'">
<div class="flex flex-col justify-center text-sm collapse-title" x-on:click="open = !open">
<div class="flex flex-col justify-center text-sm select-text collapse-title" x-on:click="open = !open">
{{ $title }}
</div>
<div class="collapse-content">

View File

@@ -3,6 +3,10 @@
href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button>
</a>
<a class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}">
<button>Backups</button>

View File

@@ -4,7 +4,7 @@
<x-chevron-down />
</label>
<div class="absolute hidden group-hover:block">
<div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0"
class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if ($links->count() > 0)

View File

@@ -1,4 +1,8 @@
<div class="navbar-main">
<a class="{{ request()->routeIs('project.service') ? 'text-white' : '' }}"
href="{{ route('project.service', $parameters) }}">
<button>Configuration</button>
</a>
<x-services.links :service="$service" />
<div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded')

View File

@@ -1,2 +1,2 @@
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-20 hover:opacity-100 hover:text-white z-50']) }}
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-60 hover:opacity-100 hover:text-white z-50']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('version') }}">v{{ config('version') }}</a>

View File

@@ -40,7 +40,7 @@
<x-boarding-step title="Server">
<x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" />
or on a<x-highlighted text="Remote Server" />?
or on a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')"

View File

@@ -0,0 +1,13 @@
<div class="pb-10" x-data>
<h1>Compose</h1>
<div>All kinds of compose files.</div>
<h3 class="pt-4">Services</h3>
@foreach ($services as $serviceName => $value)
<x-forms.button wire:click="setService('{{ $serviceName }}')">{{ Str::headline($serviceName) }}</x-forms.button>
@endforeach
<h3 class="pt-4">Base64 En/Decode</h3>
<x-forms.button x-on:click="copyToClipboard('{{ $base64 }}')">Copy Base64 Compose</x-forms.button>
<div class="pt-4">
<x-forms.textarea realtimeValidation rows="40" id="compose"></x-forms.textarea>
</div>
</div>

View File

@@ -22,29 +22,15 @@
@if (data_get($team, 'discord_enabled'))
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Enabled" />
</div>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" />
@endif
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Enabled" />
</div>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Status" />
</div>
@endif
</div>

View File

@@ -104,24 +104,14 @@
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Enabled" />
</div>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" />
@endif
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Enabled" />
</div>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes"
label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments"
label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Backup Status" />
</div>
@endif
</div>

View File

@@ -27,34 +27,30 @@
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-96">
@if (isDev())
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Enabled" />
<div class="w-64">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<div class="w-64">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Enabled" />
label="Container Status Changes" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<div class="w-64">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Enabled" />
label="Application Deployments" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<div class="w-64">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Enabled" />
label="Backup Status" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />

View File

@@ -15,16 +15,8 @@
<div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
@if ($wildcard_domain)
@if ($global_wildcard_domain)
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
</x-forms.button>
@endif
@if ($server_wildcard_domain)
<x-forms.button wire:click="generateServerRandomDomain">Set Server Wildcard
</x-forms.button>
@endif
@endif
<x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button>
</div>
@if (!$application->dockerfile)
<div class="flex items-end gap-2">
@@ -50,7 +42,7 @@
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos. WIP" disabled />
helper="Directory to use as root. Useful for monorepos." />
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory" label="Publish Directory"
required />
@@ -89,7 +81,7 @@
id="is_auto_deploy_enabled" label="Auto Deploy" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="is_preview_deployments_enabled" label="Previews Deployments" />
instantSave id="is_preview_deployments_enabled" label="Preview Deployments" />
<x-forms.checkbox instantSave id="is_git_submodules_enabled" label="Git Submodules"
helper="Allow Git Submodules during build process." />

View File

@@ -1,6 +1,6 @@
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2">
<h2>Previews Deployments</h2>
<h2>Preview Deployments</h2>
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="resetToDefault">Reset template to default</x-forms.button>
</div>

View File

@@ -54,11 +54,11 @@
<div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (Str::of(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="$status" />
<x-status.running :status="data_get($preview, 'status')" />
@elseif(Str::of(data_get($preview, 'status'))->startsWith('restarting'))
<x-status.restarting :status="$status" />
<x-status.restarting :status="data_get($preview, 'status')" />
@else
<x-status.stopped :status="$status" />
<x-status.stopped :status="data_get($preview, 'status')" />
@endif
@if (data_get($preview, 'status') !== 'exited')
| <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview

View File

@@ -26,27 +26,29 @@
<x-forms.input label="Image" id="database.image" required
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/postgres'>https://hub.docker.com/_/postgres</a>" />
</div>
<div class="flex gap-2">
@if ($database->started_at)
<x-forms.input label="Username" id="database.postgres_username" placeholder="If empty: postgres"
readonly helper="You can only modify it before the initial start." />
<x-forms.input label="Password" id="database.postgres_password" type="password" required readonly
helper="You can only modify it before the initial start." />
<x-forms.input label="Database" id="database.postgres_db"
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Username" id="database.postgres_username" placeholder="If empty: postgres"
readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.postgres_password" type="password" required readonly
helper="You can only change this in the database." />
<x-forms.input label="Initial Database" id="database.postgres_db"
placeholder="If empty, it will be the same as Username." readonly
helper="You can only modify it before the initial start." />
@else
<x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres"
helper="You can only modify it before the initial start." />
<x-forms.input label="Password" id="database.postgres_password" type="password" required
helper="You can only modify it before the initial start." />
helper="You can only change this in the database." />
</div>
@else
<div class="pt-8 text-warning">Please verify these values. You can only modify them before the initial start. After that, you need to modify it in the database.
</div>
<div class="flex gap-2 pb-8">
<x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres" />
<x-forms.input label="Password" id="database.postgres_password" type="password" required />
<x-forms.input label="Database" id="database.postgres_db"
placeholder="If empty, it will be the same as Username."
helper="You can only modify it before the initial start." />
@endif
</div>
placeholder="If empty, it will be the same as Username." />
</div>
@endif
<div class="flex gap-2">
<x-forms.input label="Initial Arguments" id="database.postgres_initdb_args"
<x-forms.input label="Initial Database Arguments" id="database.postgres_initdb_args"
placeholder="If empty, use default. See in docker docs." />
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." />
@@ -55,11 +57,12 @@
<h3 class="py-2">Network</h3>
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@@ -95,19 +95,31 @@
<span class="loading loading-xs loading-spinner"></span>
@else
@foreach ($services as $serviceName => $service)
<button class="text-left box group"
wire:loading.attr="disabled" wire:click="setType('one-click-service-{{ $serviceName }}')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
{{ Str::headline($serviceName) }}
</div>
@if (data_get($service, 'slogan'))
<div class="text-xs">
{{ data_get($service, 'slogan') }}
@if (data_get($service, 'disabled'))
<button class="text-left bg-black cursor-not-allowed bg-coolgray-200/20 box-without-bg"
disabled>
<div class="flex flex-col mx-6">
<div class="font-bold text-coolgray-500">
{{ Str::headline($serviceName) }}
</div>
@endif
</div>
</button>
You need to upgrade to {{ data_get($service, 'minVersion') }} to use this service.
</div>
</button>
@else
<button class="text-left box group" wire:loading.attr="disabled"
wire:click="setType('one-click-service-{{ $serviceName }}')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
{{ Str::headline($serviceName) }}
</div>
@if (data_get($service, 'slogan'))
<div class="text-xs">
{{ data_get($service, 'slogan') }}
</div>
@endif
</div>
</button>
@endif
@endforeach
@endif
</div>

View File

@@ -0,0 +1,30 @@
<dialog id="composeModal" class="modal" x-data="{ raw: true }">
<form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'>
<div class="flex items-end gap-2">
<h1>Docker Compose</h1>
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
</div>
<div>Volume names are updated upon save. The service UUID will be added as a prefix to all volumes, to prevent name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage menu.</div>
<div x-cloak x-show="raw">
<x-forms.textarea rows="20" id="raw">
</x-forms.textarea>
</div>
<div x-cloak x-show="raw === false">
<x-forms.textarea rows="20" readonly id="actual">
</x-forms.textarea>
</div>
<x-forms.button onclick="composeModal.close()" type="submit">
Save
</x-forms.button>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@@ -1,23 +1,25 @@
<x-collapsible>
<x-slot:title>
<div>{{ $fileStorage->mount_path }} </div>
<div>{{$workdir}}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
</x-slot:title>
<x-slot:action>
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
<div class="w-64">
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div>
@if ($fileStorage->is_directory)
{{-- @if ($fileStorage->is_directory)
<x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input>
@else
<div class="flex gap-2">
@else --}}
{{-- <div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input> --}}
@if (!$fileStorage->is_directory)
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button>
@endif
{{-- @endif --}}
</form>
</x-slot:action>
</x-collapsible>

View File

@@ -1,11 +1,14 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.10000ms="checkStatus">
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.2000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
<a :class="activeTab === 'service-stack' && 'text-white'"
@click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'"
href="#">Service Stack</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages</a>
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment
@@ -23,96 +26,95 @@
<div>Configuration</div>
</div>
<x-forms.button type="submit">Save</x-forms.button>
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable
Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
<x-forms.button class="w-64" onclick="composeModal.showModal()">Edit Compose
File</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name"
placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" />
</div>
<div x-cloak x-show="raw">
<x-forms.textarea label="Docker Compose file"
helper="
You can use these variables in your Docker Compose file and Coolify will generate default values or replace them with the values you set on the UI forms.<br>
<br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
- SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="6" id="service.docker_compose_raw">
</x-forms.textarea>
</div>
<div x-cloak x-show="raw === false">
<x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly
rows="6" id="service.docker_compose">
</x-forms.textarea>
</div>
</form>
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
@foreach ($service->applications as $application)
<a @class([
@foreach ($applications as $application)
<div @class([
'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$application->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['starting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ Str::limit($application->fqdn, 60) }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
'flex gap-2 box group',
])>
<a class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ Str::limit($application->fqdn, 60) }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
<a class="flex gap-2 p-1 mx-4 text-xs font-bold rounded hover:no-underline hover:text-warning"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}">Logs</a>
</div>
@endforeach
@foreach ($databases as $database)
<a @class([
<div @class([
'border-l border-dashed border-red-500' => Str::of(
$database->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$database->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$database->status)->contains(['restarting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
@endif
<div class="text-xs">{{ $database->status }}</div>
</a>
'flex gap-2 box group',
])>
<a class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
@endif
<div class="text-xs">{{ $database->status }}</div>
</a>
<a class="flex gap-2 p-1 mx-4 text-xs font-bold rounded hover:no-underline hover:text-warning"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}">Logs</a>
</div>
@endforeach
</div>
</div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
@foreach ($applications as $application)
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
:resource="$application" />
@endforeach
@foreach ($databases as $database)
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" />
@endforeach
</div>
<div x-cloak x-show="activeTab === 'environment-variables'">
<div x-cloak x-show="activeTab === 'environment-variables'">

View File

@@ -11,6 +11,12 @@
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@if (data_get($parameters, 'service_name'))
<a class="{{ request()->routeIs('project.service.logs') ? 'text-white' : '' }}"
href="{{ route('project.service.logs', $parameters) }}">
<button>Logs</button>
</a>
@endif
</div>
<div class="w-full pl-8">
@isset($serviceApplication)
@@ -18,32 +24,27 @@
<livewire:project.service.application :application="$serviceApplication" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceApplication" />
@if ($serviceApplication->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceApplication->id }}"
:resource="$serviceApplication" />
</div>
@endisset
@isset($serviceDatabase)
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceDatabase" />
@if ($serviceDatabase->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}"
:resource="$serviceDatabase" />
</div>
@endisset
</div>

View File

@@ -0,0 +1,49 @@
<div>
@if (
$resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql')
<div class="flex items-center gap-2">
<h2>Storages</h2>
<x-helper
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume
name, example: <span class='text-helper'>-pr-1</span>" />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add :uuid="$resource->uuid" />
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if (
$resource->persistentStorages()->get()->count() === 0 &&
$resource->fileStorages()->get()->count() == 0)
<div>No storage found.</div>
@else
@if ($resource->persistentStorages()->get()->count() > 0)
<livewire:project.shared.storages.all :resource="$resource" />
@endif
@if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="resource-{{ $resource->uuid }}" />
@endforeach
</div>
@endif
@endif
@else
@if (
$resource->persistentStorages()->get()->count() > 0 ||
$resource->fileStorages()->get()->count() > 0)
<h3 class="pt-4">{{ Str::headline($resource->name) }} </h3>
@endif
@if ($resource->persistentStorages()->get()->count() > 0)
<livewire:project.shared.storages.all :resource="$resource" />
@endif
@if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="resource-{{ $resource->uuid }}" />
@endforeach
</div>
@endif
@endif
</div>

View File

@@ -0,0 +1,22 @@
<div x-init="$wire.getLogs">
<div class="flex gap-2">
<h2>Logs</h2>
@if ($streamLogs)
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif
</div>
<form wire:submit.prevent='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 class="w-32">
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
</div>
<div class="container w-full pt-4 mx-auto">
<div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">
<pre class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
</div>
</div>
</div>

View File

@@ -5,24 +5,27 @@
</div>
<div class="pb-4">Define how your resource's health should be checked.</div>
<div class="flex flex-col gap-4">
<div class="flex gap-2">
<x-forms.input id="resource.health_check_method" placeholder="GET" label="Method" required />
<div class="w-32">
<x-forms.checkbox instantSave id="resource.health_check_enabled" label="Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_method" placeholder="GET" label="Method" required />
<x-forms.input id="resource.health_check_scheme" placeholder="http" label="Scheme" required />
<x-forms.input id="resource.health_check_host" placeholder="localhost" label="Host" required />
<x-forms.input id="resource.health_check_port"
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
<x-forms.input id="resource.health_check_path" placeholder="/health" label="Path" required />
<x-forms.input id="resource.health_check_scheme" placeholder="http" label="Scheme" required />
<x-forms.input id="resource.health_check_host" placeholder="localhost" label="Host" required />
<x-forms.input id="resource.health_check_port"
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
<x-forms.input id="resource.health_check_path" placeholder="/health" label="Path" required />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_return_code" placeholder="200" label="Return Code" required />
<x-forms.input id="resource.health_check_response_text" placeholder="OK" label="Response Text" />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_interval" placeholder="30" label="Interval" required />
<x-forms.input id="resource.health_check_timeout" placeholder="30" label="Timeout" required />
<x-forms.input id="resource.health_check_retries" placeholder="3" label="Retries" required />
<x-forms.input id="resource.health_check_start_period" placeholder="30" label="Start Period" required />
</div>
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_return_code" placeholder="200" label="Return Code" required />
<x-forms.input id="resource.health_check_response_text" placeholder="OK" label="Response Text" />
</div>
<div class="flex gap-2">
<x-forms.input id="resource.health_check_interval" placeholder="30" label="Interval" required />
<x-forms.input id="resource.health_check_timeout" placeholder="30" label="Timeout" required />
<x-forms.input id="resource.health_check_retries" placeholder="3" label="Retries" required />
<x-forms.input id="resource.health_check_start_period" placeholder="30" label="Start Period" required />
</div>
</div>
</form>

View File

@@ -0,0 +1,32 @@
<x-layout>
@if ($type === 'application')
<h1>Logs</h1>
<livewire:project.application.heading :application="$resource" />
<div class="pt-4">
<livewire:project.shared.get-logs :server="$server" :container="$container" />
</div>
@elseif ($type === 'database')
<h1>Logs</h1>
<livewire:project.database.heading :database="$resource" />
<div class="pt-4">
@if (Str::of($status)->startsWith('running'))
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@else
Database is not running.
@endif
</div>
@elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" />
<div class="flex gap-4 pt-6">
<div>
<a class="{{ request()->routeIs('project.service.show') ? 'text-white' : '' }}"
href="{{ route('project.service.show', $parameters) }}">
<button><- Back</button>
</a>
</div>
<div class="flex-1 pl-8">
<livewire:project.shared.get-logs :server="$server" :container="$container" />
</div>
</div>
@endif
</x-layout>

View File

@@ -1,28 +1,12 @@
<div>
<div>
<div class="flex items-center gap-2">
<h2>Storages</h2>
@if ($resource->type() !== 'service')
<x-helper
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume
name, example: <span class='text-helper'>-pr-1</span>" />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add />
@endif
</div>
<div>Persistent storage to preserve data between deployments.</div>
</div>
<div class="flex flex-col gap-2 py-4">
@forelse ($resource->persistentStorages as $storage)
<div class="flex flex-col gap-4">
@foreach ($resource->persistentStorages as $storage)
@if ($resource->type() === 'service')
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage"
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" :isFirst="$loop->first"
isReadOnly='true' />
@else
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" />
@endif
@empty
<div class="text-neutral-500">No volume storages found.</div>
@endforelse
@endforeach
</div>
</div>

View File

@@ -6,19 +6,28 @@
reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
@once ($isReadOnly)
<span class="text-warning">Please modify storage layout in your <a
class="underline" href="{{ Str::of(url()->current())->beforeLast('/') }}#compose">Docker Compose</a> file.</span>
@endonce
<form wire:submit.prevent='submit' class="flex flex-col gap-2 pt-4 xl:items-end xl:flex-row">
<form wire:submit.prevent='submit' class="flex flex-col gap-2 xl:items-end xl:flex-row">
@if ($isReadOnly)
<x-forms.input id="realName" label="Volume Name" required readonly />
<x-forms.input id="storage.host_path" label="Source Path" readonly />
<x-forms.input id="storage.mount_path" label="Destination Path" required readonly />
@if ($isFirst)
<x-forms.input id="storage.name" label="Volume Name" required readonly />
<x-forms.input id="storage.host_path" label="Source Path (on host)" readonly />
<x-forms.input id="storage.mount_path" label="Destination Path (in container)" required readonly />
@else
<x-forms.input id="storage.name" required readonly />
<x-forms.input id="storage.host_path" readonly />
<x-forms.input id="storage.mount_path" required readonly />
@endif
@else
<x-forms.input id="storage.name" label="Name" required />
<x-forms.input id="storage.host_path" label="Source Path" />
<x-forms.input id="storage.mount_path" label="Destination Path" required />
@if ($isFirst)
<x-forms.input id="storage.name" label="Volume Name" required />
<x-forms.input id="storage.host_path" label="Source Path (on host)" />
<x-forms.input id="storage.mount_path" label="Destination Path (in container)" required />
@else
<x-forms.input id="storage.name" required />
<x-forms.input id="storage.host_path" />
<x-forms.input id="storage.mount_path" required />
@endif
<div class="flex gap-2">
<x-forms.button type="submit">
Update

View File

@@ -14,21 +14,20 @@
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@endif
<a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'"
href="#">Server
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@if ($application->git_based())
<a :class="activeTab === 'previews' && 'text-white'"
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Previews
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview
Deployments
</a>
@endif
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
<a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a>
@@ -56,7 +55,7 @@
<livewire:project.shared.destination :destination="$application->destination" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$application" />
<livewire:project.service.storage :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'previews'">
<livewire:project.application.previews :application="$application" />

View File

@@ -47,7 +47,7 @@
<livewire:project.shared.destination :destination="$database->destination" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$database" />
<livewire:project.service.storage :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'resource-limits'">
<livewire:project.shared.resource-limits :resource="$database" />

View File

@@ -17,17 +17,16 @@
@forelse ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}')">
<div class="flex flex-col mx-6">
<div class="flex flex-col flex-1 mx-6">
<a class=" group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">{{ $project->name }}</a>
<div class="text-xs group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
{{ $project->description }}</div>
</div>
<div class="flex-1"></div>
<a class="mx-4 rounded hover:text-white"
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path

View File

@@ -9,7 +9,9 @@ use App\Http\Controllers\ServerController;
use App\Http\Livewire\Boarding\Index as BoardingIndex;
use App\Http\Livewire\Project\Service\Index as ServiceIndex;
use App\Http\Livewire\Project\Service\Show as ServiceShow;
use App\Http\Livewire\Dev\Compose as Compose;
use App\Http\Livewire\Dashboard;
use App\Http\Livewire\Project\Shared\Logs;
use App\Http\Livewire\Server\All;
use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
@@ -28,6 +30,9 @@ use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse;
use Laravel\Fortify\Fortify;
if (isDev()) {
Route::get('/dev/compose', Compose::class)->name('dev.compose');
}
Route::post('/forgot-password', function (Request $request) {
if (is_transactional_emails_active()) {
$arrayOfRequest = $request->only(Fortify::email());
@@ -80,14 +85,19 @@ Route::middleware(['auth'])->group(function () {
[ApplicationController::class, 'deployment']
)->name('project.application.deployment');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/logs', Logs::class)->name('project.application.logs');
// Databases
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [DatabaseController::class, 'configuration'])->name('project.database.configuration');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups', [DatabaseController::class, 'backups'])->name('project.database.backups.all');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups/{backup_uuid}', [DatabaseController::class, 'executions'])->name('project.database.backups.executions');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/logs', Logs::class)->name('project.database.logs');
// Services
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs');
});
Route::middleware(['auth'])->group(function () {
@@ -99,7 +109,7 @@ Route::middleware(['auth'])->group(function () {
]))->name('server.proxy');
Route::get('/server/{server_uuid}/private-key', fn () => view('server.private-key', [
'server' => Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(),
'privateKeys' => PrivateKey::ownedByCurrentTeam()->get(),
'privateKeys' => PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false),
]))->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', fn () => view('server.destinations', [
'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail()
@@ -133,7 +143,7 @@ Route::middleware(['auth'])->group(function () {
Route::middleware(['auth'])->group(function () {
Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', fn () => view('security.private-key.index', [
'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related'])->where('is_git_related', false)->get()
'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related'])->get()
]))->name('security.private-key.index');
Route::get('/security/private-key/new', fn () => view('security.private-key.new'))->name('security.private-key.new');
Route::get('/security/private-key/{private_key_uuid}', fn () => view('security.private-key.show', [

View File

@@ -111,13 +111,17 @@ Route::post('/source/github/events', function () {
$applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false);
if ($x_github_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$branch'.");
}
}
if ($x_github_event === 'pull_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
@@ -168,9 +172,9 @@ Route::post('/source/github/events', function () {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application);
ray('Stopping container: ' . $container_name);
remote_process(["docker rm -f $container_name"], $application->destination->server);
$container_name = generateApplicationContainerName($application,$pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
return response('Preview Deployment closed.');
}
return response('Nothing to do. No Preview Deployment found');
@@ -323,7 +327,7 @@ Route::post('/payments/stripe/events', function () {
}
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
if ($cancelAtPeriodEnd) {
send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
// send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
} else {
send_internal_notification('Subscription resumed for team: ' . $subscription->team->id);
}
@@ -342,7 +346,7 @@ Route::post('/payments/stripe/events', function () {
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true,
]);
send_internal_notification('Subscription cancelled: ' . $subscription->team->id);
// send_internal_notification('Subscription cancelled: ' . $subscription->team->id);
break;
case 'customer.subscription.trial_will_end':
$customerId = data_get($data, 'customer');

View File

@@ -20,6 +20,11 @@ function help {
compgen -A function | cat -n
}
function setup:dev {
docker exec coolify bash -c "php artisan key:generate"
docker exec coolify bash -c "composer install"
docker exec coolify bash -c "php artisan migrate:fresh --seed"
}
function sync:v3 {
if [ -z "$1" ]; then
echo -e "Please provide a version.\n\nExample: run sync:v3 3.12.32"

57
scripts/sync_volume.sh Normal file
View File

@@ -0,0 +1,57 @@
#!/bin/bash
# Sync docker volumes between two servers
VERSION="1.0.0"
SOURCE=$1
DESTINATION=$2
set -e
if [ -z "$SOURCE" ]; then
echo "Source server is not specified."
exit 1
fi
if [ -z "$DESTINATION" ]; then
echo "Destination server is not specified."
exit 1
fi
SOURCE_USER=$(echo $SOURCE | cut -d@ -f1)
SOURCE_SERVER=$(echo $SOURCE | cut -d: -f1 | cut -d@ -f2)
SOURCE_PORT=$(echo $SOURCE | cut -d: -f2 | cut -d/ -f1)
SOURCE_VOLUME_NAME=$(echo $SOURCE | cut -d/ -f2)
if ! [[ "$SOURCE_PORT" =~ ^[0-9]+$ ]]; then
echo "Invalid source port: $SOURCE_PORT"
exit 1
fi
DESTINATION_USER=$(echo $DESTINATION | cut -d@ -f1)
DESTINATION_SERVER=$(echo $DESTINATION | cut -d: -f1 | cut -d@ -f2)
DESTINATION_PORT=$(echo $DESTINATION | cut -d: -f2 | cut -d/ -f1)
DESTINATION_VOLUME_NAME=$(echo $DESTINATION | cut -d/ -f2)
if ! [[ "$DESTINATION_PORT" =~ ^[0-9]+$ ]]; then
echo "Invalid destination port: $DESTINATION_PORT"
exit 1
fi
echo "Generating backup file to ./$SOURCE_VOLUME_NAME.tgz"
ssh -p $SOURCE_PORT $SOURCE_USER@$SOURCE_SERVER "docker run -v $SOURCE_VOLUME_NAME:/volume --rm --log-driver none loomchild/volume-backup backup -c pigz -v" >./$SOURCE_VOLUME_NAME.tgz
echo ""
if [ -f "./$SOURCE_VOLUME_NAME.tgz" ]; then
echo "Uploading backup file to $DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz"
scp -P $DESTINATION_PORT ./$SOURCE_VOLUME_NAME.tgz $DESTINATION_USER@$DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz
echo ""
echo "Restoring backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)"
ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "docker run -i -v $DESTINATION_VOLUME_NAME:/volume --log-driver none --rm loomchild/volume-backup restore -c pigz -vf < ~/$DESTINATION_VOLUME_NAME.tgz"
echo ""
echo "Deleting backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)"
ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "rm ~/$DESTINATION_VOLUME_NAME.tgz"
echo ""
echo "Local file ./$SOURCE_VOLUME_NAME.tgz is not deleted."
echo ""
echo "WARNING: If you are copying a database volume, you need to set the right users/passwords on the destination service's environment variables."
echo "Why? Because we are copying the volume as-is, so the database credentials will bethe same as on the source volume."
fi

View File

@@ -0,0 +1,7 @@
{
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogJzMuMycKc2VydmljZXM6CiAgcGxhdXNpYmxlX2RiOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9ldGMvZGF0YS9wbGF1c2libGUvZGItZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kUE9TVEdSRVNfUEFTU1dPUkQKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjIzLjMuNy41LWFscGluZScKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAnL2V0Yy9kYXRhL3BsYXVzaWJsZS9ldmVudC1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ID4tCiAgICAgICAgICA8Y2xpY2tob3VzZT48cHJvZmlsZXM+PGRlZmF1bHQ+PGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPjxsb2dfcXVlcnlfdGhyZWFkcz4wPC9sb2dfcXVlcnlfdGhyZWFkcz48L2RlZmF1bHQ+PC9wcm9maWxlcz48L2NsaWNraG91c2U+CiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtdXNlci1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PGxvZ2dlcj48bGV2ZWw+d2FybmluZzwvbGV2ZWw+PGNvbnNvbGU+dHJ1ZTwvY29uc29sZT48L2xvZ2dlcj48cXVlcnlfdGhyZWFkX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48cXVlcnlfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHRleHRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjx0cmFjZV9sb2cgcmVtb3ZlPSJyZW1vdmUiLz48bWV0cmljX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48YXN5bmNocm9ub3VzX21ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHNlc3Npb25fbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHBhcnRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjwvY2xpY2tob3VzZT4KICAgIHVsaW1pdHM6CiAgICAgICAgbm9maWxlOgogICAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgICBoYXJkOiAyNjIxNDQKICBwbGF1c2libGU6CiAgICBpbWFnZTogJ3BsYXVzaWJsZS9hbmFseXRpY3M6djIuMCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgY29tbWFuZDogJ3NoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biInCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgIHBvcnRzOgogICAgICAtICc4MDAwOjgwMDAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFQ1JFVF9LRVlfQkFTRQogICAgICAtIERBVEFCQVNFX1VSTD0kREFUQUJBU0VfVVJMCiAgICAgIC0gJ0NMSUNLSE9VU0VfREFUQUJBU0VfVVJMPWh0dHA6Ly9wbGF1c2libGVfZXZlbnRzX2RiOjgxMjMvcGxhdXNpYmxlX2V2ZW50c19kYicKICAgICAgLSBNQUlMRVJfQURBUFRFUj0kTUFJTEVSX0FEQVBURVIKICAgICAgLSBTRU5ER1JJRF9BUElfS0VZPSRTRU5ER1JJRF9BUElfS0VZCiAgICAgIC0gR09PR0xFX0NMSUVOVF9JRD0kR09PR0xFX0NMSUVOVF9JRAogICAgICAtIEdPT0dMRV9DTElFTlRfU0VDUkVUPSRHT09HTEVfQ0xJRU5UX1NFQ1JFVAogICAgICAtIERJU0FCTEVfUkVHSVNUUkFUSU9OPSRESVNBQkxFX1JFR0lTVFJBVElPTgogICAgICAtIEJBU0VfVVJMPSRCQVNFX1VSTAogICAgICAtIExPR19GQUlMRURfTE9HSU5fQVRURU1QVFM9JExPR19GQUlMRURfTE9HSU5fQVRURU1QVFMK"
}
}

Some files were not shown because too many files have changed in this diff Show More