Compare commits

...

86 Commits

Author SHA1 Message Date
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
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
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
Andras Bacsai
2fdf52929c Merge pull request #1670 from coollabsio/next
v4.0.0-beta.199
2024-01-22 11:02:30 +01:00
Andras Bacsai
8128dfc061 Update resource limits helper links 2024-01-22 10:47:47 +01:00
Andras Bacsai
2b394d6fea fix: show container on logs/executecontainer command views
fix: exclude containers with restart: no from hc
feat: add compose to predefined docker network
service: add glitchtip
2024-01-21 14:30:03 +01:00
Andras Bacsai
964ded1d0b fix: redis custom conf 2024-01-21 12:06:51 +01:00
Andras Bacsai
838c3830d6 Merge pull request #1664 from coollabsio/next
v4.0.0-beta.198
2024-01-18 15:06:12 +01:00
Andras Bacsai
2db93bd9b9 Add echo statement for queue cleanup and update cleanup message 2024-01-18 14:56:12 +01:00
Andras Bacsai
2f82dedd4f Add call to 'cleanup:queue' command in Init.php and remove 'cleanup:queue' command from Kernel.php 2024-01-18 14:49:47 +01:00
Andras Bacsai
8106602d15 Add CleanupQueue command to clean up Redis queue keys 2024-01-18 14:47:17 +01:00
Andras Bacsai
3d0bf6b472 Update resource name in notification messages 2024-01-18 14:03:51 +01:00
Andras Bacsai
ba7a7e9695 Update server and version configurations 2024-01-18 13:33:57 +01:00
Andras Bacsai
dd0ad04384 Merge pull request #1662 from coollabsio/next
Do not report server is not ready
2024-01-18 12:45:05 +01:00
Andras Bacsai
910a1f43a9 Do not report server is not ready 2024-01-18 12:44:20 +01:00
Andras Bacsai
e2f959ce4c Merge pull request #1659 from coollabsio/next
v4.0.0-beta.197
2024-01-18 12:14:50 +01:00
Andras Bacsai
68fe886fb0 Add restart functionality to proxy deployment 2024-01-18 12:14:11 +01:00
Andras Bacsai
4631c73809 Update general.blade.php with build server option and network section 2024-01-18 12:05:48 +01:00
Andras Bacsai
77558b37da refactor build server 2024-01-18 11:40:13 +01:00
Andras Bacsai
e060409a76 fix: links 2024-01-18 11:24:07 +01:00
Andras Bacsai
af01bc3e77 fix: service deletion bug! 2024-01-17 15:48:01 +01:00
Andras Bacsai
1e158badfc Update button text in by-ip.blade.php 2024-01-17 15:41:38 +01:00
Andras Bacsai
c620bb58ed Add build pack selection and show/hide static site options 2024-01-17 15:41:32 +01:00
Andras Bacsai
8a91395472 Update server model to use 'coolify' instead of 'coolify-overlay' for the name field 2024-01-17 14:11:46 +01:00
Andras Bacsai
c5f3398b73 Update build server and swarm support messages 2024-01-17 14:06:41 +01:00
Andras Bacsai
3878527de8 Update server unreachable notifications to include automatic revival 2024-01-17 14:02:54 +01:00
Andras Bacsai
4abcb2d5b9 Update notification labels in Discord and Telegram settings 2024-01-17 12:23:58 +01:00
Andras Bacsai
a635e51486 fix: server status job 2024-01-17 11:52:56 +01:00
Andras Bacsai
b6ce2e9122 typo 2024-01-17 11:39:45 +01:00
Andras Bacsai
8c60dd5523 typo 2024-01-17 11:21:47 +01:00
Andras Bacsai
94e2d951c4 Revert commented out code and execute remote command to remove Docker container 2024-01-16 15:45:54 +01:00
Andras Bacsai
381e24bea5 fix: git pull command for deploy key based previews 2024-01-16 15:45:19 +01:00
Andras Bacsai
2b1e35980f empty nixpacks type result in error 2024-01-16 15:26:44 +01:00
Andras Bacsai
a42c8da344 fix: proxy ui view
feat: build server 🌮
2024-01-16 15:19:14 +01:00
Andras Bacsai
7a0e415ecf fix: checkbox click 2024-01-16 12:28:59 +01:00
Andras Bacsai
d721f4809a fix ui 2024-01-16 12:20:40 +01:00
Andras Bacsai
22431eee9a fix: change proxy view 2024-01-16 11:32:56 +01:00
Andras Bacsai
c058c0a766 Update release version to 4.0.0-beta.197 2024-01-16 08:38:39 +01:00
Andras Bacsai
1724c0d3ff Merge pull request #1658 from coollabsio/next
v4.0.0-beta.196
2024-01-15 20:22:57 +01:00
Andras Bacsai
0b8f48230f Remove unnecessary echo statements in Server.php 2024-01-15 20:22:13 +01:00
Andras Bacsai
e8d84b7067 Update server and version configurations 2024-01-15 19:57:29 +01:00
157 changed files with 2972 additions and 1138 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

