Compare commits

...

104 Commits

Author SHA1 Message Date
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
Andras Bacsai
797352a5db Merge pull request #1263 from coollabsio/next
v4.0.0-beta.50
2023-09-29 10:36:25 +02:00
Andras Bacsai
03a3405524 fix: localhost privatekey update 2023-09-29 10:32:32 +02:00
Andras Bacsai
925326b885 change sentry dsn 2023-09-29 10:32:23 +02:00
Andras Bacsai
ffdf51a490 version++ 2023-09-29 10:32:13 +02:00
Andras Bacsai
272d5547c8 Merge pull request #1262 from coollabsio/next
v4.0.0-beta.49
2023-09-29 10:21:54 +02:00
Andras Bacsai
fbaec769f0 fix: reporting handler 2023-09-29 10:21:11 +02:00
Andras Bacsai
d93640a8d9 Merge pull request #1261 from coollabsio/next
v4.0.0-beta.48
2023-09-29 09:51:51 +02:00
Andras Bacsai
3749970831 fix: update process if server has been renamed 2023-09-29 09:48:20 +02:00
Andras Bacsai
7d45bb1335 version check 2023-09-28 22:55:03 +02:00
Andras Bacsai
da04e0c027 fix: sync:bunny 2023-09-28 22:42:10 +02:00
Andras Bacsai
c637d2d90d version++ 2023-09-28 22:31:53 +02:00
Andras Bacsai
1bb8860f37 Merge pull request #1260 from coollabsio/next
v4.0.0-beta.47
2023-09-28 22:25:12 +02:00
Andras Bacsai
38a22dcf4d fix: service templates 2023-09-28 22:20:49 +02:00
Andras Bacsai
91e1eb7664 fix 2023-09-28 21:56:34 +02:00
Andras Bacsai
c7946e7551 fix: next helper image 2023-09-28 21:52:50 +02:00
Andras Bacsai
958645e37f Merge branch 'main' into next 2023-09-28 21:48:03 +02:00
Andras Bacsai
acdfa89ec1 Update coolify-helper.yml 2023-09-28 21:32:53 +02:00
Andras Bacsai
62ec85ee2e Update coolify-helper.yml 2023-09-28 21:31:50 +02:00
Andras Bacsai
8f24a66456 Update coolify-helper.yml 2023-09-28 21:26:55 +02:00
Andras Bacsai
0dd0888775 Update coolify-helper.yml 2023-09-28 21:23:25 +02:00
Andras Bacsai
5040ddea28 fix: sentry 4502634789 2023-09-28 13:46:53 +02:00
Andras Bacsai
1a04a57c01 fix: sentry 4504136641 2023-09-28 13:40:58 +02:00
Andras Bacsai
9b6c162224 fix: sentry 4510197209 2023-09-28 13:15:14 +02:00
Andras Bacsai
e22c5d22f5 fix: instance fqdn setting 2023-09-28 13:13:21 +02:00
Andras Bacsai
84e5b39830 fix: prevent sync version (it needs an option) 2023-09-28 13:07:52 +02:00
Andras Bacsai
cdb6964b0b fix: install script drops an error 2023-09-28 13:05:17 +02:00
Andras Bacsai
df4ecd47a7 fix: sync command 2023-09-28 11:54:20 +02:00
Andras Bacsai
4a84c7238a fix: cannot delete env with available services 2023-09-28 11:51:01 +02:00
Andras Bacsai
2b3057e1b4 version++ 2023-09-28 11:50:50 +02:00
87 changed files with 1153 additions and 467 deletions

View File

@@ -4,7 +4,7 @@ on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-helper.yml
- .github/workflows/coolify-helper-next.yml
- docker/coolify-helper/Dockerfile
env:

View File

@@ -77,7 +77,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:

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

