Compare commits

...

93 Commits

Author SHA1 Message Date
Andras Bacsai
21e77bf0c1 Merge pull request #1691 from coollabsio/next
v4.0.0-beta.203
2024-01-31 10:02:04 +01:00
Andras Bacsai
0686e48e89 fix: service deletion 2024-01-31 09:58:41 +01:00
Andras Bacsai
1cef233db2 fix: regenerate labels on application clone 2024-01-31 09:03:54 +01:00
Andras Bacsai
907e52572c fix: validate server navbar upated 2024-01-31 08:56:49 +01:00
Andras Bacsai
795c8abf64 Refactor service and resource deletion logic 2024-01-30 17:38:07 +01:00
Andras Bacsai
cc641d8cba revert 2024-01-30 17:16:52 +01:00
Andras Bacsai
d4668ef44a refactor 2024-01-30 14:12:40 +01:00
Andras Bacsai
e8b539c3bd Refactor CleanupUnreachableServers command to update server IP address 2024-01-30 10:53:12 +01:00
Andras Bacsai
6555f0b50c Fix unreachable server cleanup and enable daily schedule 2024-01-30 10:52:57 +01:00
Andras Bacsai
bb05058dda feat: cleanup unreachable servers 2024-01-30 10:50:54 +01:00
Andras Bacsai
9667cd4a7a fix: handle duplicate error instead of sql error
fix: set fqdns to null if you delete an app or a serviceapp
fix: make stucked resources a separate command
2024-01-30 09:48:51 +01:00
Andras Bacsai
3ae9501814 fix: dns validation + duplicated fqdns 2024-01-30 09:22:34 +01:00
Andras Bacsai
73d0948734 fix: service deletion fix 2024-01-30 08:26:33 +01:00
Andras Bacsai
c3e2a741ea Update version numbers 2024-01-29 20:35:35 +01:00
Andras Bacsai
4792146f1d Merge pull request #1687 from coollabsio/next
v4.0.0-beta.202
2024-01-29 16:37:39 +01:00
Andras Bacsai
09b9305aa3 Refactor git_clone_command in generateGitImportCommands function 2024-01-29 16:33:06 +01:00
Andras Bacsai
ff7d0d442d Update button text in private key create view 2024-01-29 16:33:01 +01:00
Andras Bacsai
9a127bdc80 Refactor webhook handling logic to remove duplicate code and improve readability 2024-01-29 16:31:10 +01:00
Andras Bacsai
919e88afb4 Refactor docker run options to compose format 2024-01-29 16:21:23 +01:00
Andras Bacsai
1d1ec20cb7 Update version numbers 2024-01-29 16:13:04 +01:00
Andras Bacsai
5c29ecdf10 feat: add initial support for custom docker run commands 2024-01-29 16:07:00 +01:00
Andras Bacsai
9e09c449cf fix: service deletion function 2024-01-29 16:03:45 +01:00
Andras Bacsai
09bcd693f5 Merge pull request #1683 from coollabsio/next
v4.0.0-beta.201
2024-01-29 14:04:30 +01:00
Andras Bacsai
0c15e45419 Update WordPress database configuration 2024-01-29 14:03:59 +01:00
Andras Bacsai
31cbf552a2 Merge pull request #1677 from mraf/wordpress-template
fix: add env variables for wordpress template without database
2024-01-29 13:58:31 +01:00
Andras Bacsai
f7853ee174 Refactor deployments_per_server variable and update dashboard view
This commit refactors the `deployments_per_server` variable in the `Dashboard` class to remove the type hint and updates the corresponding view file to handle the changes. The `deployments_per_server` variable is now grouped by `server_name` and converted to an array. This improves the organization and readability of the code.
2024-01-29 13:26:50 +01:00
Andras Bacsai
de3a7b6eca Add previous page functionality to deployment index
This commit adds the functionality to navigate to the previous page in the deployment index. It includes changes to the `Index.php` and `index.blade.php` files.
2024-01-29 13:06:26 +01:00
Andras Bacsai
b56c7c34cb fix: unhealthy deployments should be failed 2024-01-29 12:51:20 +01:00
Andras Bacsai
49845f3da7 fix: webhooks for multiple apps 2024-01-29 11:23:04 +01:00
Andras Bacsai
987409bae4 fix: bitbucket manual deployments 2024-01-29 10:43:18 +01:00
Andras Bacsai
07d8461f96 Fix URL encoding in deployment and status notifications 2024-01-29 08:49:05 +01:00
Andras Bacsai
f255a71434 Merge pull request #1673 from Niki2k1/feat/bitbucket-manual-webhook
feat: added manual webhook support for bitbucket
2024-01-29 08:38:05 +01:00
Andras Bacsai
2a2818ac0d Merge pull request #1680 from iamEvanYT/discord-deployment-url-fix
fix: encode project name in discord webhook notifications
2024-01-29 08:32:57 +01:00
Andras Bacsai
fd3cdc2c7d Update deployment status border color 2024-01-29 08:32:04 +01:00
Andras Bacsai
84c3f832ae Add missing closing div tag in dashboard.blade.php 2024-01-29 08:30:00 +01:00
Andras Bacsai
70c28fceeb fix: change env variable length 2024-01-29 08:24:21 +01:00
iamEvan
f2c4f83f5a Fix 2024-01-27 20:33:22 +00:00
iamEvan
c8dd6f07ac Encode Project Name 2024-01-27 20:22:27 +00:00
Andras Bacsai
561e424a7d feat: dashboard live deployment view 2024-01-27 18:44:40 +01:00
Andras Bacsai
c46d38907e fix: queue 2024-01-27 17:18:13 +01:00
Andras Bacsai
5c334bbac6 feat: add PR comments 2024-01-26 18:46:50 +01:00
Andras Bacsai
9628072b0c Update dependencies in package.json and mix-manifest.json 2024-01-26 11:36:44 +01:00
Andras Bacsai
39ecff9f90 Update version numbers 2024-01-26 11:34:18 +01:00
Andras Bacsai
d1daec060a Merge pull request #1675 from coollabsio/next
v4.0.0-beta.200
2024-01-26 11:25:32 +01:00
Andras Bacsai
fb5bea7f91 Update Docker actions versions 2024-01-26 11:23:49 +01:00
Andras Bacsai
efc3ea6e40 Update Docker actions versions 2024-01-26 11:20:27 +01:00
Andras Bacsai
ecefb9e1f5 Update vite version to 4.5.2 2024-01-26 11:15:35 +01:00
Andras Bacsai
a4dea2009a Remove unused imports and routes 2024-01-26 11:13:02 +01:00
Andras Bacsai
829e41f93f Delete TeamSharedVariablesIndexTest.php 2024-01-26 11:12:07 +01:00
Andras Bacsai
e7e3adc7fb Fix condition for checking localhost key in ProductionSeeder.php 2024-01-26 11:11:41 +01:00
Andras Bacsai
a993fef235 Update Docker build command 2024-01-26 10:56:22 +01:00
Andras Bacsai
81df71416c Update Docker images and add pull_policy 2024-01-26 10:52:58 +01:00
Andras Bacsai
40af7aa025 Update Docker build command to include latest tag 2024-01-26 10:46:46 +01:00
Andras Bacsai
050155328b Update toast.blade.php to use x-html instead of x-text 2024-01-26 10:37:53 +01:00
Andras Bacsai
376c081bed Add .env file as read-only volume 2024-01-26 10:37:33 +01:00
Andras Bacsai
9f5e1fa9e3 Update docker-compose.windows.yml file 2024-01-26 10:34:48 +01:00
Andras Bacsai
7d139fd33b Add environment file for Windows Docker Desktop 2024-01-26 10:31:37 +01:00
Andras Bacsai
b75a2857a0 Update environment variables and Docker image for Windows Docker Desktop 2024-01-26 10:24:42 +01:00
Andras Bacsai
ab6c1ddc20 Update docker-compose.windows.yml with bind mount for .env file 2024-01-26 10:16:44 +01:00
Andras Bacsai
dc0b0980a9 Update coolify image and environment variables 2024-01-26 09:57:59 +01:00
Andras Bacsai
788d1711db Refactor SSH command generation in remoteProcess.php 2024-01-26 09:36:08 +01:00
Andras Bacsai
d92dc4c5e6 Update testing-host image and remove unnecessary build configuration 2024-01-26 09:03:59 +01:00
Andras Bacsai
4bf34aea62 Add Coolify Testing Host workflow 2024-01-26 08:59:32 +01:00
Andras Bacsai
7e9a54ce67 Fix SSH command generation and disable mux in validateConnection() 2024-01-26 08:54:56 +01:00
Andras Bacsai
f8c19e1fb3 Update contact links in error and subscription views 2024-01-26 08:39:54 +01:00
Andras Bacsai
27c1bda09b Update button labels in create forms 2024-01-25 16:02:31 +01:00
Andras Bacsai
1af7ffcdc4 Refactor input field for port number 2024-01-25 15:58:58 +01:00
Andras Bacsai
39647367a5 Add build pack selection and update related fields 2024-01-25 15:57:04 +01:00
Andras Bacsai
ae4b263810 Update GitHub App registration button 2024-01-25 15:26:23 +01:00
Andras Bacsai
8901bb5df8 Refactor deployment cancellation and queue management 2024-01-25 13:45:17 +01:00
mraf
053aa25d2c fix: add env variables for wordpress template without database 2024-01-25 12:27:41 +01:00
Andras Bacsai
7a7157c155 fix: deployment queue
fix: cancel deployment
ui: changed to simpler toaster
2024-01-25 11:57:47 +01:00
Andras Bacsai
0c5e8600bd Update build pack settings and port values 2024-01-25 08:59:11 +01:00
Andras Bacsai
048e153025 Remove unnecessary condition in setDestination method 2024-01-25 08:57:16 +01:00
Andras Bacsai
e7cafe6850 fix: restrict concurrent deployments per server 2024-01-25 08:36:47 +01:00
Andras Bacsai
1385a86084 Refactor team() method and update references to team() in get_real_environment_variables() method 2024-01-24 15:56:43 +01:00
Andras Bacsai
348923ae02 Refactor realValue() method to include resource lookup 2024-01-24 15:54:55 +01:00
Andras Bacsai
7d754558b0 Fix branch selection and handle missing service 2024-01-24 12:26:14 +01:00
Andras Bacsai
744609e7e9 fix sentry errors 2024-01-24 12:10:03 +01:00
Andras Bacsai
9bd05b65a3 Update build pack and make GithubApp nullable 2024-01-24 12:07:58 +01:00
Andras Bacsai
d42934f258 fix: sentry error 2024-01-24 11:57:51 +01:00
Andras Bacsai
ff752e2411 feat: able to deploy multiple resources with webhook 2024-01-24 11:49:40 +01:00
Andras Bacsai
4120fba9a8 Update default concurrent_builds value to 2 2024-01-24 11:48:00 +01:00
Andras Bacsai
6ecb9c21ce cloud: send notification email if payment 2024-01-24 11:28:01 +01:00
Andras Bacsai
01f7b07fa3 feat: concurrent builds / server 2024-01-24 11:12:23 +01:00
Niklas Lausch
54d8cb9027 feat: added manual webhook support for bitbucket 2024-01-24 10:56:24 +01:00
Andras Bacsai
238337fecb Add new shared variable and update variable usage 2024-01-23 20:26:45 +01:00
Andras Bacsai
7a51acbf8d add slide-over component 2024-01-23 19:01:17 +01:00
Andras Bacsai
fb478c79b3 feat: shared environments 2024-01-23 17:13:23 +01:00
Andras Bacsai
abcc004953 feat: clone any resource 2024-01-22 16:08:18 +01:00
Andras Bacsai
2edf71a0dd feat: move resources between projects / environments 2024-01-22 15:12:38 +01:00
Andras Bacsai
cbec39099a Refactor Git section in advanced.blade.php 2024-01-22 14:13:40 +01:00
Andras Bacsai
dba5499182 Update version numbers + update glitchtip 2024-01-22 12:00:44 +01:00
151 changed files with 3693 additions and 2720 deletions

View File

@@ -0,0 +1,12 @@
IS_WINDOWS_DOCKER_DESKTOP=true
APP_ID=coolify-windows-docker-desktop
APP_NAME=Coolify
APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80=
DB_PASSWORD=coolify
REDIS_PASSWORD=coolify
PUSHER_APP_ID=coolify
PUSHER_APP_KEY=coolify
PUSHER_APP_SECRET=coolify

View File

@@ -0,0 +1,84 @@
name: Coolify Testing Host (v4-non-prod)
on:
push:
branches: [ "main", "next" ]
paths:
- .github/workflows/coolify-testing-host.yml
- docker/testing-host/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-testing-host"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
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:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -15,15 +15,15 @@ jobs:
amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod-ssu/Dockerfile
@@ -36,15 +36,15 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod-ssu/Dockerfile
@@ -59,13 +59,13 @@ jobs:
needs: [amd64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}