@@ -93,6 +93,10 @@ Contact us [here](https://coolify.io/docs/contact).
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image")
# Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

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

@@ -3,6 +3,7 @@
namespace App\Actions\Database;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -106,7 +107,7 @@ class StartRedis
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' /usr/local/etc/redis/redis.conf';
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
@@ -150,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()) {
@@ -165,8 +166,9 @@ class StartRedis
return;
}
$filename = 'redis.conf';
$content = $this->database->redis_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
Storage::disk('local')->put("tmp/redis.conf_{$this->database->uuid}", $this->database->redis_conf);
$path = Storage::path("tmp/redis.conf_{$this->database->uuid}");
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
Storage::disk('local')->delete("tmp/redis.conf_{$this->database->uuid}");
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,7 +14,6 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity
{
try {
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();

View File

@@ -10,8 +10,10 @@ class DeleteService
use AsAction;
public function handle(Service $service)
{
StopService::run($service);
$server = data_get($service, 'server');
if ($server->isFunctional()) {
StopService::run($service);
}
$storagesToDelete = collect([]);
$service->environment_variables()->delete();

View File

@@ -12,7 +12,6 @@ class StartService
public function handle(Service $service)
{
ray('Starting service: ' . $service->name);
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
@@ -24,10 +23,13 @@ class StartService
$commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate --build";
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
$compose = data_get($service, 'docker_compose', []);
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
return $activity;

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupQueue extends Command
{
protected $signature = 'cleanup:queue';
protected $description = 'Cleanup Queue';
public function handle()
{
echo "Running queue cleanup...\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
}
}
}

View File

@@ -9,8 +9,6 @@ use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
@@ -18,13 +16,11 @@ use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use App\Notifications\Test;
use App\Notifications\TransactionalEmails\InvitationLink;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Illuminate\Support\Str;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;

View File

@@ -30,7 +30,7 @@ class Init extends Command
$this->alive();
$cleanup = $this->option('cleanup');
if ($cleanup) {
echo "Running cleanup\n";
echo "Running cleanups...\n";
$this->cleanup_stucked_resources();
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
@@ -54,6 +54,7 @@ class Init extends Command
$settings->update(['is_auto_update_enabled' => false]);
}
}
$this->call('cleanup:queue');
}
private function restore_coolify_db_backup()
{

View File

@@ -60,10 +60,10 @@ class Kernel extends ConsoleKernel
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false);
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false);
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
}
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
@@ -72,7 +72,7 @@ class Kernel extends ConsoleKernel
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyFiveMinutes()->onOneServer();
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)
@@ -111,7 +111,8 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_tasks($schedule) {
private function check_scheduled_tasks($schedule)
{
$scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) {
ray('no scheduled tasks');
@@ -134,7 +135,6 @@ class Kernel extends ConsoleKernel
task: $scheduled_task
))->cron($scheduled_task->frequency)->onOneServer();
}
}
protected function commands(): void

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception("Team id is null");
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -56,7 +56,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
// Deploy to Server
private Server $server;
// Build Server
private Server $build_server;
private bool $use_build_server = false;
// Save original server between phases
private Server $original_server;
private Server $mainServer;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
@@ -196,6 +202,24 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Check custom port
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
if (data_get($this->application, 'settings.is_build_server_enabled')) {
$teamId = data_get($this->application, 'environment.project.team.id');
$buildServers = Server::buildServers($teamId)->get();
if ($buildServers->count() === 0) {
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
$this->build_server = $this->server;
$this->original_server = $this->server;
} else {
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
$this->build_server = $buildServers->random();
$this->original_server = $this->server;
$this->use_build_server = true;
}
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server;
$this->original_server = $this->server;
}
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
@@ -225,7 +249,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage' && !$this->application->destination->server->isSwarm()) {
// Otherwise built image needs to be pushed before from the build server.
if (!$this->use_build_server) {
$this->push_to_docker_registry();
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
@@ -234,23 +259,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->fail($e);
throw $e;
} finally {
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
if ($this->use_build_server) {
$this->server = $this->build_server;
} else {
$this->write_deployment_configurations();
}
$this->execute_remote_command(
[
@@ -269,42 +281,71 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
private function push_to_docker_registry()
private function write_deployment_configurations()
{
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
if (isset($this->docker_compose_base64)) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
"mkdir -p $this->configuration_dir"
],
["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
if ($this->use_build_server) {
$this->server = $this->build_server;
}
}
}
private function push_to_docker_registry($forceFail = false)
{
if (
$this->application->docker_registry_image_name &&
$this->application->build_pack !== 'dockerimage' &&
!$this->application->destination->server->isSwarm() &&
!$this->restart_only &&
!(str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged())
) {
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
['echo -n "Tagging and pushing image with latest tag."'],
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$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.'");
if ($forceFail) {
throw $e;
}
ray($e);
}
$this->execute_remote_command([
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
if ($this->application->destination->server->isSwarm()) {
throw $e;
}
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
ray($e);
}
}
private function generate_image_names()
@@ -340,20 +381,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function just_restart()
{
$this->execute_remote_command(
[
"echo '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();
$this->generate_image_names();
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->execute_remote_command([
"echo 'Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.'",
]);
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->create_workdir();
$this->generate_compose_file();
$this->rolling_update();
@@ -380,11 +415,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"));
@@ -397,16 +432,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function deploy_simple_dockerfile()
{
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$dockerfile_base64 = base64_encode($this->application->dockerfile);
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->name}.'"
],
);
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
$this->prepare_builder_image();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > {$this->workdir}{$this->dockerfile_location}")
],
);
$this->generate_image_names();
@@ -422,11 +456,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.");
$this->generate_image_names();
$this->prepare_builder_image();
$this->generate_compose_file();
@@ -496,24 +526,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
]);
}
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
}
$this->write_deployment_configurations();
// Start compose file
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
@@ -528,14 +541,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_dockerfile_buildpack()
{
if ($this->use_build_server) {
$this->server = $this->build_server;
}
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
$this->execute_remote_command(
[
"echo '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->clone_repository();
@@ -555,11 +567,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_nixpacks_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$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();
@@ -568,17 +579,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->execute_remote_command([
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]);
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->rolling_update();
return;
}
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
$this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image.");
}
}
$this->clone_repository();
@@ -592,11 +599,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_static_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$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();
@@ -619,18 +625,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
}
if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) {
$this->execute_remote_command(
[
"echo 'There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/frameworks/laravel#requirements'", 'type' => 'err'
],
);
$this->application_deployment_queue->addLogEntry("There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/frameworks/laravel#requirements", 'stderr');
}
}
private function rolling_update()
{
if ($this->server->isSwarm()) {
if ($this->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
$this->push_to_docker_registry(forceFail: true);
}
$this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command(
@@ -640,22 +642,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
if ($this->use_build_server) {
$this->push_to_docker_registry(forceFail: true);
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
@@ -676,17 +675,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 1;
$this->execute_remote_command(
[
"echo 'Waiting for healthcheck to pass on the new container.'"
]
);
$this->application_deployment_queue->addLogEntry("Waiting for healthcheck to pass on the new container.");
if ($this->full_healthcheck_url) {
$this->execute_remote_command(
[
"echo 'Healthcheck URL (inside the container): {$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) {
$this->execute_remote_command(
@@ -698,19 +689,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
$this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
"echo 'New container is healthy.'"
],
);
$this->application_deployment_queue->addLogEntry("New container is healthy.");
break;
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
@@ -725,11 +708,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_pull_request()
{
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$this->newVersionIsHealthy = true;
$this->generate_image_names();
$this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'",
]);
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image();
$this->clone_repository();
$this->set_base_dir();
@@ -754,10 +738,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
$this->application_deployment_queue->addLogEntry("Starting preview deployment.");
if ($this->use_build_server) {
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}
}
private function create_workdir()
@@ -774,16 +764,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
if ($this->use_build_server) {
if ($this->dockerConfigFileExists === 'NOK') {
throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.');
}
$runCommand = "docker run -d --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command(
[
"echo -n 'Preparing container with helper image: $helperImage.'",
],
[
$runCommand,
"hidden" => true,
@@ -801,19 +795,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command(
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
continue;
}
$this->server = $server;
$this->execute_remote_command(
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}.");
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
@@ -821,11 +807,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function set_base_dir()
{
$this->execute_remote_command(
[
"echo -n 'Setting base directory to {$this->workdir}.'"
],
);
$this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
}
private function check_git_if_build_needed()
{
@@ -898,26 +880,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs()
{
$nixpacks_command = $this->nixpacks_build_cmd();
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command), "save" => "nixpacks_plan", "hidden" => true],
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), "save" => "nixpacks_type", "hidden" => true],
);
if ($this->saved_outputs->get('nixpacks_type')) {
$this->nixpacks_type = $this->saved_outputs->get('nixpacks_type');
if (str($this->nixpacks_type)->isEmpty()) {
throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
}
}
if ($this->saved_outputs->get('nixpacks_plan')) {
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
if ($this->nixpacks_plan) {
$this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}.");
$this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}");
$parsed = Toml::Parse($this->nixpacks_plan);
// Do any modifications here
$this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
data_set($parsed, 'variables', $merged_envs->toArray());
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
ray($this->nixpacks_plan);
}
}
}
@@ -943,11 +927,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}");
}
}
@@ -958,11 +942,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);
@@ -1173,22 +1157,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
@@ -1234,9 +1215,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function pull_latest_image($image)
{
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command(
["echo -n 'Pulling latest image ($image) from the registry.'"],
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
]
@@ -1244,25 +1224,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function build_image()
{
$this->application_deployment_queue->addLogEntry("----------------------------------------");
if ($this->application->build_pack === 'static') {
$this->execute_remote_command([
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
$this->application_deployment_queue->addLogEntry("Static deployment. Copying static assets to the image.");
} else {
$this->execute_remote_command(
[
"echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
$this->application_deployment_queue->addLogEntry("Building docker image started.");
$this->application_deployment_queue->addLogEntry("To check the current progress, click on Show Debug Logs.");
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image);
$this->execute_remote_command(
["echo -n 'Continue with the building process.'"],
);
$this->application_deployment_queue->addLogEntry("Continuing with the building process.");
}
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -1405,9 +1378,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
}
$this->execute_remote_command([
"echo -n 'Building docker image completed.'",
]);
$this->application_deployment_queue->addLogEntry("Building docker image completed.");
}
private function stop_running_container(bool $force = false)
@@ -1463,13 +1434,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
if ($this->docker_compose_location) {
if ($this->use_build_server) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}
@@ -1481,12 +1452,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}");
}
}
@@ -1502,11 +1473,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"));
@@ -1535,13 +1506,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'", 'type' => 'err'],
);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
$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');
$this->execute_remote_command(
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
}