@@ -16,8 +16,7 @@ class UpdateCoolify
try {
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost';
$this->server = Server::where('name', $localhost_name)->first();
$this->server = Server::find(0)->first();
if (!$this->server) {
return;
}

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,100 @@
<?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\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;
}
$application = select(
'What application do you want to delete?',
$applications->pluck('name')->toArray(),
);
$application = $applications->where('name', $application)->first();
$confirmed = confirm("Are you sure you want to delete {$application->name}?");
if (!$confirmed) {
return;
}
$application->delete();
}
private function deleteDatabase()
{
$databases = StandalonePostgresql::all();
if ($databases->count() === 0) {
$this->error('There are no databases to delete.');
return;
}
$database = select(
'What database do you want to delete?',
$databases->pluck('name')->toArray(),
);
$database = $databases->where('name', $database)->first();
$confirmed = confirm("Are you sure you want to delete {$database->name}?");
if (!$confirmed) {
return;
}
$database->delete();
}
private function deleteService()
{
$services = Service::all();
if ($services->count() === 0) {
$this->error('There are no services to delete.');
return;
}
$service = select(
'What service do you want to delete?',
$services->pluck('name')->toArray(),
);
$service = $services->where('name', $service)->first();
$confirmed = confirm("Are you sure you want to delete {$service->name}?");
if (!$confirmed) {
return;
}
$service->delete();
}
}

View File