View File

@@ -12,9 +12,9 @@ jobs:
amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -24,7 +24,7 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod-ssu/Dockerfile
@@ -34,9 +34,9 @@ jobs:
aarch64:
runs-on: [self-hosted, arm64]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -46,7 +46,7 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod-ssu/Dockerfile
@@ -61,13 +61,13 @@ jobs:
needs: [amd64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -78,7 +78,7 @@ jobs:
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:

View File

@@ -11,6 +11,9 @@ class StopApplication
public function handle(Application $application)
{
$server = $application->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
} else {

View File

@@ -140,7 +140,7 @@ class StartMariadb
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {

View File

@@ -156,7 +156,7 @@ class StartMongodb
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {

View File

@@ -140,7 +140,7 @@ class StartMysql
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {

View File

@@ -164,7 +164,7 @@ class StartPostgresql
ray('Generate Environment Variables')->green();
ray($this->database->runtime_environment_variables)->green();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {

View File

@@ -151,7 +151,7 @@ class StartRedis
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {

View File

@@ -17,6 +17,9 @@ class StopDatabase
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{
$server = $database->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
instant_remote_process(
["docker rm -f {$database->uuid}"],
$server

View File

@@ -10,35 +10,41 @@ class DeleteService
use AsAction;
public function handle(Service $service)
{
$server = data_get($service, 'server');
if ($server->isFunctional()) {
StopService::run($service);
}
$storagesToDelete = collect([]);
try {
$server = data_get($service, 'server');
if ($server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
$commands = [];
foreach ($service->applications()->get() as $application) {
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
$service->environment_variables()->delete();
$commands = [];
foreach ($service->applications()->get() as $application) {
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
}
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false);
}
$application->forceDelete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
foreach ($service->databases()->get() as $database) {
$database->forceDelete();
}
$database->forceDelete();
}
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false);
$service->forceDelete();
}
}

View File

@@ -10,6 +10,10 @@ class StopService
use AsAction;
public function handle(Service $service)
{
$server = $service->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping service: ' . $service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {

View File

@@ -0,0 +1,295 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command;
class CleanupStuckedResources extends Command
{
protected $signature = 'cleanup:stucked-resources';
protected $description = 'Cleanup Stucked Resources';
public function handle()
{
echo "Running cleanup stucked...\n";
$this->cleanup_stucked_resources();
}
private function cleanup_stucked_resources()
{
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
echo "Deleting stuck application: {$application->name}\n";
$application->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
echo "Deleting stuck postgresql: {$postgresql->name}\n";
$postgresql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($redis as $redis) {
echo "Deleting stuck redis: {$redis->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) {
echo "Deleting stuck mongodb: {$mongodb->name}\n";
$mongodb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mysqls as $mysql) {
echo "Deleting stuck mysql: {$mysql->name}\n";
$mysql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mariadbs as $mariadb) {
echo "Deleting stuck mariadb: {$mariadb->name}\n";
$mariadb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($services as $service) {
echo "Deleting stuck service: {$service->name}\n";
$service->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
}
try {
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceApps as $serviceApp) {
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
$serviceApp->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
try {
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceDbs as $serviceDb) {
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
$serviceDb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
foreach ($applications as $application) {
if (!data_get($application, 'environment')) {
echo 'Application without environment: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!$application->destination()) {
echo 'Application without destination: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!data_get($application, 'destination.server')) {
echo 'Application without server: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!$postgresql->destination()) {
echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) {
echo 'Redis without environment: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!$redis->destination()) {
echo 'Redis without destination: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!data_get($redis, 'destination.server')) {
echo 'Redis without server: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) {
echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!$mongodb->destination()) {
echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) {
echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!$mysql->destination()) {
echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) {
echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!$mariadb->destination()) {
echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::all();
foreach ($services as $service) {
if (!data_get($service, 'environment')) {
echo 'Service without environment: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!$service->destination()) {
echo 'Service without destination: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!data_get($service, 'server')) {
echo 'Service without server: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in service: {$e->getMessage()}\n";
}
try {
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in serviceApplications: {$e->getMessage()}\n";
}
try {
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
class CleanupUnreachableServers extends Command
{
protected $signature = 'cleanup:unreachable-servers';
protected $description = 'Cleanup Unreachable Servers (3 days)';
public function handle()
{
echo "Running unreachable server cleanup...\n";
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
$server->update([
'ip' => '1.2.3.4'
]);
}
}
}
}

View File

@@ -4,19 +4,11 @@ namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
@@ -31,7 +23,7 @@ class Init extends Command
$cleanup = $this->option('cleanup');
if ($cleanup) {
echo "Running cleanups...\n";
$this->cleanup_stucked_resources();
$this->call('cleanup:stucked-resources');
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
@@ -137,273 +129,5 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_resources()
{
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
echo "Deleting stuck application: {$application->name}\n";
$application->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
echo "Deleting stuck postgresql: {$postgresql->name}\n";
$postgresql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($redis as $redis) {
echo "Deleting stuck redis: {$redis->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) {
echo "Deleting stuck mongodb: {$mongodb->name}\n";
$mongodb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mysqls as $mysql) {
echo "Deleting stuck mysql: {$mysql->name}\n";
$mysql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mariadbs as $mariadb) {
echo "Deleting stuck mariadb: {$mariadb->name}\n";
$mariadb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($services as $service) {
echo "Deleting stuck service: {$service->name}\n";
$service->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
}
try {
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceApps as $serviceApp) {
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
$serviceApp->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
try {
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceDbs as $serviceDb) {
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
$serviceDb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
foreach ($applications as $application) {
if (!data_get($application, 'environment')) {
echo 'Application without environment: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!$application->destination()) {
echo 'Application without destination: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!data_get($application, 'destination.server')) {
echo 'Application without server: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!$postgresql->destination()) {
echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) {
echo 'Redis without environment: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!$redis->destination()) {
echo 'Redis without destination: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!data_get($redis, 'destination.server')) {
echo 'Redis without server: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) {
echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!$mongodb->destination()) {
echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) {
echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!$mysql->destination()) {
echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) {
echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!$mariadb->destination()) {
echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::all();
foreach ($services as $service) {
if (!data_get($service, 'environment')) {
echo 'Service without environment: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!$service->destination()) {
echo 'Service without destination: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!data_get($service, 'server')) {
echo 'Service without server: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in service: {$e->getMessage()}\n";
}
try {
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in serviceApplications: {$e->getMessage()}\n";
}
try {
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
}
}
}

View File

@@ -36,6 +36,8 @@ class Kernel extends ConsoleKernel
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();

View File

@@ -10,4 +10,5 @@ enum ProcessStatus: string
case ERROR = 'error';
case KILLED = 'killed';
case CANCELLED = 'cancelled';
case CLOSED = 'closed';
}

View File

@@ -3,7 +3,7 @@
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyTypes;
use App\Enums\ProcessStatus;
use App\Events\ApplicationStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
@@ -158,6 +158,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->preview->fqdn = $preview_fqdn;
$this->preview->save();
}
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
}
}
}
@@ -220,7 +223,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_server = $this->server;
$this->original_server = $this->server;
}
ray($this->build_server);
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
@@ -230,6 +232,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(false);
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
@@ -241,11 +245,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else {
$this->deploy_nixpacks_buildpack();
}
$this->deploy_nixpacks_buildpack();
}
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
@@ -255,8 +255,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->push_to_docker_registry();
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
}
}
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
}
ray($e);
$this->fail($e);
throw $e;
} finally {
@@ -311,7 +320,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function push_to_docker_registry($forceFail = false)
{
ray((str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()));
if (
$this->application->docker_registry_image_name &&
$this->application->build_pack !== 'dockerimage' &&
@@ -340,9 +348,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.'");
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.");
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.'");
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
if ($forceFail) {
throw $e;
}
@@ -383,7 +391,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function just_restart()
{
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'");
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
@@ -417,11 +425,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$envs = collect([]);
if ($this->pull_request_id !== 0) {
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->value);
$envs->push($env->key . '=' . $env->real_value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
$envs->push($env->key . '=' . $env->real_value);
}
}
$envs_base64 = base64_encode($envs->implode("\n"));
@@ -480,10 +488,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
$this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
ray('asddf');
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->generate_image_names();
@@ -681,7 +687,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->full_healthcheck_url) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
while ($counter < $this->application->health_check_retries) {
while ($counter <= $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
@@ -724,10 +730,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_nixpacks_confs();
}
$this->generate_compose_file();
// Needs separate preview variables
$this->generate_build_env_variables();
if ($this->application->build_pack !== 'nixpacks') {
if ($this->application->build_pack === 'dockerfile') {
$this->add_build_env_variables_to_dockerfile();
}
$this->build_image();
@@ -863,7 +867,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_git_import_commands()
{
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands(
deployment_uuid: $this->deployment_uuid,
pull_request_id: $this->pull_request_id,
git_type: $this->git_type,
commit: $this->commit
);
return $commands;
}
@@ -929,11 +938,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_nixpacks_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->value}");
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
} else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->value}");
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
}
}
@@ -944,11 +953,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$this->env_args->put($env->key, $env->value);
$this->env_args->put($env->key, $env->real_value);
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$this->env_args->put($env->key, $env->value);
$this->env_args->put($env->key, $env->real_value);
}
}
$this->env_args->put('SOURCE_COMMIT', $this->commit);
@@ -1117,6 +1126,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -1159,22 +1173,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_environment_variables($ports)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
foreach ($this->application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
$environment_variables->push("$env->key=$env->real_value");
}
}
// Add PORT if not exists, use the first port as default
@@ -1457,12 +1468,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$value = escapeshellarg($env->value);
$value = escapeshellarg($env->real_value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$value = escapeshellarg($env->value);
$value = escapeshellarg($env->real_value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
@@ -1478,11 +1489,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -1494,13 +1505,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function next(string $status)
{
queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value) {
$this->application_deployment_queue->update([
'status' => $status,
]);
}
queue_next_deployment($this->application);
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
@@ -1512,7 +1523,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
if (str($exception->getMessage())->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
}
if ($this->application->build_pack !== 'dockercompose') {
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');

View File

@@ -1,180 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
use Throwable;
class ApplicationDeploymentNewJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public static int $batch_counter = 0;
public Server $mainServer;
public $servers;
public string $basedir;
public string $workdir;
public string $deploymentUuid;
public int $pullRequestId = 0;
// Git related
public string $gitImportCommands;
public ?string $gitType = null;
public string $gitRepository;
public string $gitBranch;
public int $gitPort;
public string $gitFullRepoUrl;
public function __construct(public ApplicationDeploymentQueue $deployment, public Application $application)
{
$this->mainServer = data_get($this->application, 'destination.server');
$this->deploymentUuid = data_get($this->deployment, 'deployment_uuid');
$this->pullRequestId = data_get($this->deployment, 'pull_request_id', 0);
$this->gitType = data_get($this->deployment, 'git_type');
$this->basedir = $this->application->generateBaseDir($this->deploymentUuid);
$this->workdir = $this->basedir . rtrim($this->application->base_directory, '/');
}
public function handle()
{
try {
ray()->clearAll();
$this->deployment->setStatus(ApplicationDeploymentStatus::IN_PROGRESS->value);
$hostIpMappings = $this->mainServer->getHostIPMappings($this->application->destination->network);
if ($this->application->dockerfile_target_build) {
$buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get the git repository and port (custom port or default port)
[
'repository' => $this->gitRepository,
'port' => $this->gitPort
] = $this->application->customRepository();
// Get the git branch and git import commands
[
'commands' => $this->gitImportCommands,
'branch' => $this->gitBranch,
'fullRepoUrl' => $this->gitFullRepoUrl
] = $this->application->generateGitImportCommands($this->deploymentUuid, $this->pullRequestId, $this->gitType);
$this->servers = $this->application->servers();
if ($this->deployment->restart_only) {
if ($this->application->build_pack === 'dockerimage') {
throw new \Exception('Restart only is not supported for docker image based deployments');
}
$this->deployment->addLogEntry("Starting deployment of {$this->application->name}.");
$this->servers->each(function ($server) {
$this->deployment->addLogEntry("Restarting {$this->application->name} on {$server->name}.");
$this->restartOnly($server);
});
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
} catch (Throwable $exception) {
$this->fail($exception);
} finally {
$this->servers->each(function ($server) {
$this->deployment->addLogEntry("Cleaning up temporary containers on {$server->name}.");
$server->executeRemoteCommand(
commands: collect([])->push([
"command" => "docker rm -f {$this->deploymentUuid}",
"hidden" => true,
"ignoreErrors" => true,
]),
loggingModel: $this->deployment
);
});
}
}
public function restartOnly(Server $server)
{
$server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deploymentUuid),
loggingModel: $this->deployment
);
$privateKey = data_get($this->application, 'private_key.private_key', null);
$gitLsRemoteCommand = collect([]);
if ($privateKey) {
$privateKey = base64_decode($privateKey);
$gitLsRemoteCommand
->push([
"command" => executeInDocker($this->deploymentUuid, "mkdir -p /root/.ssh")
])
->push([
"command" => executeInDocker($this->deploymentUuid, "echo '{$privateKey}' | base64 -d > /root/.ssh/id_rsa")
])
->push([
"command" => executeInDocker($this->deploymentUuid, "chmod 600 /root/.ssh/id_rsa")
])
->push([
"name" => "git_commit_sha",
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
"hidden" => true,
]);
} else {
$gitLsRemoteCommand->push([
"name" => "git_commit_sha",
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
"hidden" => true,
]);
}
$this->deployment->addLogEntry("Checking if there is any new commit on {$this->gitBranch} branch.");
$server->executeRemoteCommand(
commands: $gitLsRemoteCommand,
loggingModel: $this->deployment
);
$commit = str($this->deployment->getOutput('git_commit_sha'))->before("\t");
[
'productionImageName' => $productionImageName
] = $this->application->generateImageNames($commit, $this->pullRequestId);
$this->deployment->addLogEntry("Checking if the image {$productionImageName} already exists.");
$server->checkIfDockerImageExists($productionImageName, $this->deployment);
if (str($this->deployment->getOutput('local_image_found'))->isNotEmpty()) {
$this->deployment->addLogEntry("Image {$productionImageName} already exists. Skipping the build.");
$server->createWorkDirForDeployment($this->workdir, $this->deployment);
$this->application->generateDockerComposeFile($server, $this->deployment, $this->workdir);
$this->application->rollingUpdateApplication($server, $this->deployment, $this->workdir);
return;
}
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
public function failed(Throwable $exception): void
{
ray($exception);
$this->next(ApplicationDeploymentStatus::FAILED->value);
}
private function next(string $status)
{
// If the deployment is cancelled by the user, don't update the status
if ($this->deployment->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
$this->deployment->update([
'status' => $status,
]);
}
queue_next_deployment($this->application, isNew: true);
}
}

View File

@@ -17,38 +17,34 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $build_logs_url;
public Application $application;
public ApplicationPreview $preview;
public string $body;
public function __construct(
public string $application_id,
public int $pull_request_id,
public string $deployment_uuid,
public string $status
public Application $application,
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
) {
}
public function handle()
{
try {
$this->application = Application::findOrFail($this->application_id);
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
$this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
if ($this->status === ProcessStatus::IN_PROGRESS->value) {
if ($this->status === ProcessStatus::CLOSED) {
$this->delete_comment();
return;
} else if ($this->status === ProcessStatus::IN_PROGRESS) {
$this->body = "The preview deployment is in progress. 🟡\n\n";
}
if ($this->status === ProcessStatus::FINISHED->value) {
} else if ($this->status === ProcessStatus::FINISHED) {
$this->body = "The preview deployment is ready. 🟢\n\n";
if ($this->preview->fqdn) {
$this->body .= "[Open Preview]({$this->preview->fqdn}) | ";
}
}
if ($this->status === ProcessStatus::ERROR->value) {
} else if ($this->status === ProcessStatus::ERROR) {
$this->body = "The preview deployment failed. 🔴\n\n";
}
$this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->body .= "[Open Build Logs](" . $this->build_logs_url . ")\n\n\n";
$this->body .= "Last updated at: " . now()->toDateTimeString() . " CET";
@@ -77,10 +73,14 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
private function create_comment()
{
['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->pull_request_id}/comments", method: 'post', data: [
['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->preview->pull_request_id}/comments", method: 'post', data: [
'body' => $this->body,
]);
$this->preview->pull_request_issue_comment_id = $data['id'];
$this->preview->save();
}
private function delete_comment()
{
githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'delete');
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneMariadb;
@@ -30,41 +31,22 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
try {
$server = $this->resource->destination->server;
$this->resource->delete();
if (!$server->isFunctional()) {
if ($this->resource->type() === 'service') {
ray('dispatching delete service');
DeleteService::dispatch($this->resource);
} else {
$this->resource->forceDelete();
}
return 'Server is not functional';
}
$this->resource->forceDelete();
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
break;
case 'standalone-postgresql':
StopDatabase::run($this->resource);
break;
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'standalone-mongodb':
StopDatabase::run($this->resource);
break;
case 'standalone-mysql':
StopDatabase::run($this->resource);
break;
case 'standalone-mariadb':
StopDatabase::run($this->resource);
break;
}
if ($this->resource->type() === 'service') {
DeleteService::dispatch($this->resource);
} else {
$this->resource->forceDelete();
case 'service':
StopService::run($this->resource);
DeleteService::run($this->resource);
break;
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@@ -38,6 +38,8 @@ class ScheduledTaskJob implements ShouldQueue
$this->resource = $service;
} else if ($application = $task->application()->first()) {
$this->resource = $application;
} else {
throw new \Exception('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
}

View File

@@ -2,18 +2,35 @@
namespace App\Livewire;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class Dashboard extends Component
{
public $projects = [];
public $servers = [];
public Collection $servers;
public $deployments_per_server;
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
$this->get_deployments();
}
public function get_deployments()
{
$this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([
"id",
"application_id",
"application_name",
"deployment_url",
"pull_request_id",
"server_name",
"server_id",
"status"
])->sortBy('id')->groupBy('server_name')->toArray();
}
// public function getIptables()
// {

View File

@@ -15,6 +15,7 @@ class Index extends Component
public int $skip = 0;
public int $default_take = 40;
public bool $show_next = false;
public bool $show_prev = false;
public ?string $pull_request_id = null;
protected $queryString = ['pull_request_id'];
public function mount()
@@ -60,15 +61,30 @@ class Index extends Component
{
$this->load_deployments();
}
public function previous_page(?int $take = null)
{
public function load_deployments(int|null $take = null)
if ($take) {
$this->skip = $this->skip - $take;
}
$this->skip = $this->skip - $this->default_take;
if ($this->skip < 0) {
$this->show_prev = false;
$this->skip = 0;
}
$this->load_deployments();
}
public function next_page(?int $take = null)
{
if ($take) {
$this->skip = $this->skip + $take;
}
$take = $this->default_take;
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take);
$this->show_prev = true;
$this->load_deployments();
}
public function load_deployments()
{
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take);
$this->deployments = $deployments;
$this->deployments_count = $count;
$this->show_pull_request_only();

View File

@@ -41,13 +41,10 @@ class DeploymentNavbar extends Component
public function cancel()
{
try {
$kill_command = "kill -9 {$this->application_deployment_queue->current_process_id}";
if ($this->application_deployment_queue->current_process_id) {
$process = Process::run("ps -p {$this->application_deployment_queue->current_process_id} -o command --no-headers");
if (Str::of($process->output())->contains([$this->server->ip, 'EOF-COOLIFY-SSH'])) {
Process::run($kill_command);
}
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
if ($this->application_deployment_queue->logs) {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$new_log_entry = [
'command' => $kill_command,
'output' => "Deployment cancelled by user.",
@@ -60,15 +57,17 @@ class DeploymentNavbar extends Component
$this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
]);
instant_remote_process([$kill_command], $this->server);
}
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
queue_next_deployment($this->application);
// queue_next_deployment($this->application);
}
}
}

View File

@@ -61,11 +61,12 @@ class General extends Component
'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
'application.custom_labels' => 'nullable',
'application.custom_docker_run_options' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
];
@@ -97,9 +98,10 @@ class General extends Component
'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_static' => 'Is static',
'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
];
@@ -243,12 +245,16 @@ class General extends Component
$domains = $domains->unique();
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS settings for: $domain failed.<br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
}
}
check_fqdn_usage($this->application);
$this->application->fqdn = $domains->implode(',');
}
if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
}
if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port && !$this->application->ports_exposes) {

View File

@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob;
use App\Models\Application;
@@ -55,7 +54,7 @@ class Heading extends Component
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
@@ -83,7 +82,7 @@ class Heading extends Component
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
@@ -112,7 +111,7 @@ class Heading extends Component
{
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
is_new_deployment: true,
@@ -128,7 +127,7 @@ class Heading extends Component
{
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);

View File

@@ -47,7 +47,7 @@ class Previews extends Component
]);
}
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: false,
pull_request_id: $pull_request_id,

View File

@@ -24,7 +24,7 @@ class Rollback extends Component
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application_id: $this->application->id,
application: $this->application,
deployment_uuid: $deployment_uuid,
commit: $commit,
force_rebuild: false,

View File

@@ -12,7 +12,24 @@ class Edit extends Component
'project.name' => 'required|min:3|max:255',
'project.description' => 'nullable|string|max:255',
];
public function mount() {
protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
public function saveKey($data)
{
try {
$this->project->environment_variables()->create([
'key' => $data['key'],
'value' => $data['value'],
'type' => 'project',
'team_id' => currentTeam()->id,
]);
$this->project->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$projectUuid = request()->route('project_uuid');
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();

View File

@@ -12,14 +12,30 @@ class EnvironmentEdit extends Component
public Application $application;
public $environment;
public array $parameters;
protected $rules = [
'environment.name' => 'required|min:3|max:255',
'environment.description' => 'nullable|min:3|max:255',
];
public function mount() {
$this->parameters = get_route_parameters();
protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
public function saveKey($data)
{
try {
$this->environment->environment_variables()->create([
'key' => $data['key'],
'value' => $data['value'],
'type' => 'environment',
'team_id' => currentTeam()->id,
]);
$this->environment->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first();
$this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first();
}

View File

@@ -39,6 +39,8 @@ class GithubPrivateRepository extends Component
public bool $is_static = false;
public string|null $publish_directory = null;
protected int $page = 1;
public $build_pack = 'nixpacks';
public bool $show_is_static = true;
public function mount()
@@ -49,6 +51,20 @@ class GithubPrivateRepository extends Component
$this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private();
}
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
$this->show_is_static = true;
$this->port = 3000;
} else if ($this->build_pack === 'static') {
$this->show_is_static = false;
$this->is_static = false;
$this->port = 80;
} else {
$this->show_is_static = false;
$this->is_static = false;
}
}
public function loadRepositories($github_app_id)
{
$this->repositories = collect();
@@ -95,7 +111,7 @@ class GithubPrivateRepository extends Component
$this->loadBranchByPage();
}
}
$this->selected_branch_name = data_get($this->branches,'0.name');
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
}
protected function loadBranchByPage()

View File

@@ -29,12 +29,17 @@ class GithubPrivateRepositoryDeployKey extends Component
public string $repository_url;
public string $branch;
public $build_pack = 'nixpacks';
public bool $show_is_static = true;
protected $rules = [
'repository_url' => 'required',
'branch' => 'required|string',
'port' => 'required|numeric',
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
];
protected $validationAttributes = [
'repository_url' => 'Repository',
@@ -42,6 +47,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'port' => 'Port',
'is_static' => 'Is static',
'publish_directory' => 'Publish directory',
'build_pack' => 'Build pack',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
@@ -62,6 +68,20 @@ class GithubPrivateRepositoryDeployKey extends Component
}
}
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
$this->show_is_static = true;
$this->port = 3000;
} else if ($this->build_pack === 'static') {
$this->show_is_static = false;
$this->is_static = false;
$this->port = 80;
} else {
$this->show_is_static = false;
$this->is_static = false;
}
}
public function instantSave()
{
if ($this->is_static) {

View File

@@ -30,7 +30,7 @@ class PublicGitRepository extends Component
public GithubApp|GitlabApp|string $git_source = 'other';
public string $git_host;
public string $git_repository;
public $build_pack;
public $build_pack = 'nixpacks';
public bool $show_is_static = true;
protected $rules = [
@@ -61,9 +61,11 @@ class PublicGitRepository extends Component
{
if ($this->build_pack === 'nixpacks') {
$this->show_is_static = true;
$this->port = 3000;
} else if ($this->build_pack === 'static') {
$this->show_is_static = false;
$this->is_static = false;
$this->port = 80;
} else {
$this->show_is_static = false;
$this->is_static = false;

View File

@@ -53,6 +53,7 @@ class Application extends Component
public function submit()
{
try {
check_fqdn_usage($this->application);
$this->validate();
$this->application->save();
updateCompose($this->application);

View File

@@ -30,7 +30,10 @@ class Configuration extends Component
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->first();
if (!$this->service) {
return redirect()->route('dashboard');
}
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}

View File

@@ -24,7 +24,8 @@ class Danger extends Component
public function delete()
{
try {
DeleteResourceJob::dispatchSync($this->resource);
$this->resource->delete();
DeleteResourceJob::dispatch($this->resource);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName

View File

@@ -32,7 +32,6 @@ class Add extends Component
public function submit()
{
$this->validate();
ray($this->key, $this->value, $this->is_build_time);
$this->dispatch('saveKey', [
'key' => $this->key,
'value' => $this->value,

View File

@@ -3,16 +3,18 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
public $parameters;
public ModelsEnvironmentVariable $env;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
public ?string $modalId = null;
public bool $isDisabled = false;
public bool $isLocked = false;
public bool $isSharedVariable = false;
public string $type;
protected $rules = [
@@ -20,16 +22,20 @@ class Show extends Component
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
];
protected $validationAttributes = [
'key' => 'Key',
'value' => 'Value',
'is_build_time' => 'Build Time',
'is_shown_once' => 'Shown Once',
'env.key' => 'Key',
'env.value' => 'Value',
'env.is_build_time' => 'Build Time',
'env.is_shown_once' => 'Shown Once',
];
public function mount()
{
if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
$this->isSharedVariable = true;
}
$this->modalId = new Cuid2(7);
$this->parameters = get_route_parameters();
$this->checkEnvs();
@@ -44,9 +50,16 @@ class Show extends Component
$this->isLocked = true;
}
}
public function serialize() {
data_forget($this->env, 'real_value');
if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
data_forget($this->env, 'is_build_time');
}
}
public function lock()
{
$this->env->is_shown_once = true;
$this->serialize();
$this->env->save();
$this->checkEnvs();
$this->dispatch('refreshEnvs');
@@ -57,10 +70,23 @@ class Show extends Component
}
public function submit()
{
$this->validate();
$this->env->save();
$this->dispatch('success', 'Environment variable updated successfully.');
$this->dispatch('refreshEnvs');
try {
if ($this->isSharedVariable) {
$this->validate([
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_shown_once' => 'required|boolean',
]);
} else {
$this->validate();
}
$this->serialize();
$this->env->save();
$this->dispatch('success', 'Environment variable updated successfully.');
$this->dispatch('refreshEnvs');
} catch(\Exception $e) {
return handleError($e);
}
}
public function delete()

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Environment;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class ResourceOperations extends Component
{
public $resource;
public $projectUuid;
public $environmentName;
public $projects;
public $servers;
public function mount()
{
$parameters = get_route_parameters();
$this->projectUuid = $parameters['project_uuid'];
$this->environmentName = $parameters['environment_name'];
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = currentTeam()->servers;
}
public function cloneTo($destination_id)
{
$new_destination = StandaloneDocker::find($destination_id);
if (!$new_destination) {
$new_destination = SwarmDocker::find($destination_id);
}
if (!$new_destination) {
return $this->addError('destination_id', 'Destination not found.');
}
$uuid = (string)new Cuid2(7);
$server = $new_destination->server;
if ($this->resource->getMorphClass() === 'App\Models\Application') {
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name . '-clone-' . $uuid,
'fqdn' => generateFqdn($server, $uuid),
'status' => 'exited',
'destination_id' => $new_destination->id,
]);
$new_resource->save();
if ($new_resource->destination->server->proxyType() === 'TRAEFIK_V2') {
$customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n");
$new_resource->custom_labels = base64_encode($customLabels);
$new_resource->save();
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $new_resource->id,
]);
$newEnvironmentVariable->save();
}
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $new_resource->uuid . '-' . str($volume->name)->afterLast('-'),
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
}
$route = route('project.application.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'application_uuid' => $new_resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
} else if (
$this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMysql' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneRedis'
) {
$uuid = (string)new Cuid2(7);
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name . '-clone-' . $uuid,
'status' => 'exited',
'started_at' => null,
'destination_id' => $new_destination->id,
]);
$new_resource->save();
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($this->resource->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $new_resource->id;
} else if ($this->resource->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $new_resource->id;
} else if ($this->resource->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $new_resource->id;
} else if ($this->resource->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $new_resource->id;
} else if ($this->resource->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $new_resource->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save();
}
$route = route('project.database.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'database_uuid' => $new_resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
} else if ($this->resource->type() === 'service') {
$uuid = (string)new Cuid2(7);
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name . '-clone-' . $uuid,
'destination_id' => $new_destination->id,
]);
$new_resource->save();
foreach ($new_resource->applications() as $application) {
$application->update([
'status' => 'exited',
]);
}
foreach ($new_resource->databases() as $database) {
$database->update([
'status' => 'exited',
]);
}
$new_resource->parse();
$route = route('project.service.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'service_uuid' => $new_resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
}
return;
}
public function moveTo($environment_id)
{
try {
$new_environment = Environment::findOrFail($environment_id);
$this->resource->update([
'environment_id' => $environment_id
]);
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'application_uuid' => $this->resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
} else if (str($this->resource->type())->startsWith('standalone-')) {
$route = route('project.database.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'database_uuid' => $this->resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
} else if ($this->resource->type() === 'service') {
$route = route('project.service.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'service_uuid' => $this->resource->uuid,
]) . "#resource-operations";
return redirect()->to($route);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.resource-operations');
}
}

View File

@@ -10,9 +10,11 @@ class Webhooks extends Component
public ?string $deploywebhook = null;
public ?string $githubManualWebhook = null;
public ?string $gitlabManualWebhook = null;
public ?string $bitbucketManualWebhook = null;
protected $rules = [
'resource.manual_webhook_secret_github' => 'nullable|string',
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
'resource.manual_webhook_secret_bitbucket' => 'nullable|string',
];
public function saveSecret()
{
@@ -29,6 +31,7 @@ class Webhooks extends Component
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
}
public function render()
{

View File

@@ -27,6 +27,7 @@ class Form extends Component
'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class Form extends Component
'server.settings.is_swarm_manager' => 'Swarm Manager',
'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server',
'server.settings.concurrent_builds' => 'Concurrent Builds',
];
public function mount()

View File

@@ -11,6 +11,7 @@ class Show extends Component
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated' => '$refresh'];
public function mount()
{
$this->parameters = get_route_parameters();
@@ -19,14 +20,13 @@ class Show extends Component
if (is_null($this->server)) {
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
$this->dispatch('serverRefresh',false);
$this->dispatch('serverRefresh', false);
}
public function render()
{

View File

@@ -17,7 +17,7 @@ class Change extends Component
public ?bool $preview_deployment_permissions = true;
public $parameters;
public GithubApp $github_app;
public ?GithubApp $github_app;
public string $name;
public bool $is_system_wide;

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Livewire;
use App\Models\Team;
use Livewire\Component;
class TeamSharedVariablesIndex extends Component
{
public Team $team;
protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
public function saveKey($data)
{
try {
$this->team->environment_variables()->create([
'key' => $data['key'],
'value' => $data['value'],
'type' => 'team',
'team_id' => currentTeam()->id,
]);
$this->team->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->team = currentTeam();
}
public function render()
{
return view('livewire.team-shared-variables-index');
}
}

View File

@@ -6,11 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel
@@ -39,6 +37,7 @@ class Application extends BaseModel
]);
});
static::deleting(function ($application) {
$application->update(['fqdn' => null]);
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
$server = data_get($application, 'destination.server');
@@ -52,65 +51,14 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete();
});
}
// Build packs / deployment types
public function servers(): Collection
public function is_github_based(): bool
{
$mainServer = data_get($this, 'destination.server');
$additionalDestinations = data_get($this, 'additional_destinations', null);
$additionalServers = collect([]);
if ($this->isMultipleServerDeployment()) {
ray('asd');
if (str($additionalDestinations)->isNotEmpty()) {
$additionalDestinations = str($additionalDestinations)->explode(',');
foreach ($additionalDestinations as $destinationId) {
$destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first();
$server = data_get($destination, 'server');
$additionalServers->push($server);
}
}
if (data_get($this, 'source')) {
return true;
}
return collect([$mainServer])->merge($additionalServers);
return false;
}
public function generateImageNames(string $commit, int $pullRequestId)
{
if ($this->dockerfile) {
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:latest");
} else {
$buildImageName = Str::lower("{$this->uuid}:build");
$productionImageName = Str::lower("{$this->uuid}:latest");
}
} else if ($this->build_pack === 'dockerimage') {
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$this->docker_registry_image_tag}");
} else if ($pullRequestId === 0) {
$dockerImageTag = str($commit)->substr(0, 128);
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}-build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}");
} else {
$buildImageName = Str::lower("{$this->uuid}:{$dockerImageTag}-build");
$productionImageName = Str::lower("{$this->uuid}:{$dockerImageTag}");
}
} else if ($pullRequestId !== 0) {
if ($this->docker_registry_image_name) {
$buildImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}-build");
$productionImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}");
} else {
$buildImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}-build");
$productionImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}");
}
}
return [
'buildImageName' => $buildImageName,
'productionImageName' => $productionImageName,
];
}
// End of build packs / deployment types
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -263,6 +211,10 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes)
);
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
@@ -431,7 +383,7 @@ class Application extends BaseModel
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables->all());
$newConfigHash .= json_encode($this->environment_variables());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
@@ -454,31 +406,6 @@ class Application extends BaseModel
return true;
}
}
public function isMultipleServerDeployment()
{
return false;
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
return false;
}
public function healthCheckUrl()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
return null;
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
} else {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
}
return $full_healthcheck_url;
}
function customRepository()
{
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
@@ -500,296 +427,7 @@ class Application extends BaseModel
{
return "/artifacts/{$uuid}";
}
function generateHealthCheckCommands()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null"
];
} else {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"
];
}
return implode(' ', $generated_healthchecks_commands);
}
function generateLocalPersistentVolumes(int $pullRequestId)
{
$persistentStorages = [];
$volumeNames = [];
foreach ($this->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$volumeNames[$name] = [
'name' => $name,
'external' => false,
];
}
return [
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames,
];
}
public function generateEnvironmentVariables($ports)
{
$environmentVariables = collect();
// ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) {
// ray($this->runtime_environment_variables)->green();
foreach ($this->runtime_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
} else {
// ray($this->runtime_environment_variables_preview)->green();
foreach ($this->runtime_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environmentVariables->push("PORT={$ports[0]}");
}
return $environmentVariables->all();
}
function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$ports = $this->settings->is_static ? [80] : $this->ports_exposes_array;
$container_name = generateApplicationContainerName($this, $this->pull_request_id);
$commit = str($deployment->getOutput('git_commit_sha'))->before("\t");
[
'productionImageName' => $productionImageName
] = $this->generateImageNames($commit, $pullRequestId);
[
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames
] = $this->generateLocalPersistentVolumes($pullRequestId);
$environmentVariables = $this->generateEnvironmentVariables($ports);
if (data_get($this, 'custom_labels')) {
$labels = collect(str($this->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
});
$this->custom_labels = $labels->implode(',');
$this->save();
} else {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
$labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $productionImageName,
'container_name' => $container_name,
'restart' => RESTART_MODE,
'environment' => $environmentVariables,
'expose' => $ports,
'networks' => [
$this->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
$this->generateHealthCheckCommands()
],
'interval' => $this->health_check_interval . 's',
'timeout' => $this->health_check_timeout . 's',
'retries' => $this->health_check_retries,
'start_period' => $this->health_check_start_period . 's'
],
'mem_limit' => $this->limits_memory,
'memswap_limit' => $this->limits_memory_swap,
'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (float) $this->limits_cpus,
'cpu_shares' => $this->limits_cpu_shares,
]
],
'networks' => [
$this->destination->network => [
'external' => true,
'name' => $this->destination->network,
'attachable' => true
]
]
];
if (!is_null($this->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->limits_cpuset);
}
if ($server->isSwarm()) {
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
data_forget($docker_compose, 'services.' . $container_name . '.expose');
data_forget($docker_compose, 'services.' . $container_name . '.restart');
data_forget($docker_compose, 'services.' . $container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $container_name . '.cpus');
data_forget($docker_compose, 'services.' . $container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares');
$docker_compose['services'][$container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels,
'resources' => [
'limits' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
],
'reservations' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
]
]
];
} else {
$docker_compose['services'][$container_name]['labels'] = $labels;
}
if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($this->settings->is_gpu_enabled) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this, 'settings.gpu_options', [])
]
];
if (data_get($this, 'settings.gpu_count')) {
$count = data_get($this, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this, 'settings.gpu_device_ids')) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids');
}
}
if ($this->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $container_name . '.healthcheck');
}
if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array;
}
if (count($persistentStorages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistentStorages;
}
if (count($volumeNames) > 0) {
$docker_compose['volumes'] = $volumeNames;
}
$docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name];
data_forget($docker_compose, 'services.' . $container_name);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
}
function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$containerName = generateApplicationContainerName($this, $pullRequestId);
// if (count($this->ports_mappings_array) > 0) {
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
// if ($pullRequestId === 0) {
// $containers = $containers->filter(function ($container) use ($containerName) {
// return data_get($container, 'Names') !== $containerName;
// });
// }
$containers->each(function ($container) use ($server, $deployment) {
$removingContainerName = data_get($container, 'Names');
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => "docker rm -f $removingContainerName",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
});
// }
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
$deployment->addLogEntry("New container started.");
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') {
@@ -803,7 +441,7 @@ class Application extends BaseModel
}
return $git_clone_command;
}
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
@@ -816,7 +454,6 @@ class Application extends BaseModel
if ($pull_request_id !== 0) {
$pr_branch_name = "pr-{$pull_request_id}-coolify";
}
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
@@ -895,8 +532,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
if ($git_type === 'github') {
} else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@@ -904,6 +540,13 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
} else if ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
}
}
@@ -922,6 +565,34 @@ class Application extends BaseModel
$fullRepoUrl = $customRepository;
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
} else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
} else if ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
@@ -934,34 +605,6 @@ class Application extends BaseModel
];
}
}
public function prepareHelperImage(string $deploymentUuid)
{
$basedir = $this->generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$server = data_get($this, 'destination.server');
$network = data_get($this, 'destination.network');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function parseCompose(int $pull_request_id = 0)
{
if ($this->docker_compose_raw) {
@@ -1002,7 +645,7 @@ class Application extends BaseModel
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
throw new \RuntimeException("Could not load base compose file from $workdir$composeFile");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
@@ -1072,4 +715,12 @@ class Application extends BaseModel
$this->save();
return $customLabels;
}
public function fqdns(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
}
}

View File

@@ -17,6 +17,9 @@ class Environment extends Model
$this->services()->count() == 0;
}
public function environment_variables() {
return $this->hasMany(SharedEnvironmentVariable::class);
}
public function applications()
{
return $this->hasMany(Application::class);

View File

@@ -15,6 +15,7 @@ class EnvironmentVariable extends Model
'value' => 'encrypted',
'is_build_time' => 'boolean',
];
protected $appends = ['real_value', 'is_shared'];
protected static function booted()
{
@@ -48,24 +49,78 @@ class EnvironmentVariable extends Model
set: fn (?string $value = null) => $this->set_environment_variables($value),
);
}
private function get_environment_variables(?string $environment_variable = null): string|null
public function realValue(): Attribute
{
$resource = null;
if ($this->application_id) {
$resource = Application::find($this->application_id);
} else if ($this->service_id) {
$resource = Service::find($this->service_id);
} else if ($this->database_id) {
$resource = StandalonePostgresql::find($this->database_id);
if (!$resource) {
$resource = StandaloneMysql::find($this->database_id);
if (!$resource) {
$resource = StandaloneRedis::find($this->database_id);
if (!$resource) {
$resource = StandaloneMongodb::find($this->database_id);
if (!$resource) {
$resource = StandaloneMariadb::find($this->database_id);
}
}
}
}
}
return Attribute::make(
get: function () use ($resource) {
return $this->get_real_environment_variables($this->value, $resource);
}
);
}
protected function isShared(): Attribute
{
return Attribute::make(
get: function () {
$type = str($this->value)->after("{{")->before(".")->value;
if (str($this->value)->startsWith('{{' . $type) && str($this->value)->endsWith('}}')) {
return true;
}
return false;
}
);
}
private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null
{
// $team_id = currentTeam()->id;
if (!$environment_variable) {
return null;
}
$environment_variable = trim(decrypt($environment_variable));
if (Str::startsWith($environment_variable, '{{') && Str::endsWith($environment_variable, '}}') && Str::contains($environment_variable, 'global.')) {
$variable = Str::after($environment_variable, 'global.');
$environment_variable = trim($environment_variable);
$type = str($environment_variable)->after("{{")->before(".")->value;
if (str($environment_variable)->startsWith("{{" . $type) && str($environment_variable)->endsWith('}}')) {
$variable = Str::after($environment_variable, "{$type}.");
$variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value;
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
ray('global env variable');
return $environment_variable;
if ($type === 'environment') {
$id = $resource->environment->id;
} else if ($type === 'project') {
$id = $resource->environment->project->id;
} else {
$id = $resource->team()->id;
}
$environment_variable_found = SharedEnvironmentVariable::where("type", $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first();
if ($environment_variable_found) {
return $environment_variable_found->value;
}
}
return $environment_variable;
}
private function get_environment_variables(?string $environment_variable = null): string|null
{
if (!$environment_variable) {
return null;
}
return trim(decrypt($environment_variable));
}
private function set_environment_variables(?string $environment_variable = null): string|null
{
@@ -73,6 +128,10 @@ class EnvironmentVariable extends Model
return null;
}
$environment_variable = trim($environment_variable);
$type = str($environment_variable)->after("{{")->before(".")->value;
if (str($environment_variable)->startsWith("{{" . $type) && str($environment_variable)->endsWith('}}')) {
return encrypt((string) str($environment_variable)->replace(' ', ''));
}
return encrypt($environment_variable);
}

View File

@@ -27,7 +27,9 @@ class Project extends BaseModel
$project->settings()->delete();
});
}
public function environment_variables() {
return $this->hasMany(SharedEnvironmentVariable::class);
}
public function environments()
{
return $this->hasMany(Environment::class);

View File

@@ -2,16 +2,12 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -335,20 +331,6 @@ class Server extends BaseModel
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
return false;
}
// foreach ($this->applications() as $application) {
// if (data_get($application, 'fqdn')) {
// $shouldRun = true;
// break;
// }
// }
// ray($this->services()->get());
// if ($this->id === 0) {
// $settings = InstanceSettings::get();
// if (data_get($settings, 'fqdn')) {
// $shouldRun = true;
// }
// }
return true;
}
public function isFunctional()
@@ -398,6 +380,8 @@ class Server extends BaseModel
}
public function validateConnection()
{
config()->set('coolify.mux_enabled', false);
$server = Server::find($this->id);
if (!$server) {
return false;
@@ -481,153 +465,4 @@ class Server extends BaseModel
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
}
}
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{
static::$batch_counter++;
foreach ($commands as $command) {
$realCommand = data_get($command, 'command');
if (is_null($realCommand)) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($command, 'hidden', false);
$ignoreErrors = data_get($command, 'ignoreErrors', false);
$customOutputType = data_get($command, 'customOutputType');
$name = data_get($command, 'name');
$remoteCommand = generateSshCommand($this, $realCommand);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($realCommand),
'output' => remove_iip($output),
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if ($loggingModel) {
if (!$loggingModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
if ($name) {
$newLogEntry['name'] = $name;
}
$previousLogs[] = $newLogEntry;
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$loggingModel->save();
}
});
if ($loggingModel) {
$loggingModel->update([
'current_process_id' => $process->id(),
]);
}
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
if ($loggingModel) {
$status = ApplicationDeploymentStatus::FAILED->value;
$loggingModel->status = $status;
$loggingModel->save();
}
throw new \RuntimeException($processResult->errorOutput());
}
}
}
}
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
{
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
$containers = $containers->filter(function ($container) use ($containerName) {
return data_get($container, 'Names') !== $containerName;
});
$containers->each(function ($container) {
$removableContainer = data_get($container, 'Names');
$this->server->executeRemoteCommand(
commands: collect([
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $this->deploymentQueueEntry
);
});
}
public function getHostIPMappings($network)
{
$addHosts = null;
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $this);
if (!is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
return $addHosts;
}
public function checkIfDockerImageExists(string $imageName, ApplicationDeploymentQueue $deployment)
{
$this->executeRemoteCommand(
commands: collect([
[
"name" => "local_image_found",
"command" => "docker images -q {$imageName} 2>/dev/null",
"hidden" => true,
]
]),
loggingModel: $deployment
);
if (str($deployment->getOutput('local_image_found'))->isEmpty()) {
$this->executeRemoteCommand(
commands: collect([
[
"command" => "docker pull {$imageName} 2>/dev/null",
"ignoreErrors" => true,
"hidden" => true
],
[
"name" => "local_image_found",
"command" => "docker images -q {$imageName} 2>/dev/null",
"hidden" => true,
]
]),
loggingModel: $deployment
);
}
}
public function createWorkDirForDeployment(string $workdir, ApplicationDeploymentQueue $deployment)
{
$this->executeRemoteCommand(
commands: collect([
[
"command" => executeInDocker($deployment->deployment_uuid, "mkdir -p {$workdir}"),
"ignoreErrors" => true,
"hidden" => true
],
]),
loggingModel: $deployment
);
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Models;
use App\Actions\Service\DeleteService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -17,6 +16,10 @@ class Service extends BaseModel
{
return 'service';
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function extraFields()
{
$fields = collect([]);
@@ -423,7 +426,7 @@ class Service extends BaseModel
$envs = $this->environment_variables()->get();
$commands[] = "rm -f .env || true";
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
}
if ($envs->count() === 0) {
$commands[] = "touch .env";

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
class ServiceApplication extends BaseModel
{
@@ -15,6 +14,7 @@ class ServiceApplication extends BaseModel
protected static function booted()
{
static::deleting(function ($service) {
$service->update(['fqdn' => null]);
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
@@ -55,7 +55,6 @@ class ServiceApplication extends BaseModel
get: fn () => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
}
public function getFilesFromServer(bool $isInit = false)

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SharedEnvironmentVariable extends Model
{
protected $guarded = [];
protected $casts = [
'key' => 'string',
'value' => 'encrypted',
];
}

View File

@@ -42,6 +42,10 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete();
});
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {

View File

@@ -45,6 +45,10 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete();
});
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

@@ -42,6 +42,10 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete();
});
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {

View File

@@ -74,7 +74,10 @@ class StandalonePostgresql extends BaseModel
);
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function type(): string
{
return 'standalone-postgresql';

View File

@@ -37,6 +37,10 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete();
});
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {

View File

@@ -70,7 +70,9 @@ class Team extends Model implements SendsDiscord, SendsEmail
);
}
public function environment_variables() {
return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id');
}
public function members()
{
return $this->belongsToMany(User::class, 'team_user', 'team_id', 'user_id')->withPivot('role');

View File

@@ -38,7 +38,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array

View File

@@ -38,7 +38,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array

View File

@@ -31,7 +31,7 @@ class StatusChanged extends Notification implements ShouldQueue
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
}
$this->resource_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->resource->uuid}";
$this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}";
}
public function via(object $notifiable): array