View File

@@ -27,6 +27,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
@@ -37,15 +40,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->uuid;
}
public function __construct(public Server $server)
{
$this->handle();
}
public function handle()
{
if (!$this->server->isServerReady($this->tries)) {
return 'Server is not reachable.';
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
};
try {
if ($this->server->isSwarm()) {

View File

@@ -31,11 +31,16 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{
try {
$server = $this->resource->destination->server;
$this->resource->delete();
if (!$server->isFunctional()) {
$this->resource->forceDelete();
if ($this->resource->type() === 'service') {
ray('dispatching delete service');
DeleteService::dispatch($this->resource);
} else {
$this->resource->forceDelete();
}
return 'Server is not functional';
}
$this->resource->delete();
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);

View File

@@ -19,7 +19,7 @@ class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string

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

@@ -37,6 +37,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
if (!$this->server->isServerReady($this->tries)) {
throw new \RuntimeException('Server is not ready.');
};
try {
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);

View File

@@ -3,6 +3,7 @@
namespace App\Livewire;
use App\Enums\ProcessStatus;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
@@ -10,14 +11,16 @@ class ActivityMonitor extends Component
{
public ?string $header = null;
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newMonitorActivity'];
public function newMonitorActivity($activityId)
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity();
@@ -35,13 +38,28 @@ class ActivityMonitor extends Component
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
if ($exit_code === 0) {
// $this->setStatus(ProcessStatus::FINISHED);
} else {
// $this->setStatus(ProcessStatus::ERROR);
}
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
$this->dispatch('activityFinished');
if ($exit_code === 0) {
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
}
}
}
}

View File

@@ -16,6 +16,7 @@ class Advanced extends Component
'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',

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

@@ -67,6 +67,7 @@ class General extends Component
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -100,6 +101,7 @@ class General extends Component
'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_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
];
public function mount()
{
@@ -227,7 +229,6 @@ class General extends Component
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',

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;
@@ -56,6 +55,7 @@ class Heading extends Component
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
@@ -74,12 +74,17 @@ class Heading extends Component
return;
}
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'Please set a Docker image name first.');
$this->dispatch('error', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return;
}
if (data_get($this->application, 'settings.is_build_server_enabled') && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'To use a build server you must set a Docker image name first.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
@@ -109,6 +114,7 @@ class Heading extends Component
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
is_new_deployment: true,
@@ -125,6 +131,7 @@ class Heading extends Component
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);