@@ -16,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
protected $signature = 'sync:bunny {--only-template}';
protected $signature = 'sync:bunny {--only-template} {--only-version}';
/**
* The console command description.
@@ -30,7 +30,9 @@ class SyncBunny extends Command
*/
public function handle()
{
$that = $this;
$only_template = $this->option('only-template');
$only_version = $this->option('only-version');
$bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn";
@@ -46,57 +48,63 @@ class SyncBunny extends Command
$versions = "versions.json";
PendingRequest::macro('storage', function ($file) {
PendingRequest::macro('storage', function ($fileName) use($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream'
];
$fileStream = fopen($file, "r");
$file = fread($fileStream, filesize($file));
$fileStream = fopen($fileName, "r");
$file = fread($fileStream, filesize($fileName));
$that->info('Uploading: ' . $fileName);
return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw();
});
PendingRequest::macro('purge', function ($url) {
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_API_KEY'),
'Accept' => 'application/json',
];
ray('Purging: ' . $url);
$that->info('Purging: ' . $url);
return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [
"url" => $url,
"async" => false
]);
});
try {
$confirmed = confirm('Are you sure?');
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
Http::pool(fn (Pool $pool) => [
$pool->storage(file: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]);
$this->info('Service template uploaded & purged...');
return;
}
if ($only_version) {
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(file: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(file: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(file: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(file: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(file: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
$pool->storage(file: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
$this->info("{$bunny_cdn}/{$bunny_cdn_path}");
Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file_prod"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$production_env"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info("All files uploaded & purged...");
} catch (\Throwable $e) {

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

@@ -55,10 +55,12 @@ class Handler extends ExceptionHandler
}
app('sentry')->configureScope(
function (Scope $scope) {
$email = auth()?->user() ? auth()->user()->email : 'guest';
$instanceAdmin = User::find(0)->email ?? 'admin@localhost';
$scope->setUser(
[
'email' => auth()->user()->email,
'instanceAdmin' => User::find(0)->email
'email' => $email,
'instanceAdmin' => $instanceAdmin
]
);
}

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
@@ -45,10 +45,10 @@ class ProjectController extends Controller
public function new()
{
$services = Cache::get('services', []);
$services = getServiceTemplates();
$type = Str::of(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server');
$server_id = request()->query('server_id');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
@@ -66,19 +66,23 @@ class ProjectController extends Controller
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($type->startsWith('one-click-service-')) {
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();

View File

@@ -129,6 +129,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function selectExistingPrivateKey()
{
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
public function createNewServer()

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

@@ -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

@@ -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

@@ -60,28 +60,9 @@ class Select extends Component
if ($forceReload) {
Cache::forget('services');
}
if (isDev()) {
$cached = Cache::remember('services', 3600, function () {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
$this->emit('success', 'Successfully reloaded services from filesystem (development mode).');
return $services;
});
} else {
$cached = Cache::remember('services', 3600, function () {
$services = Http::get(config('constants.services.official'));
if ($services->failed()) {
throw new \Exception($services->body());
}
$services = collect($services->json())->sortKeys();
$this->emit('success', 'Successfully reloaded services from the internet.');
return $services;
});
}
$this->services = $cached;
$this->services = getServiceTemplates();
$this->emit('success', 'Successfully loaded services.');
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
} finally {
$this->loadingServices = false;

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,25 @@ 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->refreshStack();
}
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 +52,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

@@ -11,8 +11,8 @@ use Livewire\Component;
class Show extends Component
{
public Service $service;
public ServiceApplication $serviceApplication;
public ServiceDatabase $serviceDatabase;
public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null;
public array $parameters;
public array $query;
public Collection $services;
@@ -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,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();
$name = $this->uuid . '-' . $this->name;
$this->emitUp('submit', [
'name' => $this->name,
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);

View File

@@ -7,6 +7,7 @@ use Livewire\Component;
class All extends Component
{
public bool $isHeaderVisible = true;
public $resource;
protected $listeners = ['refreshStorages', 'submit'];

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

@@ -14,8 +14,8 @@ class ShowPrivateKey extends Component
public function setPrivateKey($newPrivateKeyId)
{
try {
refresh_server_connection($this->server->privateKey);
$oldPrivateKeyId = $this->server->private_key_id;
refresh_server_connection($this->server->privateKey);
$this->server->update([
'private_key_id' => $newPrivateKeyId
]);

View File

@@ -73,7 +73,7 @@ class Configuration extends Component
{
$file = "$this->dynamic_config_path/coolify.yaml";
if (empty($this->settings->fqdn)) {
remote_process([
instant_remote_process([
"rm -f $file",
], $this->server);
} else {
@@ -129,7 +129,6 @@ class Configuration extends Component
];
}
$this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ContainerStatusJob($this->server));
}
}
@@ -142,7 +141,7 @@ class Configuration extends Component
$yaml;
$base64 = base64_encode($yaml);
remote_process([
instant_remote_process([
"mkdir -p $this->dynamic_config_path",
"echo '$base64' | base64 -d > $file",
], $this->server);

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

@@ -89,7 +89,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -97,7 +97,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();
@@ -284,7 +286,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,6 +303,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function health_check()
{
if ($this->application->isHealthcheckDisabled()) {
$this->newVersionIsHealthy = true;
return;
}
ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
@@ -373,9 +379,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}";
@@ -399,7 +402,7 @@ 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->workdir}. '"
],
[
$this->importing_git_repository()
@@ -571,6 +574,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;
}

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();
@@ -212,13 +218,9 @@ class Application extends BaseModel
{
if (data_get($this, 'private_key_id')) {
return 'deploy_key';
}
if (data_get($this, 'source')) {
} else if (data_get($this, 'source')) {
return 'source';
}
if (data_get($this, 'private_key_id')) {
return 'deploy_key';
}
throw new \Exception('No deployment type found');
}
public function could_set_build_commands(): bool
@@ -230,9 +232,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

@@ -14,7 +14,7 @@ class Environment extends Model
public function can_delete_environment()
{
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0;
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
}
public function applications()

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,40 @@ 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);
ray($isFile);
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 (($isFile == 'NOK' && !$fileVolume->is_directory) || $isFile == 'OK') {
$rootDir = Str::of($path)->dirname();
$commands->push("mkdir -p $rootDir > /dev/null 2>&1 || true");
$commands->push("touch $path > /dev/null 2>&1 || true");
if ($content) {
$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");
}
return instant_remote_process($commands, $server);
}
}

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
{
@@ -20,6 +19,7 @@ class Service extends BaseModel
static::deleted(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);
@@ -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, $isNew) {
$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
@@ -344,8 +385,7 @@ class Service extends BaseModel
}
if ($key->startsWith('SERVICE_FQDN')) {
if (is_null(data_get($savedService, 'fqdn'))) {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
$fqdn = generateFqdn($this->server, $containerName);
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) {
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
@@ -358,7 +398,7 @@ class Service extends BaseModel
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
$fqdn = "http://$containerName.$sslip$path";
$fqdn = "$fqdn$path";
}
if (!$isDatabase) {
$savedService->fqdn = $fqdn;
@@ -379,8 +419,7 @@ class Service extends BaseModel
$forService = $value->afterLast('_');
$generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
$fqdn = generateFqdn($this->server, $containerName);
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {

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

@@ -26,8 +26,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

@@ -29,9 +29,20 @@ class StandalonePostgresql extends BaseModel
]);
});
static::deleted(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);
});
}

View File

@@ -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)
@@ -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));

View File

@@ -64,40 +64,57 @@ 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");
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;
}
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($isFile === 'NOK' &&!$fileVolume->is_directory && $isInit) {
$fileVolume->saveStorageOnServer($oneService);
continue;
}
$content = $fileVolume->content;
$dir = $path->beforeLast('/');
if ($dir->startsWith('.')) {
$dir = $dir->after('.');
$dir = $workdir . $dir;
if ($isFile == 'OK' && !$fileVolume->is_directory) {
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if (base64_encode($filesystemContent) != base64_encode($content)) {
$fileVolume->content = $filesystemContent;
$fileVolume->save();
}
} else {
if ($isDir == 'OK') {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
} else {
$fileVolume->content = null;
$fileVolume->is_directory = false;
$fileVolume->save();
}
}
$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');
@@ -111,7 +128,7 @@ function updateCompose($resource) {
$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();
}

View File

@@ -395,14 +395,42 @@ 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()
{
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
} else {
$services = Http::get(config('constants.services.official'));
if ($services->failed()) {
throw new \Exception($services->body());
}
$services = collect($services->json())->sortKeys();
}
return $services;
}

View File

@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://62de992090e4e0cb28f18231835ea006@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://72f02655749d5d687297b6b9f078b8b9@o1082494.ingest.sentry.io/4505347448045568',
// 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.46',
'release' => '4.0.0-beta.62',
// 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.46';
return '4.0.0-beta.62';

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

@@ -69,11 +69,12 @@ class ProductionSeeder extends Seeder
PrivateKey::updateOrCreate(
[
'id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'team_id' => 0,
],
['private_key' => $coolify_key]
[
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).', 'private_key' => $coolify_key
]
);
} else {
echo "No SSH key found for the Coolify host machine (localhost).\n";

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
- ./_data/coolify/_volumes/database/:/var/lib/postgresql/data
redis:
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
- .env
volumes:
- /data/coolify/_volumes/redis/:/data
- ./_data/coolify/_volumes/redis/:/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
- ./_data/coolify/:/data/coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail
@@ -76,6 +76,6 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- /data/coolify/_volumes/minio/:/data
- ./_data/coolify/_volumes/minio/:/data
networks:
- coolify

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

@@ -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

@@ -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">
@@ -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

@@ -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">
<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,120 @@
<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'">
@foreach ($applications as $application)
@if ($loop->first)
<livewire:project.shared.storages.all :resource="$application" />
@else
<livewire:project.shared.storages.all :resource="$application" :isHeaderVisible="false" />
@endif
@if ($application->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($application->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
@endforeach
@foreach ($databases as $database)
@if ($loop->first)
<h3 class="pt-4">{{ Str::headline($database->name) }}</h3>
@if ($applications->count() > 0)
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@else
<livewire:project.shared.storages.all :resource="$database" />
@endif
@if ($database->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($database->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
@else
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@endif
@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)
@@ -20,9 +26,9 @@
<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>
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get() as $fileStorage)
@foreach ($serviceApplication->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@@ -32,14 +38,13 @@
@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>
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get() as $fileStorage)
@foreach ($serviceDatabase->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</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,41 @@
<x-layout>
@if ($type === 'application')
<h1>Logs</h1>
<livewire:project.application.heading :application="$resource" />
<div class="pt-4">
@if (Str::of($status)->startsWith('running'))
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@else
Application is not running.
@endif
</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">
@if (serviceStatus($resource) === 'running')
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@else
Service is not running.
@endif
</div>
</div>
@endif
</x-layout>

View File

@@ -1,28 +1,33 @@
<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
@if ($isHeaderVisible)
<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 />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add :uuid="$resource->uuid" />
@endif
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if ($resource->type() === 'service')
<span class="text-warning">Please modify storage layout in your <a class="underline"
href="{{ Str::of(url()->current())->beforeLast('/') }}">Docker Compose</a> file.</span>
<h2 class="pt-4">{{ Str::headline($resource->name) }} </h2>
@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)
@endif
<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

@@ -6,7 +6,7 @@
Save
</x-forms.button>
</div>
<div class="">General configuration for your Coolify instance.</div>
<div>General configuration for your Coolify instance.</div>
<div class="flex flex-col gap-2 pt-4">
<div class="flex gap-2 w-96">

View File

@@ -22,7 +22,7 @@
</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

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

@@ -10,6 +10,7 @@ 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\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;
@@ -80,14 +81,20 @@ 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 +106,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 +140,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

@@ -72,14 +72,21 @@ cat >/etc/docker/daemon.json.coolify <<EOL
EOL
cat <<<$(jq . /etc/docker/daemon.json.coolify) >/etc/docker/daemon.json.coolify
cat <<<$(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) >/etc/docker/daemon.json
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-$DATE))
if [ "$DIFF" != "" ]; then
if [ -s /etc/docker/daemon.json.original-$DATE ]; then
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-$DATE))
if [ "$DIFF" != "" ]; then
echo "Docker configuration updated, restart docker daemon..."
systemctl restart docker
else
echo "Docker configuration is up to date."
fi
else
echo "Docker configuration updated, restart docker daemon..."
systemctl restart docker
else
echo "Docker configuration is up to date."
fi
echo -e "-------------"
mkdir -p /data/coolify/ssh/keys

View File

@@ -20,6 +20,11 @@ function help {
compgen -A function | cat -n
}
function setup:dev {
docker exec coolify bash -c "composer install"
docker exec coolify bash -c "php artisan key:generate"
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"

View File

@@ -0,0 +1,7 @@
{
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlLWFuYWx5dGljczoKICAgIGltYWdlOiBwbGF1c2libGUvYW5hbHl0aWNzOnYyLjAKICAgIGNvbW1hbmQ6IHNoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biIKICAgIGVudmlyb25tZW50OgogICAgICAtIERBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYXVzaWJsZV9kYi9wbGF1c2libGUKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1BMQVVTSUJMRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfUExBVVNJQkxFCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgICAgLSBtYWlsCgogIG1haWw6CiAgICBpbWFnZTogYnl0ZW1hcmsvc210cAoKICBwbGF1c2libGVfZGI6CiAgICBpbWFnZTogcG9zdGdyZXM6MTQtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIGRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1wbGF1c2libGUKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwoKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6IGNsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMy43LjUtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIHR5cGU6IHZvbHVtZQogICAgICAgIHNvdXJjZTogZXZlbnQtZGF0YQogICAgICAgIHRhcmdldDogL3Zhci9saWIvY2xpY2tob3VzZQogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLWNvbmZpZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PHByb2ZpbGVzPjxkZWZhdWx0Pjxsb2dfcXVlcmllcz4wPC9sb2dfcXVlcmllcz48bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+PC9kZWZhdWx0PjwvcHJvZmlsZXM+PC9jbGlja2hvdXNlPgogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLXVzZXItY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2xvZ2dpbmcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogPi0KICAgICAgICAgIDxjbGlja2hvdXNlPjxsb2dnZXI+PGxldmVsPndhcm5pbmc8L2xldmVsPjxjb25zb2xlPnRydWU8L2NvbnNvbGU+PC9sb2dnZXI+PHF1ZXJ5X3RocmVhZF9sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHF1ZXJ5X2xvZyByZW1vdmU9InJlbW92ZSIvPjx0ZXh0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48dHJhY2VfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PG1ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PGFzeW5jaHJvbm91c19tZXRyaWNfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjxzZXNzaW9uX2xvZyByZW1vdmU9InJlbW92ZSIvPjxwYXJ0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48L2NsaWNraG91c2U+CiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg=="
},
}

View File

@@ -1,9 +1,4 @@
{
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlLWFuYWx5dGljczoKICAgIGltYWdlOiBwbGF1c2libGUvYW5hbHl0aWNzOnYyLjAKICAgIGNvbW1hbmQ6IHNoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biIKICAgIGVudmlyb25tZW50OgogICAgICAtIERBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYXVzaWJsZV9kYi9wbGF1c2libGUKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1BMQVVTSUJMRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfUExBVVNJQkxFCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgICAgLSBtYWlsCgogIG1haWw6CiAgICBpbWFnZTogYnl0ZW1hcmsvc210cAoKICBwbGF1c2libGVfZGI6CiAgICBpbWFnZTogcG9zdGdyZXM6MTQtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIGRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1wbGF1c2libGUKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwoKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6IGNsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMy43LjUtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIHR5cGU6IHZvbHVtZQogICAgICAgIHNvdXJjZTogZXZlbnQtZGF0YQogICAgICAgIHRhcmdldDogL3Zhci9saWIvY2xpY2tob3VzZQogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLWNvbmZpZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PHByb2ZpbGVzPjxkZWZhdWx0Pjxsb2dfcXVlcmllcz4wPC9sb2dfcXVlcmllcz48bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+PC9kZWZhdWx0PjwvcHJvZmlsZXM+PC9jbGlja2hvdXNlPgogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLXVzZXItY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2xvZ2dpbmcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogPi0KICAgICAgICAgIDxjbGlja2hvdXNlPjxsb2dnZXI+PGxldmVsPndhcm5pbmc8L2xldmVsPjxjb25zb2xlPnRydWU8L2NvbnNvbGU+PC9sb2dnZXI+PHF1ZXJ5X3RocmVhZF9sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHF1ZXJ5X2xvZyByZW1vdmU9InJlbW92ZSIvPjx0ZXh0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48dHJhY2VfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PG1ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PGFzeW5jaHJvbm91c19tZXRyaWNfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjxzZXNzaW9uX2xvZyByZW1vdmU9InJlbW92ZSIvPjxwYXJ0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48L2NsaWNraG91c2U+CiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg=="
},
"umami": {
"documentation": "https://umami.is/docs/getting-started",
"slogan": "Umami makes it easy to collect, analyze, and understand your web data — while maintaining visitor privacy and data ownership.",
@@ -12,7 +7,7 @@
"uptime-kuma": {
"documentation": "https://github.com/louislam/uptime-kuma",
"slogan": "A free and fancy self-hosted monitoring tool.",
"compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogbG91aXNsYW0vdXB0aW1lLWt1bWE6MQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROCiAgICB2b2x1bWVzOgogICAgICAtIHVwdGltZS1rdW1hOi9hcHAvZGF0YQ=="
"compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogbG91aXNsYW0vdXB0aW1lLWt1bWE6MQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROCiAgICB2b2x1bWVzOgogICAgICAtIHVwdGltZS1rdW1hOi9hcHAvZGF0YQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6IFsiQ01ELVNIRUxMIiwgImV4dHJhL2hlYWx0aGNoZWNrIl0KICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQ=="
},
"appsmith": {
"documentation": "https://docs.appsmith.com/",

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.46"
"version": "4.0.0-beta.62"
}
}
}

View File

@@ -4,6 +4,9 @@ import vue from "@vitejs/plugin-vue";
export default defineConfig({
server: {
watch: {
ignored: ['**/_data/**'],
},
host: "0.0.0.0",
hmr: process.env.GITPOD_WORKSPACE_URL
? {