View File

@@ -1,77 +0,0 @@
<?php
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
trait ExecuteRemoteCommandNew
{
public static $batch_counter = 0;
public function executeRemoteCommand(Server $server, $logModel, $commands)
{
static::$batch_counter++;
if ($commands instanceof Collection) {
$commandsText = $commands;
} else {
$commandsText = collect($commands);
}
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($singleCommand, 'hidden', false);
$customType = data_get($singleCommand, 'type');
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
$save = data_get($singleCommand, 'save');
$remote_command = generateSshCommand($server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$logModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
$previousLogs[] = $newLogEntry;
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$logModel->save();
if ($save) {
$this->remoteCommandOutputs[$save] = str($output)->trim();
}
});
$logModel->update([
'current_process_id' => $process->id(),
]);
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$logModel->status = $status;
$logModel->save();
throw new \RuntimeException($processResult->errorOutput());
}
}
});
}
}

View File

@@ -19,7 +19,7 @@ class Select extends Component
public string|null $label = null,
public string|null $helper = null,
public bool $required = false,
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
public string $defaultClass = "select select-sm w-full rounded text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
) {
//
}

View File

@@ -1,22 +1,25 @@
<?php
use App\Jobs\ApplicationDeployDockerImageJob;
use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploymentNewJob;
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
use App\Jobs\ApplicationRestartJob;
use App\Jobs\MultipleApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
use Spatie\Url\Url;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $is_new_deployment = false)
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{
$application_id = $application->id;
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
$deployment_url = $deployment_link->getPath();
$server_id = $application->destination->server->id;
$server_name = $application->destination->server->name;
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'application_name' => $application->name,
'server_id' => $server_id,
'server_name' => $server_name,
'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook,
@@ -24,305 +27,42 @@ function queue_application_deployment(int $application_id, string $deployment_uu
'commit' => $commit,
'git_type' => $git_type
]);
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');
$running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at');
ray('Q:' . $queued_deployments->count() . 'R:' . $running_deployments->count() . '| Queuing deployment: ' . $deployment_uuid . ' of applicationID: ' . $application_id . ' pull request: ' . $pull_request_id . ' with commit: ' . $commit . ' and is it forced: ' . $force_rebuild);
if ($queued_deployments->count() > 1) {
$queued_deployments = $queued_deployments->skip(1);
$queued_deployments->each(function ($queued_deployment, $key) {
$queued_deployment->status = 'cancelled by system';
$queued_deployment->save();
});
}
if ($running_deployments->count() > 0) {
return;
}
if ($is_new_deployment) {
dispatch(new ApplicationDeploymentNewJob(
deployment: $deployment,
application: Application::find($application_id)
));
} else {
if (next_queuable($server_id, $application_id)) {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
}
function queue_next_deployment(Application $application, bool $isNew = false)
function queue_next_deployment(Application $application)
{
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
$server_id = $application->destination->server_id;
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
if ($next_found) {
if ($isNew) {
dispatch(new ApplicationDeploymentNewJob(
deployment: $next_found,
application: $application
));
} else {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
));
}
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
));
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
function next_queuable(string $server_id, string $application_id): bool
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
$deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at');
$same_application_deployments = $deployments->where('application_id', $application_id);
$in_progress = $same_application_deployments->filter(function ($value, $key) {
return $value->status === 'in_progress';
});
if ($in_progress->count() > 0) {
return false;
}
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
if ($deployments->count() > $concurrent_builds) {
return false;
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$workDir = generateWorkdir($deploymentUuid, $application);
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
if (data_get($application, 'custom_labels')) {
$labels = collect(str($application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !str($value)->startsWith('coolify.');
});
$application->custom_labels = $labels->implode(',');
$application->save();
} else {
$labels = collect(generateLabelsApplication($application, $preview));
}
if ($pullRequestId !== 0) {
$labels = collect(generateLabelsApplication($application, $preview));
}
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => $imageName,
'container_name' => $containerName,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$network,
],
'mem_limit' => $application->limits_memory,
'memswap_limit' => $application->limits_memory_swap,
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpu_shares' => $application->limits_cpu_shares,
]
],
'networks' => [
$network => [
'external' => true,
'name' => $network,
'attachable' => true
]
]
];
if (!is_null($application->limits_cpuset)) {
data_set($docker_compose, "services.{$containerName}.cpuset", $application->limits_cpuset);
}
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($application->settings->is_gpu_enabled) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($application, 'settings.gpu_options', [])
]
];
if (data_get($application, 'settings.gpu_count')) {
$count = data_get($application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
}
}
if ($application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
}
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$commands = collect([]);
$commands->push([
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
"hidden" => true,
]);
return $commands;
}
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes = [];
foreach ($application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes_names = [];
foreach ($application->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($pullRequestId === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
return $environment_variables->all();
}
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if ($application->build_pack === 'dockerimage') {
$loggingModel->addLogEntry('Pulling latest images from the registry.');
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
"hidden" => true
],
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
} else {
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
}
return $commands;
}
function removeOldDeployment(string $containerName)
{
$commands = collect([]);
$commands->push(
["docker rm -f $containerName >/dev/null 2>&1"],
);
return $commands;
return true;
}