View File

@@ -48,9 +48,11 @@ class Previews extends Component
}
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
deployment_uuid: $this->deployment_uuid,
force_rebuild: true,
force_rebuild: false,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],

View File

@@ -25,6 +25,7 @@ class Rollback extends Component
queue_application_deployment(
application_id: $this->application->id,
server_id: $this->application->destination->server->id,
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,18 +30,22 @@ class PublicGitRepository extends Component
public GithubApp|GitlabApp|string $git_source = 'other';
public string $git_host;
public string $git_repository;
public $build_pack = 'nixpacks';
public bool $show_is_static = true;
protected $rules = [
'repository_url' => 'required|url',
'port' => 'required|numeric',
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
];
protected $validationAttributes = [
'repository_url' => 'repository',
'port' => 'port',
'is_static' => 'static',
'publish_directory' => 'publish directory',
'build_pack' => 'build pack',
];
public function mount()
@@ -53,7 +57,20 @@ class PublicGitRepository extends Component
$this->parameters = get_route_parameters();
$this->query = request()->query();
}
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) {
@@ -157,6 +174,7 @@ class PublicGitRepository extends Component
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
];
} else {
$application_init = [
@@ -170,7 +188,8 @@ class PublicGitRepository extends Component
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass()
'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
];
}

View File