View File

@@ -3,9 +3,7 @@
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
@@ -323,3 +321,89 @@ function isDatabaseImage(?string $image = null)
}
return false;
}
function convert_docker_run_to_compose(?string $custom_docker_run_options = null)
{
$options = [];
$compose_options = collect([]);
preg_match_all('/(--\w+(?:-\w+)*)(?:\s|=)?([^\s-]+)?/', $custom_docker_run_options, $matches, PREG_SET_ORDER);
$list_options = collect([
'--cap-add',
'--cap-drop',
'--security-opt',
'--sysctl',
'--ulimit',
'--device'
]);
$mapping = collect([
'--cap-add' => 'cap_add',
'--cap-drop' => 'cap_drop',
'--security-opt' => 'security_opt',
'--sysctl' => 'sysctls',
'--device' => 'devices',
'--ulimit' => 'ulimits',
'--init' => 'init',
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
]);
foreach ($matches as $match) {
$option = $match[1];
$value = isset($match[2]) && $match[2] !== '' ? $match[2] : true;
if ($list_options->contains($option)) {
$value = explode(',', $value);
}
if (array_key_exists($option, $options)) {
if (is_array($options[$option])) {
$options[$option][] = $value;
} else {
$options[$option] = [$options[$option], $value];
}
} else {
$options[$option] = $value;
}
}
$options = collect($options);
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
foreach ($options as $option => $value) {
if (!data_get($mapping, $option)) {
continue;
}
if ($option === '--ulimit') {
$ulimits = collect([]);
collect($value)->map(function ($ulimit) use ($ulimits){
$ulimit = explode('=', $ulimit);
$type = $ulimit[0];
$limits = explode(':', $ulimit[1]);
if (count($limits) == 2) {
$soft_limit = $limits[0];
$hard_limit = $limits[1];
$ulimits->put($type, [
'soft' => $soft_limit,
'hard' => $hard_limit
]);
} else {
$soft_limit = $ulimit[1];
$ulimits->put($type, [
'soft' => $soft_limit,
]);
}
});
$compose_options->put($mapping[$option], $ulimits);
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
$compose_options->put($mapping[$option], $options->get($mapping[$option]) . ',' . $value);
} else {
$compose_options->put($mapping[$option], $value);
}
continue;
} else {
$compose_options->put($mapping[$option], $value);
continue;
}
$compose_options->forget($option);
}
}
return $compose_options->toArray();
}

View File

@@ -69,6 +69,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
}
$json = $response->json();
if ($response->failed() && $throwError) {
ray($json);
throw new \Exception("Failed to get data from {$source->name} with error:<br><br>" . $json['message'] . "<br><br>Rate Limit resets at: " . Carbon::parse((int)$response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s') . 'UTC');
}
return [

View File

@@ -108,7 +108,7 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
}
return $output;
}
function generateSshCommand(Server $server, string $command, bool $isMux = true)
function generateSshCommand(Server $server, string $command)
{
$user = $server->user;
$port = $server->port;
@@ -120,7 +120,7 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
$delimiter = 'EOF-COOLIFY-SSH';
$ssh_command = "timeout $timeout ssh ";
if ($isMux && config('coolify.mux_enabled')) {
if (config('coolify.mux_enabled') && config('coolify.is_windows_docker_desktop') == false) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {

View File

@@ -22,6 +22,7 @@ use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Cache;
@@ -107,6 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "A resource with the same name already exists.");
}
return "A resource with the same name already exists.";
}
if ($error instanceof Throwable) {
$message = $error->getMessage();
@@ -1660,3 +1667,33 @@ function ip_match($ip, $cidrs, &$match = null)
}
return false;
}
function check_fqdn_usage(ServiceApplication|Application $own_resource)
{
$domains = collect($own_resource->fqdns)->map(function ($domain) {
return Url::fromString($domain)->getHost();
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid ) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
$apps = ServiceApplication::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
}

View File

@@ -24,7 +24,6 @@
"league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "^3.0",
"lorisleiva/laravel-actions": "^2.7",
"masmerise/livewire-toaster": "^2.0",
"nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0",
"poliander/cron": "^3.0",

567
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
<?php
return [
'docs' => 'https://coolify.io/docs/contact',
'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact',
'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io',
'mux_enabled' => env('MUX_ENABLED', true),
'dev_webhook' => env('SERVEO_URL'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
];

View File

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

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.199';
return '4.0.0-beta.203';

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->string('manual_webhook_secret_bitbucket')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('manual_webhook_secret_bitbucket');
});
}
};