@@ -104,7 +104,7 @@ class Select extends Component
if ($this->includeSwarm) {
$this->servers = $this->allServers;
} else {
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
}
}
public function setType(string $type)
@@ -120,13 +120,13 @@ class Select extends Component
case 'mongodb':
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
break;
}
if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty')) {
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
}
if ($type === "existing-postgresql") {
$this->current_step = $type;

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

@@ -14,6 +14,7 @@ class StackForm extends Component
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
'service.connect_to_docker_network' => 'nullable',
];
public $validationAttributes = [];
public function mount()
@@ -44,6 +45,9 @@ class StackForm extends Component
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function instantSave() {
$this->service->save();
}
public function submit()
{

View File

@@ -8,5 +8,5 @@ class Destination extends Component
{
public $resource;
public $servers = [];
public $additionalServers = [];
public $additional_servers = [];
}

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

@@ -79,21 +79,21 @@ class ExecuteContainerCommand extends Component
$this->resource = $resource;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
if (str(data_get($this,'resource.status'))->startsWith('running')) {
// if (!str(data_get($this,'resource.status'))->startsWith('exited')) {
$this->containers->push($this->container);
}
// }
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
// if (str(data_get($application, 'status'))->contains('running')) {
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
}
// }
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
// if (str(data_get($database, 'status'))->contains('running')) {
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
}
// }
});
$this->server = $this->resource->server;