View File

@@ -0,0 +1,37 @@
<?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::create('shared_environment_variables', function (Blueprint $table) {
$table->id();
$table->string('key');
$table->string('value')->nullable();
$table->boolean('is_shown_once')->default(false);
$table->enum('type', ['team', 'project', 'environment'])->default('team');
$table->foreignId('team_id')->constrained()->onDelete('cascade');
$table->foreignId('project_id')->nullable()->constrained()->onDelete('cascade');
$table->foreignId('environment_id')->nullable()->constrained()->onDelete('cascade');
$table->unique(['key', 'project_id', 'team_id']);
$table->unique(['key', 'environment_id', 'team_id']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('shared_environment_variables');
}
};

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('server_settings', function (Blueprint $table) {
$table->integer('concurrent_builds')->default(2);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('concurrent_builds');
});
}
};

View File

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

View File

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

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('shared_environment_variables', function (Blueprint $table) {
$table->text('value')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('shared_environment_variables', function (Blueprint $table) {
$table->string('value')->change();
});
}
};

View File

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

View File

@@ -18,6 +18,7 @@ class DatabaseSeeder extends Seeder
ProjectSeeder::class,
ProjectSettingSeeder::class,
EnvironmentSeeder::class,
TeamEnvironmentVariableSeeder::class,
StandaloneDockerSeeder::class,
SwarmDockerSeeder::class,
KubernetesSeeder::class,

View File

@@ -14,7 +14,6 @@ use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
@@ -67,7 +66,8 @@ class ProductionSeeder extends Seeder
]);
}
if (!isCloud()) {
if (!isCloud() && config('coolify.is_windows_docker_desktop') == false) {
echo "Checking localhost key.\n";
// Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
@@ -123,6 +123,59 @@ class ProductionSeeder extends Seeder
]);
}
}
if (config('coolify.is_windows_docker_desktop')) {
PrivateKey::updateOrCreate(
[
'id' => 0,
'team_id' => 0,
],
[
"name" => "Testing-host",
"description" => "This is a a docker container with SSH access",
"private_key" => "-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----
"
]
);
if (Server::find(0) == null) {
$server_details = [
'id' => 0,
'uuid' => 'coolify-testing-host',
'name' => "localhost",
'description' => "This is the server where Coolify is running on. Don't delete this!",
'user' => 'root',
'ip' => "coolify-testing-host",
'team_id' => 0,
'private_key_id' => 0
];
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK_V2->value,
'status' => ProxyStatus::EXITED->value
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
} else {
$server = Server::find(0);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
}
if (StandaloneDocker::find(0) == null) {
StandaloneDocker::create([
'id' => 0,
'name' => 'localhost-coolify',
'network' => 'coolify',
'server_id' => 0,
]);
}
}
try {
$settings = InstanceSettings::get();

View File

@@ -0,0 +1,36 @@
<?php
namespace Database\Seeders;
use App\Models\SharedEnvironmentVariable;
use Illuminate\Database\Seeder;
class SharedEnvironmentVariableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
SharedEnvironmentVariable::create([
'key' => 'NODE_ENV',
'value' => 'team_env',
'type' => 'team',
'team_id' => 0,
]);
SharedEnvironmentVariable::create([
'key' => 'NODE_ENV',
'value' => 'env_env',
'type' => 'environment',
'environment_id' => 1,
'team_id' => 0,
]);
SharedEnvironmentVariable::create([
'key' => 'NODE_ENV',
'value' => 'project_env',
'type' => 'project',
'project_id' => 1,
'team_id' => 0,
]);
}
}

View File

@@ -1,13 +1,5 @@
version: "3.8"
x-testing-host: &testing-host-base
build:
dockerfile: Dockerfile
context: ./docker/testing-host
networks:
- coolify
init: true
services:
coolify:
build:
@@ -30,6 +22,7 @@ services:
volumes:
- .:/var/www/html/:cached
postgres:
pull_policy: always
ports:
- "${FORWARD_DB_PORT:-5432}:5432"
env_file:
@@ -43,6 +36,7 @@ services:
- /data/coolify/_volumes/database/:/var/lib/postgresql/data
# - coolify-pg-data-dev:/var/lib/postgresql/data
redis:
pull_policy: always
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
@@ -62,6 +56,7 @@ services:
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
vite:
image: node:20
pull_policy: always
working_dir: /var/www/html
# environment:
# VITE_PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
@@ -73,7 +68,9 @@ services:
networks:
- coolify
testing-host:
<<: *testing-host-base
image: "ghcr.io/coollabsio/coolify-testing-host:latest"
pull_policy: always
init: true
container_name: coolify-testing-host
volumes:
- /:/host
@@ -83,6 +80,7 @@ services:
- coolify
mailpit:
image: "axllent/mailpit:latest"
pull_policy: always
container_name: coolify-mail
ports:
- "${FORWARD_MAILPIT_PORT:-1025}:1025"
@@ -91,6 +89,7 @@ services:
- coolify
minio:
image: minio/minio:latest
pull_policy: always
container_name: coolify-minio
command: server /data --console-address ":9001"
ports:

View File

@@ -1,7 +1,7 @@
version: '3.8'
services:
coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.190}"
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:latest}"
volumes:
- type: bind
source: /data/coolify/source/.env

128
docker-compose.windows.yml Normal file
View File

@@ -0,0 +1,128 @@
version: '3.8'
services:
coolify-testing-host:
init: true
image: "ghcr.io/coollabsio/coolify-testing-host:latest"
pull_policy: always
container_name: coolify-testing-host
volumes:
- //var/run/docker.sock://var/run/docker.sock
- ./:/data/coolify
coolify:
image: "ghcr.io/coollabsio/coolify:latest"
pull_policy: always
container_name: coolify
restart: always
working_dir: /var/www/html
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- type: bind
source: .env
target: /var/www/html/.env
read_only: true
- ./ssh:/var/www/html/storage/app/ssh
- ./applications:/var/www/html/storage/app/applications
- ./databases:/var/www/html/storage/app/databases
- ./services:/var/www/html/storage/app/services
- ./backups:/var/www/html/storage/app/backups
env_file:
- .env
environment:
- APP_ID
- APP_ENV=production
- APP_NAME
- APP_KEY
- DB_PASSWORD
- REDIS_PASSWORD
- SSL_MODE=off
- PHP_PM_CONTROL=dynamic
- PHP_PM_START_SERVERS=1
- PHP_PM_MIN_SPARE_SERVERS=1
- PHP_PM_MAX_SPARE_SERVERS=10
- PUSHER_APP_ID
- PUSHER_APP_KEY
- PUSHER_APP_SECRET
- AUTOUPDATE=true
- SELF_HOSTED=true
- MUX_ENABLED=false
- IS_WINDOWS_DOCKER_DESKTOP=true
ports:
- "${APP_PORT:-8000}:80"
expose:
- "${APP_PORT:-8000}"
healthcheck:
test: curl --fail http://localhost:80/api/health || exit 1
interval: 5s
retries: 10
timeout: 2s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:15-alpine
pull_policy: always
container_name: coolify-db
restart: always
env_file:
- .env
volumes:
- coolify-db:/var/lib/postgresql/data
environment:
POSTGRES_USER: "${DB_USERNAME:-coolify}"
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: "${DB_DATABASE:-coolify}"
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USERNAME:-coolify}",
"-d",
"${DB_DATABASE:-coolify}"
]
interval: 5s
retries: 10
timeout: 2s
redis:
image: redis:alpine
pull_policy: always
container_name: coolify-redis
restart: always
command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD}
env_file:
- .env
environment:
REDIS_PASSWORD: "${REDIS_PASSWORD}"
volumes:
- coolify-redis:/data
healthcheck:
test: redis-cli ping
interval: 5s
retries: 10
timeout: 2s
soketi:
image: 'quay.io/soketi/soketi:1.6-16-alpine'
pull_policy: always
container_name: coolify-realtime
restart: always
env_file:
- .env
ports:
- "${SOKETI_PORT:-6001}:6001"
environment:
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
healthcheck:
test: wget -qO- http://localhost:6001/ready || exit 1
interval: 5s
retries: 10
timeout: 2s
volumes:
coolify-db:
name: coolify-db
coolify-redis:
name: coolify-redis

230
package-lock.json generated
View File

@@ -6,22 +6,22 @@
"": {
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.3",
"alpinejs": "3.13.5",
"daisyui": "4.4.19",
"ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.16",
"axios": "1.6.5",
"autoprefixer": "10.4.17",
"axios": "1.6.7",
"laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.1",
"vue": "3.4.13"
"vite": "4.5.2",
"vue": "3.4.15"
}
},
"node_modules/@alloc/quick-lru": {
@@ -36,9 +36,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -524,77 +524,77 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.13.tgz",
"integrity": "sha512-zGUdmB3j3Irn9z51GXLJ5s0EAHxmsm5/eXl0y6MBaajMeOAaiT4+zaDoxui4Ets98dwIRr8BBaqXXHtHSfm+KA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.13",
"@vue/shared": "3.4.15",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.13.tgz",
"integrity": "sha512-XSNbpr5Rs3kCfVAmBqMu/HDwOS+RL6y28ZZjDlnDUuf146pRWt2sQkwhsOYc9uu2lxjjJy2NcyOkK7MBLVEc7w==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-core": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.13.tgz",
"integrity": "sha512-SkpmQN8xIFBd5onT413DFSDdjxULJf6jmJg/t3w/DZ9I8ZzyNlLIBLO0qFLewVHyHCiAgpPZlWqSRZXYrawk3Q==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.13",
"@vue/compiler-dom": "3.4.13",
"@vue/compiler-ssr": "3.4.13",
"@vue/shared": "3.4.13",
"@vue/compiler-core": "3.4.15",
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.13.tgz",
"integrity": "sha512-rwnw9SVBgD6eGKh8UucnwztieQo/R3RQrEGpE0b0cxb2xxvJeLs/fe7DoYlhEfaSyzM/qD5odkK87hl3G3oW+A==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-dom": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/reactivity": {
@@ -606,64 +606,64 @@
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.13.tgz",
"integrity": "sha512-Ov4d4At7z3goxqzSqQxdfVYEcN5HY4dM1uDYL6Hu/Es9Za9BEN602zyjWhhi2+BEki5F9NizRSvn02k/tqNWlg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
"dev": true,
"dependencies": {
"@vue/reactivity": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/reactivity": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.13.tgz",
"integrity": "sha512-/ZdUOrGKkGVONzVJkfDqNcn2fLMvaa5VlYx2KwTbnRbX06YZ4GJE0PVTmWzIxtBYdpSTLLXgw3pDggO+96KXzg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
"dev": true,
"dependencies": {
"@vue/shared": "3.4.13"
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.13.tgz",
"integrity": "sha512-ynde9p16eEV3u1VCxUre2e0nKzD0l3NzH0r599+bXeLT1Yhac8Atcot3iL9XNqwolxYCI89KBII+2MSVzfrz6w==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
"dev": true,
"dependencies": {
"@vue/runtime-core": "3.4.13",
"@vue/shared": "3.4.13",
"@vue/runtime-core": "3.4.15",
"@vue/shared": "3.4.15",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/server-renderer": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.13.tgz",
"integrity": "sha512-hkw+UQyDZZtSn1q30nObMfc8beVEQv2pG08nghigxGw+iOWodR+tWSuJak0mzWAHlP/xt/qLc//dG6igfgvGEA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
"dev": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"vue": "3.4.13"
"vue": "3.4.15"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/shared": {
@@ -672,9 +672,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
@@ -708,9 +708,9 @@
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
"dev": true,
"funding": [
{
@@ -727,9 +727,9 @@
}
],
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"browserslist": "^4.22.2",
"caniuse-lite": "^1.0.30001578",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -745,9 +745,9 @@
}
},
"node_modules/axios": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.4",
@@ -789,9 +789,9 @@
}
},
"node_modules/browserslist": {
"version": "4.21.11",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz",
"integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"dev": true,
"funding": [
{
@@ -808,9 +808,9 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001538",
"electron-to-chromium": "^1.4.526",
"node-releases": "^2.0.13",
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -829,9 +829,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001539",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz",
"integrity": "sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==",
"version": "1.0.30001580",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz",
"integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==",
"dev": true,
"funding": [
{
@@ -1014,9 +1014,9 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.528",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.528.tgz",
"integrity": "sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==",
"version": "1.4.647",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz",
"integrity": "sha512-Z/fTNGwc45WrYQhPaEcz5tAJuZZ8G7S/DBnhS6Kgp4BxnS40Z/HqlJ0hHg3Z79IGVzuVartIlTcjw/cQbPLgOw==",
"dev": true
},
"node_modules/entities": {
@@ -1168,9 +1168,9 @@
}
},
"node_modules/fraction.js": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"engines": {
"node": "*"
@@ -1506,9 +1506,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/normalize-path": {
@@ -2041,9 +2041,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@@ -2106,16 +2106,16 @@
}
},
"node_modules/vue": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.13.tgz",
"integrity": "sha512-FE3UZ0p+oUZTwz+SzlH/hDFg+XsVRFvwmx0LXjdD1pRK/cO4fu5v6ltAZji4za4IBih3dV78elUK3di8v3pWIg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.13",
"@vue/compiler-sfc": "3.4.13",
"@vue/runtime-dom": "3.4.13",
"@vue/server-renderer": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-sfc": "3.4.15",
"@vue/runtime-dom": "3.4.15",
"@vue/server-renderer": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"typescript": "*"
@@ -2127,9 +2127,9 @@
}
},
"node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/wrappy": {

View File

@@ -7,19 +7,19 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.16",
"axios": "1.6.5",
"autoprefixer": "10.4.17",
"axios": "1.6.7",
"laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.1",
"vue": "3.4.13"
"vite": "4.5.2",
"vue": "3.4.15"
},
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.3",
"alpinejs": "3.13.5",
"daisyui": "4.4.19",
"ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0"

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=79bae40dcb18de9ca1b5d0008c577471",
"/app.js": "/app.js?id=b4f3f08e60211bd6948ec35e5e9de9a1",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",

View File

@@ -19,7 +19,7 @@ button[isError] {
}
.main {
@apply pt-4 pl-24 pr-10 mx-auto;
@apply pt-4 pl-24 pr-10 lg:pr-32 lg:pl-44;
}
.custom-modal {
@@ -168,3 +168,6 @@ tr td:first-child {
input.input-sm {
@apply pr-10;
}
option{
@apply text-white;
}

View File

@@ -1,6 +1,5 @@
import { createApp } from "vue";
import MagicBar from "./components/MagicBar.vue";
import '../../vendor/masmerise/livewire-toaster/resources/js';
import "../../vendor/wire-elements/modal/resources/js/modal";
const app = createApp({});

View File

@@ -429,6 +429,13 @@ const magicActions = [{
tags: 'api,tokens,rest',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 26,
name: 'Goto: Team Shared Variables',
tags: 'team,shared,variables',
icon: 'goto',
sequence: ['main', 'redirect']
}
]
const initialState = {
@@ -665,6 +672,9 @@ async function redirect() {
case 25:
targetUrl.pathname = `/security/api-tokens`
break;
case 26:
targetUrl.pathname = `/team/shared-variables`
break;
}
window.location.href = targetUrl;
}

View File

@@ -13,7 +13,7 @@
</a>
</li>
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<a class="hover:bg-transparent" href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">

View File

@@ -258,7 +258,7 @@
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance?
<x-forms.button>
<a class="font-bold text-white hover:no-underline" href="{{ config('coolify.docs') }}">Contact
<a class="font-bold text-white hover:no-underline" href="{{ config('coolify.contact') }}">Contact
Us</a>
</x-forms.button>
</div>

View File

@@ -0,0 +1,48 @@
<div x-data="{
slideOverOpen: false
}" class="relative w-auto h-auto">
{{ $slot }}
<template x-teleport="body">
<div x-show="slideOverOpen" @keydown.window.escape="slideOverOpen=false" class="relative z-[99]">
<div x-show="slideOverOpen" @click="slideOverOpen = false" class="fixed inset-0 bg-black bg-opacity-60"></div>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="slideOverOpen" @click.away="slideOverOpen = false"
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="w-screen max-w-md">
<div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800">
<div class="px-4 pb-10 sm:px-5">
<div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title">
{{ $title }}</h2>
<div class="flex items-center h-auto ml-3">
<button class="icon" @click="slideOverOpen=false"
class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="relative flex-1 px-4 mt-5 sm:px-5">
<div class="absolute inset-0 px-4 sm:px-5">
{{ $content }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>

View File

@@ -14,20 +14,24 @@
</ol>
</nav>
<nav class="navbar-main">
<a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('team.member.index') ? 'text-white' : '' }}" href="{{ route('team.member.index') }}">
<a class="{{ request()->routeIs('team.member.index') ? 'text-white' : '' }}" href="{{ route('team.member.index') }}">
<button>Members</button>
</a>
<a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}"
href="{{ route('team.storage.index') }}">
<button>S3 Storages</button>
</a>
<a class="{{ request()->routeIs('team.notification.index') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('team.notification.index') ? 'text-white' : '' }}"
href="{{ route('team.notification.index') }}">
<button>Notifications</button>
</a>
<a class="{{ request()->routeIs('team.shared-variables.index') ? 'text-white' : '' }}"
href="{{ route('team.shared-variables.index') }}">
<button>Shared Variables</button>
</a>
<div class="flex-1"></div>
<div class="-mt-9">
<livewire:switch-team />

View File