View File

@@ -70,21 +70,21 @@ class Logs extends Component
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
if (str(data_get($this, 'resource.status'))->startsWith('running')) {
// if (str(data_get($this, 'resource.status'))->startsWith('running')) {
$this->containers->push($this->container);
}
// }
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
// if (str(data_get($application, 'status'))->contains('running')) {
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
}
// }
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
// if (str(data_get($database, 'status'))->contains('running')) {
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
}
// }
});
$this->server = $this->resource->server;

View File

@@ -0,0 +1,173 @@
<?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();
$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

@@ -26,6 +26,8 @@ class Form extends Component
'server.settings.is_reachable' => 'required',
'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 = [
@@ -38,6 +40,8 @@ class Form extends Component
'server.settings.is_reachable' => 'Is reachable',
'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()
@@ -76,7 +80,7 @@ class Form extends Component
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
}
@@ -85,12 +89,12 @@ class Form extends Component
try {
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();

View File

@@ -25,6 +25,8 @@ class ByIp extends Component
public bool $is_swarm_worker = false;
public $selected_swarm_cluster = null;
public bool $is_build_server = false;
public $swarm_managers = [];
protected $rules = [
'name' => 'required|string',
@@ -34,6 +36,7 @@ class ByIp extends Component
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
'is_build_server' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -43,6 +46,7 @@ class ByIp extends Component
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker',
'is_build_server' => 'Build Server',
];
public function mount()
@@ -89,8 +93,14 @@ class ByIp extends Component
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
}
$server = Server::create($payload);
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->is_swarm_worker = $this->is_swarm_worker;
if ($this->is_build_server) {
$this->is_swarm_manager = false;
$this->is_swarm_worker = false;
} else {
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->is_swarm_worker = $this->is_swarm_worker;
}
$server->settings->is_build_server = $this->is_build_server;
$server->settings->save();
$server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);

View File