@@ -0,0 +1,439 @@
<div x-data="{
title: 'Default Toast Notification',
description: '',
type: 'default',
position: 'top-center',
expanded: false,
popToast(custom) {
let html = '';
if (typeof custom != 'undefined') {
html = custom;
}
toast(this.title, { description: this.description, type: this.type, position: this.position, html: html })
}
}" x-init="window.toast = function(message, options = {}) {
let description = '';
let type = 'default';
let position = 'top-center';
let html = '';
if (typeof options.description != 'undefined') description = options.description;
if (typeof options.type != 'undefined') type = options.type;
if (typeof options.position != 'undefined') position = options.position;
if (typeof options.html != 'undefined') html = options.html;
window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } }));
}
window.customToastHTML = `
<div class='relative flex items-start justify-center p-4'>
<div class='flex flex-col'>
<p class='text-sm font-medium text-gray-800'>New Friend Request</p>
<p class='mt-1 text-xs leading-none text-gray-800'>Friend request from John Doe.</p>
<div class='flex mt-3'>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 text-xs font-semibold text-white bg-indigo-600 rounded shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'>Accept</button>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 ml-3 text-xs font-semibold text-gray-900 bg-white rounded shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50'>Decline</button>
</div>
</div>
</div>
`" class="relative space-y-5">
<template x-teleport="body">
<ul x-data="{
toasts: [],
toastsHovered: false,
expanded: false,
layout: 'default',
position: 'top-center',
paddingBetweenToasts: 15,
deleteToastWithId(id) {
for (let i = 0; i < this.toasts.length; i++) {
if (this.toasts[i].id === id) {
this.toasts.splice(i, 1);
break;
}
}
},
burnToast(id) {
burnToast = this.getToastWithId(id);
burnToastElement = document.getElementById(burnToast.id);
if (burnToastElement) {
if (this.toasts.length == 1) {
if (this.layout == 'default') {
this.expanded = false;
}
burnToastElement.classList.remove('translate-y-0');
if (this.position.includes('bottom')) {
burnToastElement.classList.add('translate-y-full');
} else {
burnToastElement.classList.add('-translate-y-full');
}
burnToastElement.classList.add('-translate-y-full');
}
burnToastElement.classList.add('opacity-0');
let that = this;
setTimeout(function() {
that.deleteToastWithId(id);
setTimeout(function() {
that.stackToasts();
}, 1)
}, 300);
}
},
getToastWithId(id) {
for (let i = 0; i < this.toasts.length; i++) {
if (this.toasts[i].id === id) {
return this.toasts[i];
}
}
},
stackToasts() {
this.positionToasts();
this.calculateHeightOfToastsContainer();
let that = this;
setTimeout(function() {
that.calculateHeightOfToastsContainer();
}, 300);
},
positionToasts() {
if (this.toasts.length == 0) return;
let topToast = document.getElementById(this.toasts[0].id);
topToast.style.zIndex = 100;
if (this.expanded) {
if (this.position.includes('bottom')) {
topToast.style.top = 'auto';
topToast.style.bottom = '0px';
} else {
topToast.style.top = '0px';
}
}
let bottomPositionOfFirstToast = this.getBottomPositionOfElement(topToast);
if (this.toasts.length == 1) return;
let middleToast = document.getElementById(this.toasts[1].id);
middleToast.style.zIndex = 90;
if (this.expanded) {
middleToastPosition = topToast.getBoundingClientRect().height +
this.paddingBetweenToasts + 'px';
if (this.position.includes('bottom')) {
middleToast.style.top = 'auto';
middleToast.style.bottom = middleToastPosition;
} else {
middleToast.style.top = middleToastPosition;
}
middleToast.style.scale = '100%';
middleToast.style.transform = 'translateY(0px)';
} else {
middleToast.style.scale = '94%';
if (this.position.includes('bottom')) {
middleToast.style.transform = 'translateY(-16px)';
} else {
this.alignBottom(topToast, middleToast);
middleToast.style.transform = 'translateY(16px)';
}
}
if (this.toasts.length == 2) return;
let bottomToast = document.getElementById(this.toasts[2].id);
bottomToast.style.zIndex = 80;
if (this.expanded) {
bottomToastPosition = topToast.getBoundingClientRect().height +
this.paddingBetweenToasts +
middleToast.getBoundingClientRect().height +
this.paddingBetweenToasts + 'px';
if (this.position.includes('bottom')) {
bottomToast.style.top = 'auto';
bottomToast.style.bottom = bottomToastPosition;
} else {
bottomToast.style.top = bottomToastPosition;
}
bottomToast.style.scale = '100%';
bottomToast.style.transform = 'translateY(0px)';
} else {
bottomToast.style.scale = '88%';
if (this.position.includes('bottom')) {
bottomToast.style.transform = 'translateY(-32px)';
} else {
this.alignBottom(topToast, bottomToast);
bottomToast.style.transform = 'translateY(32px)';
}
}
if (this.toasts.length == 3) return;
let burnToast = document.getElementById(this.toasts[3].id);
burnToast.style.zIndex = 70;
if (this.expanded) {
burnToastPosition = topToast.getBoundingClientRect().height +
this.paddingBetweenToasts +
middleToast.getBoundingClientRect().height +
this.paddingBetweenToasts +
bottomToast.getBoundingClientRect().height +
this.paddingBetweenToasts + 'px';
if (this.position.includes('bottom')) {
burnToast.style.top = 'auto';
burnToast.style.bottom = burnToastPosition;
} else {
burnToast.style.top = burnToastPosition;
}
burnToast.style.scale = '100%';
burnToast.style.transform = 'translateY(0px)';
} else {
burnToast.style.scale = '82%';
this.alignBottom(topToast, burnToast);
burnToast.style.transform = 'translateY(48px)';
}
burnToast.firstElementChild.classList.remove('opacity-100');
burnToast.firstElementChild.classList.add('opacity-0');
let that = this;
// Burn 🔥 (remove) last toast
setTimeout(function() {
that.toasts.pop();
}, 300);
if (this.position.includes('bottom')) {
middleToast.style.top = 'auto';
}
return;
},
alignBottom(element1, element2) {
// Get the top position and height of the first element
let top1 = element1.offsetTop;
let height1 = element1.offsetHeight;
// Get the height of the second element
let height2 = element2.offsetHeight;
// Calculate the top position for the second element
let top2 = top1 + (height1 - height2);
// Apply the calculated top position to the second element
element2.style.top = top2 + 'px';
},
alignTop(element1, element2) {
// Get the top position of the first element
let top1 = element1.offsetTop;
// Apply the same top position to the second element
element2.style.top = top1 + 'px';
},
resetBottom() {
for (let i = 0; i < this.toasts.length; i++) {
if (document.getElementById(this.toasts[i].id)) {
let toastElement = document.getElementById(this.toasts[i].id);
toastElement.style.bottom = '0px';
}
}
},
resetTop() {
for (let i = 0; i < this.toasts.length; i++) {
if (document.getElementById(this.toasts[i].id)) {
let toastElement = document.getElementById(this.toasts[i].id);
toastElement.style.top = '0px';
}
}
},
getBottomPositionOfElement(el) {
return (el.getBoundingClientRect().height + el.getBoundingClientRect().top);
},
calculateHeightOfToastsContainer() {
if (this.toasts.length == 0) {
$el.style.height = '0px';
return;
}
lastToast = this.toasts[this.toasts.length - 1];
lastToastRectangle = document.getElementById(lastToast.id).getBoundingClientRect();
firstToast = this.toasts[0];
firstToastRectangle = document.getElementById(firstToast.id).getBoundingClientRect();
if (this.toastsHovered) {
if (this.position.includes('bottom')) {
$el.style.height = ((firstToastRectangle.top + firstToastRectangle.height) - lastToastRectangle.top) + 'px';
} else {
$el.style.height = ((lastToastRectangle.top + lastToastRectangle.height) - firstToastRectangle.top) + 'px';
}
} else {
$el.style.height = firstToastRectangle.height + 'px';
}
}
}"
@set-toasts-layout.window="
layout=event.detail.layout;
if(layout == 'expanded'){
expanded=true;
} else {
expanded=false;
}
stackToasts();
"
@toast-show.window="
event.stopPropagation();
if(event.detail.position){
position = event.detail.position;
}
toasts.unshift({
id: 'toast-' + Math.random().toString(16).slice(2),
show: false,
message: event.detail.message,
description: event.detail.description,
type: event.detail.type,
html: event.detail.html
});
"
@mouseenter="toastsHovered=true;" @mouseleave="toastsHovered=false" x-init="if (layout == 'expanded') {
expanded = true;
}
stackToasts();
$watch('toastsHovered', function(value) {
if (layout == 'default') {
if (position.includes('bottom')) {
resetBottom();
} else {
resetTop();
}
if (value) {
// calculate the new positions
expanded = true;
if (layout == 'default') {
stackToasts();
}
} else {
if (layout == 'default') {
expanded = false;
//setTimeout(function(){
stackToasts();
//}, 10);
setTimeout(function() {
stackToasts();
}, 10)
}
}
}
});"
class="fixed block w-full group z-[99] sm:max-w-xs"
:class="{ 'right-0 top-0 sm:mt-6 sm:mr-6': position=='top-right', 'left-0 top-0 sm:mt-6 sm:ml-6': position=='top-left', 'left-1/2 -translate-x-1/2 top-0 sm:mt-6': position=='top-center', 'right-0 bottom-0 sm:mr-6 sm:mb-6': position=='bottom-right', 'left-0 bottom-0 sm:ml-6 sm:mb-6': position=='bottom-left', 'left-1/2 -translate-x-1/2 bottom-0 sm:mb-6': position=='bottom-center' }"
x-cloak>
<template x-for="(toast, index) in toasts" :key="toast.id">
<li :id="toast.id" x-data="{
toastHovered: false
}" x-init="if (position.includes('bottom')) {
$el.firstElementChild.classList.add('toast-bottom');
$el.firstElementChild.classList.add('opacity-0', 'translate-y-full');
} else {
$el.firstElementChild.classList.add('opacity-0', '-translate-y-full');
}
setTimeout(function() {
setTimeout(function() {
if (position.includes('bottom')) {
$el.firstElementChild.classList.remove('opacity-0', 'translate-y-full');
} else {
$el.firstElementChild.classList.remove('opacity-0', '-translate-y-full');
}
$el.firstElementChild.classList.add('opacity-100', 'translate-y-0');
setTimeout(function() {
stackToasts();
}, 10);
}, 5);
}, 50);
setTimeout(function() {
setTimeout(function() {
$el.firstElementChild.classList.remove('opacity-100');
$el.firstElementChild.classList.add('opacity-0');
if (toasts.length == 1) {
$el.firstElementChild.classList.remove('translate-y-0');
$el.firstElementChild.classList.add('-translate-y-full');
}
setTimeout(function() {
deleteToastWithId(toast.id)
}, 300);
}, 5);
}, 4000);"
@mouseover="toastHovered=true" @mouseout="toastHovered=false"
class="absolute w-full duration-200 ease-out select-none sm:max-w-xs"
:class="{ 'toast-no-description': !toast.description }">
<span
class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full transition-all duration-200 ease-out bg-coolgray-200 border border-coolgray-100 sm:rounded-md sm:max-w-xs group"
:class="{ 'p-4': !toast.html, 'p-0': toast.html }">
<template x-if="!toast.html">
<div class="relative">
<div class="flex items-start"
:class="{ 'text-green-500': toast.type=='success', 'text-blue-500': toast.type=='info', 'text-orange-400': toast.type=='warning', 'text-red-500': toast.type=='danger', 'text-gray-800': toast.type=='default' }">
<svg x-show="toast.type=='success'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM16.7744 9.63269C17.1238 9.20501 17.0604 8.57503 16.6327 8.22559C16.2051 7.87615 15.5751 7.93957 15.2256 8.36725L10.6321 13.9892L8.65936 12.2524C8.24484 11.8874 7.61295 11.9276 7.248 12.3421C6.88304 12.7566 6.92322 13.3885 7.33774 13.7535L9.31046 15.4903C10.1612 16.2393 11.4637 16.1324 12.1808 15.2547L16.7744 9.63269Z"
fill="currentColor"></path>
</svg>
<svg x-show="toast.type=='info'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9ZM13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16V12Z"
fill="currentColor"></path>
</svg>
<svg x-show="toast.type=='warning'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9.44829 4.46472C10.5836 2.51208 13.4105 2.51168 14.5464 4.46401L21.5988 16.5855C22.7423 18.5509 21.3145 21 19.05 21L4.94967 21C2.68547 21 1.25762 18.5516 2.4004 16.5862L9.44829 4.46472ZM11.9995 8C12.5518 8 12.9995 8.44772 12.9995 9V13C12.9995 13.5523 12.5518 14 11.9995 14C11.4473 14 10.9995 13.5523 10.9995 13V9C10.9995 8.44772 11.4473 8 11.9995 8ZM12.0009 15.99C11.4486 15.9892 11.0003 16.4363 10.9995 16.9886L10.9995 16.9986C10.9987 17.5509 11.4458 17.9992 11.9981 18C12.5504 18.0008 12.9987 17.5537 12.9995 17.0014L12.9995 16.9914C13.0003 16.4391 12.5532 15.9908 12.0009 15.99Z"
fill="currentColor"></path>
</svg>
<svg x-show="toast.type=='danger'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z"
fill="currentColor"></path>
</svg>
<p class="font-bold leading-2 text-neutral-200"
x-html="toast.message">
</p>
</div>
<p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
class="mt-1.5 text-xs leading-2 opacity-90" x-html="toast.description"></p>
</div>
</template>
<template x-if="toast.html">
<div x-html="toast.html"></div>
</template>
<span @click="burnToast(toast.id)"
class="absolute right-0 p-1.5 mr-2.5 text-neutral-400 duration-100 ease-in-out rounded-full opacity-0 cursor-pointer hover:bg-coolgray-400 hover:text-neutral-300"
:class="{
'top-1/2 -translate-y-1/2': !toast.description && !toast.html,
'top-0 mt-2.5': (toast
.description || toast.html),
'opacity-100': toastHovered,
'opacity-0': !
toastHovered
}">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</span>
</span>
</li>
</template>
</ul>
</template>
</div>

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