@@ -33,7 +33,6 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyStatusUpdated');
}
public function select_proxy($proxy_type)

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Livewire\Component;
@@ -14,7 +15,17 @@ class Deploy extends Component
public ?string $currentRoute = null;
public ?string $serverIp = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
'proxyStatusUpdated',
'traefikDashboardAvailable',
'serverRefresh' => 'proxyStatusUpdated',
"checkProxy", "startProxy"
];
}
public function mount()
{
@@ -29,12 +40,22 @@ class Deploy extends Component
{
$this->traefikDashboardAvailable = $data;
}
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('success', 'Proxy started.');
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function ip()
{
public function restart() {
try {
$this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
@@ -50,7 +71,7 @@ class Deploy extends Component
{
try {
$activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -77,6 +98,5 @@ class Deploy extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

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

@@ -263,6 +263,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 +435,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());
}
@@ -1002,7 +1006,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();

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

@@ -71,7 +71,7 @@ class Server extends BaseModel
static public function isUsable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false);
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false);
}
static public function destinationsByServer(string $server_id)
@@ -112,7 +112,7 @@ class Server extends BaseModel
]);
} else {
StandaloneDocker::create([
'name' => 'coolify-overlay',
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $this->id,
]);
@@ -141,6 +141,10 @@ class Server extends BaseModel
{
return $this->ip === 'host.docker.internal' || $this->id === 0;
}
static public function buildServers($teamId)
{
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
}
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
@@ -194,7 +198,7 @@ class Server extends BaseModel
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services() as $service) {
foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
@@ -328,7 +332,7 @@ class Server extends BaseModel
}
public function isProxyShouldRun()
{
if ($this->proxyType() === ProxyTypes::NONE->value) {
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
return false;
}
// foreach ($this->applications() as $application) {
@@ -394,6 +398,8 @@ class Server extends BaseModel
}
public function validateConnection()
{
config()->set('coolify.mux_enabled', false);
$server = Server::find($this->id);
if (!$server) {
return false;
@@ -436,7 +442,7 @@ class Server extends BaseModel
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork(isSwarm: false);
$this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server);
return true;
}
public function validateDockerSwarm()
@@ -466,8 +472,11 @@ class Server extends BaseModel
$this->settings->save();
return true;
}
public function validateCoolifyNetwork($isSwarm = false)
public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false)
{
if ($isBuildServer) {
return;
}
if ($isSwarm) {
return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
} else {

View File

@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
class Service extends BaseModel
{
@@ -17,6 +18,10 @@ class Service extends BaseModel
{
return 'service';
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function extraFields()
{
$fields = collect([]);
@@ -423,7 +428,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

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

@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: A resource has been stopped unexpectedly on {$this->server->name}";
$message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "Coolify: A resource has been stopped unexpectedly on {$this->server->name}";
$message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
$message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
"message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."
];
}
}

View File

@@ -19,7 +19,7 @@ class Checkbox extends Component
public string|null $helper = null,
public string|bool $instantSave = false,
public bool $disabled = false,
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700"
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700",
) {
//
}

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,21 +1,19 @@
<?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;
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(int $application_id, int $server_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)
{
$server = Application::find($application_id)->destination->server;
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'server_id' => $server_id,
'deployment_uuid' => $deployment_uuid,
'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild,
@@ -24,9 +22,15 @@ 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);
$deployments_per_server = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get();
$deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('server_id', $server_id);
$queued_deployments = $deployments->where('status', 'queued')->get()->sortByDesc('created_at');
$running_deployments = $deployments->where('status', 'in_progress')->get()->sortByDesc('created_at');
ray("serverId:{$server->id}", "concurrentBuilds:{$server->settings->concurrent_builds}", "deployments:{$deployments_per_server->count()}", "queued:{$queued_deployments->count()}", "running:{$running_deployments->count()}");
// 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) {
@@ -37,6 +41,9 @@ function queue_application_deployment(int $application_id, string $deployment_uu
if ($running_deployments->count() > 0) {
return;
}
if ($deployments_per_server->count() > $server->settings->concurrent_builds) {
return;
}
if ($is_new_deployment) {
dispatch(new ApplicationDeploymentNewJob(
deployment: $deployment,
@@ -51,7 +58,10 @@ function queue_application_deployment(int $application_id, string $deployment_uu
function queue_next_deployment(Application $application, bool $isNew = false)
{
$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();;
// $next_found = ApplicationDeploymentQueue::where('status', 'queued')->get()->sortBy('created_at')->first();
ray($next_found, $server_id);
if ($next_found) {
if ($isNew) {
dispatch(new ApplicationDeploymentNewJob(

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,13 +120,13 @@ 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')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$command = "test -f ~/.profile && . ~/.profile; PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '

View File

@@ -1036,6 +1036,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE);
}
if (data_get($service, 'restart') === 'no') {
$savedService->update(['exclude_from_status' => true]);
}
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');

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

73
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "de3b59fade9b132d2582a40dcf3c00f9",
"content-hash": "19b19082b605e09867e6ae65fb8135f6",
"packages": [
{
"name": "amphp/amp",
@@ -4485,77 +4485,6 @@
],
"time": "2023-02-05T15:03:45+00:00"
},
{
"name": "masmerise/livewire-toaster",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/masmerise/livewire-toaster.git",
"reference": "89aa127df5d17b915b0818761bdf83e8915036c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/masmerise/livewire-toaster/zipball/89aa127df5d17b915b0818761bdf83e8915036c2",
"reference": "89aa127df5d17b915b0818761bdf83e8915036c2",
"shasum": ""
},
"require": {
"laravel/framework": "^10.0",
"livewire/livewire": "^3.0",
"php": "~8.2"
},
"conflict": {
"stevebauman/unfinalize": "*"
},
"require-dev": {
"dive-be/php-crowbar": "^1.0",
"laravel/pint": "^1.0",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Toaster": "Masmerise\\Toaster\\Toaster"
},
"providers": [
"Masmerise\\Toaster\\ToasterServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Masmerise\\Toaster\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Muhammed Sari",
"email": "support@muhammedsari.me",
"role": "Developer"
}
],
"description": "Beautiful toast notifications for Laravel / Livewire.",
"homepage": "https://github.com/masmerise/livewire-toaster",
"keywords": [
"alert",
"laravel",
"livewire",
"toast",
"toaster"
],
"support": {
"issues": "https://github.com/masmerise/livewire-toaster/issues",
"source": "https://github.com/masmerise/livewire-toaster/tree/2.0.3"
},
"time": "2023-09-28T12:07:49+00:00"
},
{
"name": "monolog/monolog",
"version": "3.5.0",

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.195',
'release' => '4.0.0-beta.200',
// 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.195';
return '4.0.0-beta.200';

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_settings', function (Blueprint $table) {
$table->boolean('is_build_server_enabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_build_server_enabled');
});
}
};

View File

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

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

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

@@ -14,7 +14,7 @@ class ServerSettingSeeder extends Seeder
{
$server_2 = Server::find(0)->load(['settings']);
$server_2->settings->wildcard_domain = 'http://127.0.0.1.sslip.io';
$server_2->settings->is_build_server = true;
$server_2->settings->is_build_server = false;
$server_2->settings->is_usable = true;
$server_2->settings->is_reachable = true;
$server_2->settings->save();

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:

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

8
package-lock.json generated
View File

@@ -20,7 +20,7 @@
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.1",
"vite": "4.5.2",
"vue": "3.4.13"
}
},
@@ -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",

View File

@@ -14,7 +14,7 @@
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.1",
"vite": "4.5.2",
"vue": "3.4.13"
},
"dependencies": {

View File

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

@@ -1,119 +0,0 @@
<div class="navbar-main">
<a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
@if (!$application->destination->server->isSwarm())
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
@endif
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'text-white' : '' }}"
href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<x-applications.links :application="$application" />
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
@if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" 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">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</button>
@else
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" 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">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" 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">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart (new)
</button>
@endif --}}
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif
@endif
</div>

View File

@@ -1,6 +1,6 @@
<div class="px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit">
<div class="flex flex-row items-center gap-4 px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 min-w-fit label">
<span class="flex gap-2 label-text">
@if ($label)
{!! $label !!}
@else
@@ -10,8 +10,9 @@
<x-helper :helper="$helper" />
@endif
</span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</label>
<span class="flex-grow"></span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</div>

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

@@ -2,38 +2,42 @@
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
@if (
$server->proxyType() !== 'NONE' &&
$server->isFunctional() &&
!$server->isSwarmWorker() &&
!$server->settings->is_build_server)
<livewire:server.proxy.status :server="$server" />
@endif
</div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main">
<a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}"
href="{{ route('server.show', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Private Key</button>
</a>
@if (!$server->isSwarmWorker())
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
@@ -42,7 +46,7 @@
@endif
<div class="flex-1"></div>
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker() && !$server->settings->is_build_server)
<livewire:server.proxy.deploy :server="$server" />
@endif
</nav>

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-300 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-300 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-center"
: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-medium leading-none 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-none opacity-70" 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