Compare commits

...

67 Commits

Author SHA1 Message Date
Andras Bacsai
1c90f46f2a Merge pull request #1572 from coollabsio/next
v4.0.0-beta.173
2023-12-27 17:44:07 +01:00
Andras Bacsai
e78d851c85 fix: button title 2023-12-27 17:14:18 +01:00
Andras Bacsai
52d05005ed fix: deploy instead of restart in case swarm is used 2023-12-27 17:12:09 +01:00
Andras Bacsai
f03aa57758 fix: routing, switch back to old one 2023-12-27 16:45:01 +01:00
Andras Bacsai
8c20c833ba fix: add source commit to final envs 2023-12-27 13:06:59 +01:00
Andras Bacsai
2fe6766b7f fix: cpu limit to float from int 2023-12-27 13:01:57 +01:00
Andras Bacsai
3f453ba7c0 Merge branch 'main' into next 2023-12-21 14:09:13 +01:00
Andras Bacsai
bd02c3055a update readme 2023-12-21 14:08:39 +01:00
andrasbacsai
7ea7d85d15 Deploying to main from @ coollabsio/coolify@d38350c282 🚀 2023-12-21 12:58:10 +00:00
Andras Bacsai
d38350c282 update 2023-12-21 13:57:52 +01:00
andrasbacsai
a83c70004c Deploying to main from @ coollabsio/coolify@49e1404a2c 🚀 2023-12-21 12:56:00 +00:00
Andras Bacsai
49e1404a2c test gh auto sponsor update 2023-12-21 13:55:40 +01:00
Andras Bacsai
76f23e7dbf do not check server ready on server status job 2023-12-21 10:43:39 +01:00
Andras Bacsai
ad8653f54d Merge pull request #1564 from coollabsio/next
v4.0.0-beta.172
2023-12-21 10:35:40 +01:00
Andras Bacsai
8939d77051 fix 2023-12-21 10:28:02 +01:00
Andras Bacsai
37be4a1796 fix 2023-12-21 10:00:41 +01:00
Andras Bacsai
e4c923e358 fix 2023-12-21 09:57:39 +01:00
Andras Bacsai
62ca3ffaa5 fix 2023-12-21 09:55:16 +01:00
Andras Bacsai
9af3ce4be5 fail job instead of runtime exception 2023-12-21 09:49:18 +01:00
Andras Bacsai
fe143ef8a5 Merge pull request #1563 from coollabsio/next
v4.0.0-beta.171
2023-12-21 09:35:43 +01:00
Andras Bacsai
5fb5845e90 redirect false on some urls 2023-12-21 09:33:11 +01:00
Andras Bacsai
794cfbd8eb execute handle on containerstatusjob 2023-12-21 09:28:47 +01:00
Andras Bacsai
29ee9915f3 fix: check proxy after mount on server view
fix: change realtime console log
version++
2023-12-21 09:28:39 +01:00
Andras Bacsai
331d485213 Merge pull request #1562 from coollabsio/next
v4.0.0-beta.170
2023-12-21 08:49:22 +01:00
Andras Bacsai
665e3761c4 fix: stay tuned 2023-12-21 08:48:53 +01:00
Andras Bacsai
ac19f0e34f enable docker image swarms 2023-12-21 08:46:48 +01:00
Andras Bacsai
d7cfa0578f Comment out handle() method call in
ContainerStatusJob constructor
2023-12-20 20:01:45 +01:00
Andras Bacsai
694169bb84 fix: why?! 2023-12-20 18:09:01 +01:00
Andras Bacsai
ab853cac87 Merge pull request #1560 from coollabsio/next
v4.0.0-beta.169
2023-12-20 16:21:06 +01:00
Andras Bacsai
51db2f797d fix: storage error on dbs 2023-12-20 16:19:48 +01:00
Andras Bacsai
0c90d3d0a1 fix: docker compose apps env rewritten 2023-12-20 16:15:13 +01:00
Andras Bacsai
51efe23690 Merge pull request #1559 from coollabsio/next
disable db + service deployments swarm
2023-12-20 14:46:12 +01:00
Andras Bacsai
e3ee84105c disable db + service deployments swarm 2023-12-20 14:45:47 +01:00
Andras Bacsai
6cbd61ac6c Merge pull request #1558 from coollabsio/next
fix: get swarm service logs
2023-12-20 14:25:53 +01:00
Andras Bacsai
638d0c8c99 fix: get swarm service logs 2023-12-20 14:11:50 +01:00
Andras Bacsai
aecc81fe9d Merge pull request #1557 from coollabsio/next
v4.0.0-beta.166
2023-12-20 13:21:45 +01:00
Andras Bacsai
c9a1437870 Fix handle method in ServerStatusJob 2023-12-20 12:33:58 +01:00
Andras Bacsai
66b41b3d4c Update ServerStatusJob middleware and uniqueId()
method
2023-12-20 12:33:21 +01:00
Andras Bacsai
c41cfe2a2f Fix server status check and cleanup logic 2023-12-20 12:32:46 +01:00
Andras Bacsai
5f2ad56529 Update container and server status job 2023-12-20 12:25:14 +01:00
Andras Bacsai
cd842bc1b2 Update number of tries in ContainerStatusJob 2023-12-20 12:13:34 +01:00
Andras Bacsai
27b6aad53a fix 2023-12-20 11:59:53 +01:00
Andras Bacsai
64b58b7661 hm 2023-12-20 11:59:06 +01:00
Andras Bacsai
94960d96a9 add max horizon processes 2023-12-20 11:47:51 +01:00
Andras Bacsai
2549244f97 hm 2023-12-20 11:44:46 +01:00
Andras Bacsai
5bfffce33b hm 2023-12-20 11:37:04 +01:00
Andras Bacsai
3a4f19f368 version++ 2023-12-20 11:22:10 +01:00
Andras Bacsai
50e17ed932 fix: server ready 2023-12-20 11:21:17 +01:00
Andras Bacsai
a8fcd7aee4 Merge pull request #1556 from coollabsio/next
small UI fixes
2023-12-20 10:31:59 +01:00
Andras Bacsai
87036cc49b link 2023-12-20 10:27:39 +01:00
Andras Bacsai
e48842c6ec fix: swarm support ui 2023-12-20 10:19:21 +01:00
Andras Bacsai
b9e405c497 Merge pull request #1555 from coollabsio/next
v4.0.0-beta.165
2023-12-19 21:01:40 +01:00
Andras Bacsai
f75effe022 fix 2023-12-19 15:44:41 +01:00
Andras Bacsai
a745f568f3 gh actions update 2023-12-19 15:44:01 +01:00
Andras Bacsai
e2e3ad0358 get branchname gh actions 2023-12-19 15:41:53 +01:00
Andras Bacsai
ba769f5fb7 fix 2023-12-19 15:36:59 +01:00
Andras Bacsai
0126286731 fix: server update schedule 2023-12-19 15:16:08 +01:00
Andras Bacsai
7952202435 fix: do not autovalidate server on mount 2023-12-19 14:19:23 +01:00
Andras Bacsai
798acb8ee5 add swarm server grouping
fixes for swarm
2023-12-19 13:47:12 +01:00
Andras Bacsai
ef595dd4c2 fix: server not found 2023-12-19 12:24:43 +01:00
Andras Bacsai
70c662daf8 disable swarm for the next release 2023-12-18 17:13:22 +01:00
Andras Bacsai
8ae385b9f9 fix: add alpha to swarm 2023-12-18 14:34:04 +01:00
Andras Bacsai
802a0f7684 fix: do not push dockerimage 2023-12-18 14:17:48 +01:00
Andras Bacsai
62c38c9859 wip: swarm 2023-12-18 14:01:25 +01:00
Andras Bacsai
27c36bec83 feat: custom docker compose commands 2023-12-17 20:56:12 +01:00
Andras Bacsai
c6b8eabe10 wip: swarm 2023-12-15 15:48:01 +01:00
Andras Bacsai
967fca9eca version ++ 2023-12-15 15:17:49 +01:00
117 changed files with 1895 additions and 712 deletions

View File

@@ -2,7 +2,7 @@ name: Development Build (v4)
on:
push:
branches: ["next"]
branches-ignore: ["main", "v3"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
@@ -29,51 +29,51 @@ jobs:
file: docker/prod-ssu/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
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:
context: .
file: docker/prod-ssu/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-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:
context: .
file: docker/prod-ssu/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-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 }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
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 }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -17,6 +17,39 @@ https://coolify.io/sponsorships
Thank you so much!
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="appwrite logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
## Github Sponsors ($15+)
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>
<a href="https://github.com/whitesidest"><img src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4" width="60px" alt="Tyler Whitesides" /></a>
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
<a href="https://github.com/Illyism"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://github.com/cccareers"><img src="https://github.com/cccareers.png" width="60px" alt="Creating Coding Careers" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
@@ -59,34 +92,6 @@ 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>
# 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
## Organizations
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="appwrite logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
Support this project with your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
# 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

@@ -56,7 +56,7 @@ class StartMariadb
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -56,7 +56,7 @@ class StartMysql
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -66,7 +66,7 @@ class StartPostgresql
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -65,7 +65,7 @@ class StartRedis
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -21,7 +21,10 @@ class CheckProxy
$status = getContainerStatus($server, 'coolify-proxy_traefik');
$server->proxy->set('status', $status);
$server->save();
return false;
if ($status === 'running') {
return false;
}
return true;
} else {
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {

View File

@@ -56,16 +56,20 @@ 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);
} else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false);
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
}
}
private function instance_auto_update($schedule)
{

View File

@@ -47,7 +47,7 @@ class Handler extends ExceptionHandler
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
return response()->json(['message' => $exception->getMessage()], 401);
}
return redirect()->guest($exception->redirectTo() ?? route('login'));
return redirect()->guest($exception->redirectTo() ?? route('login'));
}
/**
* Register the exception handling callbacks for the application.

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -16,7 +17,7 @@ class DecideWhatToDoWithUser
}
if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
return redirect()->route('boarding');
}
return $next($request);
}
@@ -24,27 +25,27 @@ class DecideWhatToDoWithUser
if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) {
return $next($request);
}
return redirect('/verify');
return redirect()-route('verify.email');
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('subscription');
return redirect()->route('subscription');
}
}
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('boarding');
return redirect()->route('boarding');
}
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect('/');
return redirect(RouteServiceProvider::HOME);
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
return redirect('/');
return redirect(RouteServiceProvider::HOME);
}
return $next($request);
}

View File

@@ -75,6 +75,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $docker_compose_custom_start_command = null;
private ?string $docker_compose_custom_build_command = null;
private ?string $addHosts = null;
private ?string $buildTarget = null;
private Collection $saved_outputs;
@@ -215,19 +217,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') {
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage' && !$this->application->destination->server->isSwarm()) {
$this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
[
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
]
);
}
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
@@ -299,6 +290,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"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.'"],
);
@@ -432,6 +426,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $this->application->docker_compose_location;
}
if (data_get($this->application, 'docker_compose_custom_start_command')) {
$this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command;
}
if (data_get($this->application, 'docker_compose_custom_build_command')) {
$this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command;
}
if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else {
@@ -454,7 +454,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]);
$this->save_environment_variables();
// Build new container to limit downtime.
$this->build_by_compose_file();
$this->application_deployment_queue->addLogEntry("Pulling & building required images.");
if ($this->docker_compose_custom_build_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_build_command}"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->stop_running_container(force: true);
$networkId = $this->application->uuid;
@@ -488,7 +499,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
);
}
$this->start_by_compose_file();
// Start compose file
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New container started.");
}
private function deploy_dockerfile_buildpack()
{
@@ -575,7 +596,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update()
{
if ($this->server->isSwarm()) {
// Skip this.
if ($this->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
}
$this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}")
],
);
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
@@ -674,10 +704,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
if ($this->application->destination->server->isSwarm()) {
ray("{$this->workdir}{$this->docker_compose_location}");
$this->push_to_docker_registry();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}-{$this->pull_request_id}")
],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
}
private function create_workdir()
{
@@ -914,7 +954,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => (int) $this->application->limits_cpus,
'cpus' => (float) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares,
]
@@ -941,13 +981,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'replicas' => data_get($this->application, 'swarm_replicas', 1),
'update_config' => [
'order' => 'start-first'
],
@@ -966,6 +1001,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) {
$docker_compose['services'][$this->container_name]['deploy']['placement'] = [
'constraints' => [
'node.role == worker'
]
];
}
if ($this->pull_request_id !== 0) {
$docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1;
}
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
@@ -1084,6 +1129,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
} if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) {
if (!is_null($this->commit)) {
$environment_variables->push("SOURCE_COMMIT={$this->commit}");
}
}
return $environment_variables->all();
}
@@ -1258,15 +1307,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true],
);
} else {
if ($this->docker_compose_location) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true],
);
}
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New images built.");
}

View File

@@ -17,20 +17,24 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->id;
return $this->server->uuid;
}
public function __construct(public Server $server)
@@ -40,13 +44,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
if (!$this->server->isServerReady($this->tries)) {
return 'Server is not reachable.';
};
try {
if (!$this->server->isServerReady()) {
return;
};
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
@@ -54,15 +58,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
$containerReplicates = null;
}
if (is_null($containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicase) {
$containerReplicase = format_docker_command_output_to_json($containerReplicase);
foreach ($containerReplicase as $containerReplica) {
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {

View File

@@ -17,22 +17,26 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->id;
return $this->server->uuid;
}
public function handle(): void
public function handle()
{
ray("checking server status for {$this->server->id}");
try {
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
@@ -40,7 +44,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
handleError($e);
return handleError($e);
}
}
public function cleanup(bool $notify = false): void

View File

@@ -37,7 +37,7 @@ class CheckLicense extends Component
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage());
ray($e->getMessage());
return $this->redirect('/settings/license', navigate: true);
return redirect()->route('settings.license');
}
}
}

View File

@@ -36,7 +36,7 @@ class Form extends Component
instant_remote_process(['docker network rm -f ' . $this->destination->network], $this->destination->server);
}
$this->destination->delete();
return $this->redirectRoute('dashboard', navigate: true);
return redirect()->route('dashboard');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -4,29 +4,32 @@ namespace App\Livewire\Destination\New;
use App\Models\Server;
use App\Models\StandaloneDocker as ModelsStandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class StandaloneDocker extends Component
class Docker extends Component
{
public string $name;
public string $network;
public Collection $servers;
public Server $server;
public int|null $server_id = null;
public ?int $server_id = null;
public bool $is_swarm = false;
protected $rules = [
'name' => 'required|string',
'network' => 'required|string',
'server_id' => 'required|integer'
'server_id' => 'required|integer',
'is_swarm' => 'boolean'
];
protected $validationAttributes = [
'name' => 'name',
'network' => 'network',
'server_id' => 'server'
'server_id' => 'server',
'is_swarm' => 'swarm'
];
public function mount()
@@ -43,13 +46,13 @@ class StandaloneDocker extends Component
} else {
$this->network = new Cuid2(7);
}
$this->name = Str::kebab("{$this->servers->first()->name}-{$this->network}");
$this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();
}
public function generate_name()
{
$this->server = Server::find($this->server_id);
$this->name = Str::kebab("{$this->server->name}-{$this->network}");
$this->name = str("{$this->server->name}-{$this->network}")->kebab();
}
public function submit()
@@ -57,20 +60,33 @@ class StandaloneDocker extends Component
$this->validate();
try {
$this->server = Server::find($this->server_id);
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->createNetworkAndAttachToProxy();
$this->dispatch('error', 'Network already added to this server.');
return;
if ($this->is_swarm) {
$found = $this->server->swarmDockers()->where('network', $this->network)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = SwarmDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
}
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
}
}
$this->createNetworkAndAttachToProxy();
return $this->redirectRoute('destination.show', $docker->uuid, navigate: true);
return redirect()->route('destination.show', $docker->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -13,7 +13,11 @@ class Show extends Component
public function scan()
{
$alreadyAddedNetworks = $this->server->standaloneDockers;
if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers;
} else {
$alreadyAddedNetworks = $this->server->standaloneDockers;
}
$networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
$this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';

View File

@@ -35,7 +35,7 @@ class ForcePasswordReset extends Component
if ($firstLogin) {
send_internal_notification('First login for ' . auth()->user()->email);
}
return $this->redirectRoute('dashboard', navigate: true);
return redirect()->route('dashboard');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -35,7 +35,7 @@ class Change extends Component
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return $this->redirectRoute('security.private-key.index', navigate: true);
return redirect()->route('security.private-key.index');
}
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
} catch (\Throwable $e) {

View File

@@ -67,9 +67,9 @@ class Create extends Component
'team_id' => currentTeam()->id
]);
if ($this->from === 'server') {
return $this->redirectRoute('server.create', navigate: true);
return redirect()->route('server.create');
}
return $this->redirectRoute('security.private-key.show', ['private_key_uuid' => $private_key->uuid], navigate: true);
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -27,7 +27,7 @@ class AddEmpty extends Component
'description' => $this->description,
'team_id' => currentTeam()->id,
]);
return $this->redirectRoute('project.show', $project->uuid, navigate: true);
return redirect()->route('project.show', $project->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {

View File

@@ -27,10 +27,10 @@ class AddEnvironment extends Component
'project_id' => $this->project->id,
]);
return $this->redirectRoute('project.resources', [
return redirect()->route('project.resources', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
], navigate: true);
]);
} catch (\Throwable $e) {
handleError($e, $this);
} finally {

View File

@@ -15,15 +15,15 @@ class Configuration extends Component
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return $this->redirectRoute('dashboard', navigate: true);
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return $this->redirectRoute('dashboard', navigate: true);
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return $this->redirectRoute('dashboard', navigate: true);
return redirect()->route('dashboard');
}
$this->application = $application;
$mainServer = $this->application->destination->server;

View File

@@ -64,6 +64,8 @@ class General extends Component
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -94,6 +96,8 @@ class General extends Component
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
];
public function mount()
{
@@ -109,7 +113,7 @@ class General extends Component
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels();
$this->customLabels = $this->application->parseContainerLabels();
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}
@@ -195,7 +199,8 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
ray($this->initialDockerComposeLocation, $this->application->docker_compose_location);
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$this->loadComposeFile();
}
$this->validate();

View File

@@ -60,12 +60,12 @@ class Heading extends Component
force_rebuild: false,
is_new_deployment: true,
);
return $this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
}
public function deploy(bool $force_rebuild = false)
{
@@ -73,18 +73,22 @@ class Heading extends Component
$this->dispatch('error', 'Please load a Compose file first.');
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.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
$this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
}
protected function setDeploymentUuid()
@@ -109,12 +113,12 @@ class Heading extends Component
restart_only: true,
is_new_deployment: true,
);
return $this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
}
public function restart()
{
@@ -124,11 +128,11 @@ class Heading extends Component
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
return $this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
}
}

View File

@@ -52,12 +52,12 @@ class Previews extends Component
force_rebuild: true,
pull_request_id: $pull_request_id,
);
return $this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deployment_uuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -72,10 +72,14 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh();

View File

@@ -29,12 +29,12 @@ class Rollback extends Component
commit: $commit,
force_rebuild: false,
);
return $this->redirectRoute('project.application.deployment', [
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid,
'environment_name' => $this->parameters['environment_name'],
], navigate: true);
]);
}
public function loadImages($showToast = false)

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Swarm extends Component
{
public Application $application;
public string $swarm_placement_constraints = '';
protected $rules = [
'application.swarm_replicas' => 'required',
'application.swarm_placement_constraints' => 'nullable',
'application.settings.is_swarm_only_worker_nodes' => 'required',
];
public function mount() {
if ($this->application->swarm_placement_constraints) {
$this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints);
}
}
public function instantSave() {
try {
$this->validate();
$this->application->settings->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() {
try {
$this->validate();
if ($this->swarm_placement_constraints) {
$this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints);
} else {
$this->application->swarm_placement_constraints = null;
}
$this->application->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.application.swarm');
}
}

View File

@@ -152,10 +152,10 @@ class CloneProject extends Component
}
$newService->parse();
}
return $this->redirectRoute('project.resources', [
return redirect()->route('project.resources', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
], navigate: true);
]);
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -50,9 +50,9 @@ class BackupEdit extends Component
$url = $url->withoutQueryParameter('selectedBackupId');
$url = $url->withFragment('backups');
$url = $url->getPath() . "#{$url->getFragment()}";
return $this->redirect($url,navigate: true);
return redirect($url);
} else {
return $this->redirectRoute('project.database.backups.all', $this->parameters);
return redirect()->route('project.database.backups.all', $this->parameters);
}
}

View File

@@ -23,7 +23,7 @@ class DeleteEnvironment extends Component
$environment = Environment::findOrFail($this->environment_id);
if ($environment->isEmpty()) {
$environment->delete();
return $this->redirectRoute('project.show', ['project_uuid' => $this->parameters['project_uuid']], navigate: true);
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
}
return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
}

View File

@@ -25,6 +25,6 @@ class DeleteProject extends Component
return $this->dispatch('error', 'Project has resources defined, please delete them first.');
}
$project->delete();
return $this->redirectRoute('projects', navigate: true);
return redirect()->route('projects');
}
}

View File

@@ -69,12 +69,12 @@ class DockerCompose extends Component
$service->parse(isNew: true);
return $this->redirectRoute('project.service.configuration', [
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -64,11 +64,11 @@ class DockerImage extends Component
'name' => 'docker-image-' . $application->uuid,
'fqdn' => $fqdn
]);
return $this->redirectRoute('project.application.configuration', [
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
], navigate: true);
]);
}
public function render()
{

View File

@@ -13,6 +13,6 @@ class EmptyProject extends Component
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
]);
return $this->redirectRoute('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production'], navigate: true);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
}
}

View File

@@ -151,11 +151,11 @@ class GithubPrivateRepository extends Component
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save();
return $this->redirectRoute('project.application.configuration', [
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
], navigate: true);
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -132,11 +132,11 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->name = generate_random_name($application->uuid);
$application->save();
return $this->redirectRoute('project.application.configuration', [
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
], navigate: true);
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -184,11 +184,11 @@ class PublicGitRepository extends Component
$application->fqdn = $fqdn;
$application->save();
return $this->redirectRoute('project.application.configuration', [
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
], navigate: true);
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -6,22 +6,24 @@ use App\Models\Project;
use App\Models\Server;
use Countable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public ?Server $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $allServers = [];
public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public Collection|array $allServices = [];
public bool $isDatabase = false;
public bool $includeSwarm = true;
public bool $loadingServices = true;
public bool $loading = false;
@@ -31,7 +33,7 @@ class Select extends Component
public ?string $search = null;
protected $queryString = [
'server',
'server_id',
'search'
];
@@ -53,10 +55,10 @@ class Select extends Component
public function updatedSelectedEnvironment()
{
return $this->redirectRoute('project.resources.new', [
return redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
], navigate: true);
]);
}
// public function addExistingPostgresql()
@@ -97,21 +99,45 @@ class Select extends Component
$this->loadingServices = false;
}
}
public function instantSave()
{
if ($this->includeSwarm) {
$this->servers = $this->allServers;
} else {
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
}
}
public function setType(string $type)
{
$this->type = $type;
if ($this->loading) return;
$this->loading = true;
$this->type = $type;
switch ($type) {
case 'postgresql':
case 'mysql':
case 'mariadb':
case 'redis':
case 'mongodb':
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', 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);
}
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->setServer($server);
}
// if (count($this->servers) === 1) {
// $server = $this->servers->first();
// $this->setServer($server);
// }
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
$foundServer = $this->servers->where('id', $this->server->id)->first();
if ($foundServer) {
return $this->setServer($foundServer);
}
@@ -122,6 +148,7 @@ class Select extends Component
public function setServer(Server $server)
{
$this->server_id = $server->id;
$this->server = $server;
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations';
@@ -142,5 +169,6 @@ class Select extends Component
public function loadServers()
{
$this->servers = Server::isUsable()->get();
$this->allServers = $this->servers;
}
}

View File

@@ -70,10 +70,10 @@ CMD ["nginx", "-g", "daemon off;"]
'fqdn' => $fqdn
]);
return $this->redirectRoute('project.application.configuration', [
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
], navigate: true);
]);
}
}

View File

@@ -41,7 +41,7 @@ class Application extends Component
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted successfully.');
return $this->redirectRoute('project.service.configuration', $this->parameters, navigate: true);
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -25,10 +25,10 @@ class Danger extends Component
{
try {
DeleteResourceJob::dispatchSync($this->resource);
return $this->redirectRoute('project.resources', [
return redirect()->route('project.resources', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName
], navigate: true);
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -65,7 +65,11 @@ class Show extends Component
public function delete()
{
$this->env->delete();
$this->dispatch('refreshEnvs');
try {
$this->env->delete();
$this->dispatch('refreshEnvs');
} catch (\Exception $e) {
return handleError($e);
}
}
}

View File

@@ -73,9 +73,17 @@ class GetLogs extends Component
if (!$refresh && $this->resource?->getMorphClass() === 'App\Models\Service') return;
if ($this->container) {
if ($this->showTimeStamps) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($this->server->isSwarm()) {
$sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} -t {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
}
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
if ($this->server->isSwarm()) {
$sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
}
}
if ($refresh) {
$this->outputs = '';

View File

@@ -34,7 +34,15 @@ class Logs extends Component
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($this->server->isSwarm()) {
$containers = collect([
[
'Names' => $this->resource->uuid . '_' . $this->resource->uuid,
]
]);
} else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
}
if ($containers->count() > 0) {
$containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
@@ -62,7 +70,7 @@ 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')) {

View File

@@ -2,22 +2,26 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\Application;
use Livewire\Component;
class Add extends Component
{
public $uuid;
public $parameters;
public $isSwarm = false;
public string $name;
public string $mount_path;
public string|null $host_path = null;
public ?string $host_path = null;
protected $listeners = ['clearAddStorage' => 'clear'];
protected $rules = [
public $rules = [
'name' => 'required|string',
'mount_path' => 'required|string',
'host_path' => 'string|nullable',
];
protected $listeners = ['clearAddStorage' => 'clear'];
protected $validationAttributes = [
'name' => 'name',
'mount_path' => 'mount',
@@ -27,17 +31,33 @@ class Add extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if (data_get($this->parameters, 'application_uuid')) {
$applicationUuid = $this->parameters['application_uuid'];
$application = Application::where('uuid', $applicationUuid)->first();
if (!$application) {
abort(404);
}
if ($application->destination->server->isSwarm()) {
$this->isSwarm = true;
$this->rules['host_path'] = 'required|string';
}
}
}
public function submit()
{
$this->validate();
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
try {
$this->validate($this->rules);
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
$this->dispatch('closeStorageModal');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function clear()

View File

@@ -19,7 +19,7 @@ class Delete extends Component
return;
}
$this->server->delete();
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -15,7 +15,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -25,7 +25,7 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean',
// 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -37,16 +37,13 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager',
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_swarm_worker' => 'Swarm Worker',
];
public function mount()
{
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
if (!$this->server->isFunctional()) {
$this->validateServer();
}
}
public function serverRefresh($install = true)
{

View File

@@ -43,7 +43,7 @@ class LogDrains extends Component
try {
$server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
$this->server = $server;
} catch (\Throwable $e) {

View File

@@ -22,7 +22,10 @@ class ByIp extends Component
public string $user = 'root';
public int $port = 22;
public bool $is_swarm_manager = false;
public bool $is_swarm_worker = false;
public $selected_swarm_cluster = null;
public $swarm_managers = [];
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
@@ -30,6 +33,7 @@ class ByIp extends Component
'user' => 'required|string',
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -38,12 +42,17 @@ class ByIp extends Component
'user' => 'User',
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker',
];
public function mount()
{
$this->name = generate_random_name();
$this->private_key_id = $this->private_keys->first()->id;
$this->swarm_managers = Server::isUsable()->get()->where('settings.is_swarm_manager', true);
if ($this->swarm_managers->count() > 0) {
$this->selected_swarm_cluster = $this->swarm_managers->first()->id;
}
}
public function setPrivateKey(string $private_key_id)
@@ -53,7 +62,7 @@ class ByIp extends Component
public function instantSave()
{
$this->dispatch('success', 'Application settings updated!');
// $this->dispatch('success', 'Application settings updated!');
}
public function submit()
@@ -63,7 +72,7 @@ class ByIp extends Component
if (is_null($this->private_key_id)) {
return $this->dispatch('error', 'You must select a private key');
}
$server = Server::create([
$payload = [
'name' => $this->name,
'description' => $this->description,
'ip' => $this->ip,
@@ -75,11 +84,16 @@ class ByIp extends Component
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
],
]);
];
if ($this->is_swarm_worker) {
$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;
$server->settings->save();
$server->addInitialNetwork();
return $this->redirectRoute('server.show', $server->uuid, navigate: true);
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -17,7 +17,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) {

View File

@@ -15,7 +15,7 @@ class Logs extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -20,7 +20,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -15,7 +15,6 @@ class Status extends Component
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
$this->checkProxy();
}
public function startProxyPolling()
{

View File

@@ -17,7 +17,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true);
return redirect()->route('server.all');
}
} catch (\Throwable $e) {

View File

@@ -41,7 +41,7 @@ class Change extends Component
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
if (!$this->github_app) {
return $this->redirectRoute('source.all', navigate: true);
return redirect()->route('source.all');
}
$settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
@@ -67,7 +67,7 @@ class Change extends Component
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return $this->redirectRoute($back, [
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
@@ -117,7 +117,7 @@ class Change extends Component
{
try {
$this->github_app->delete();
return $this->redirectRoute('source.all', navigate: true);
return redirect()->route('source.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -48,7 +48,7 @@ class Create extends Component
if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]);
}
return $this->redirectRoute('source.github.show', ['github_app_uuid' => $github_app->uuid], navigate: true);
return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Subscription;
use App\Models\InstanceSettings;
use App\Providers\RouteServiceProvider;
use Livewire\Component;
class Show extends Component
@@ -11,7 +12,7 @@ class Show extends Component
public bool $alreadySubscribed = false;
public function mount() {
if (!isCloud()) {
return $this->redirect('/', navigate: true);
return redirect(RouteServiceProvider::HOME);
}
$this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists();

View File

@@ -30,7 +30,7 @@ class Create extends Component
]);
auth()->user()->teams()->attach($team, ['role' => 'admin']);
refreshSession();
return $this->redirectRoute('team.index', navigate: true);
return redirect()->route('team.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -24,6 +24,6 @@ class Delete extends Component
});
refreshSession();
return $this->redirectRoute('team.index', navigate: true);
return redirect()->route('team.index');
}
}

View File

@@ -65,7 +65,7 @@ class Create extends Component
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->storage->save();
return $this->redirectRoute('team.storages.show', $this->storage->uuid, navigate: true);
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -43,7 +43,7 @@ class Form extends Component
{
try {
$this->storage->delete();
return $this->redirectRoute('team.storages.all', navigate: true);
return redirect()->route('team.storages.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

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

View File

@@ -633,7 +633,7 @@ class Application extends BaseModel
'memswap_limit' => $this->limits_memory_swap,
'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (int) $this->limits_cpus,
'cpus' => (float) $this->limits_cpus,
'cpuset' => $this->limits_cpuset,
'cpu_shares' => $this->limits_cpu_shares,
]

View File

@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -72,7 +71,7 @@ class Server extends BaseModel
static public function isUsable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true);
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false);
}
static public function destinationsByServer(string $server_id)
@@ -150,30 +149,45 @@ class Server extends BaseModel
}
return false;
}
public function isServerReady()
public function isServerReady(int $tries = 3)
{
if ($this->skipServer()) {
return false;
}
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 8;
if ($this->unreachable_count < $tries) {
$serverUptimeCheckNumber = $this->unreachable_count + 1;
}
if ($this->unreachable_count > $tries) {
$serverUptimeCheckNumber = $tries;
}
$currentTime = now()->timestamp;
$runtime = 50;
$serverUptimeCheckNumberMax = $tries;
$isReady = false;
// Run for 50 seconds max and check every 5 seconds for 8 times
while ($currentTime + $runtime > now()->timestamp) {
ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('server: ' . $this->name);
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
$result = $this->validateConnection();
if ($result) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
}
return true;
} else {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
// Reached max number of retries
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
$this->settings()->update([
'is_reachable' => false,
]);
$this->update([
'unreachable_count' => 0,
]);
if ($this->settings->is_reachable === true) {
$this->settings()->update([
'is_reachable' => false,
]);
}
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
@@ -190,23 +204,13 @@ class Server extends BaseModel
$db->update(['status' => 'exited']);
}
}
$isReady = false;
break;
}
$result = $this->validateConnection();
// ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
} else {
$this->update([
'unreachable_count' => $serverUptimeCheckNumber,
'unreachable_count' => $this->unreachable_count + 1,
]);
Sleep::for(5)->seconds();
continue;
}
$isReady = true;
break;
return false;
}
return $isReady;
}
public function getDiskUsage()
{
@@ -380,9 +384,20 @@ class Server extends BaseModel
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function isSwarmManager()
{
return data_get($this, 'settings.is_swarm_manager');
}
public function isSwarmWorker()
{
return data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection()
{
$server = Server::find($this->id);
if (!$server) {
return false;
}
if ($server->skipServer()) {
return false;
}

View File

@@ -54,7 +54,7 @@ class Revived extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
return $message;
}
public function toTelegram(): array

View File

@@ -31,9 +31,9 @@ class FortifyServiceProvider extends ServiceProvider
{
// First user (root) will be redirected to /settings instead of / on registration.
if ($request->user()->currentTeam->id === 0) {
return redirect('/settings');
return redirect()->route('settings.configuration');
}
return redirect('/');
return redirect(RouteServiceProvider::HOME);
}
});
}

View File

@@ -14,20 +14,23 @@ use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{
$containers = collect([]);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
if (!$server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
return $containers;
}
return $containers;
}

View File

@@ -15,10 +15,16 @@ function get_proxy_path()
}
function connectProxyToNetworks(Server $server)
{
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
});
if ($server->isSwarm()) {
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
});
} else {
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
});
}
// Service networks
foreach ($server->services()->get() as $service) {
$networks->push($service->networks());
@@ -41,16 +47,30 @@ function connectProxyToNetworks(Server $server)
$networks->push($network);
}
$networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
if ($server->isSwarm()) {
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
} else {
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
return $commands->flatten();
}
function generate_default_proxy_configuration(Server $server)
@@ -60,14 +80,18 @@ function generate_default_proxy_configuration(Server $server)
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
} else {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
}
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$array_of_networks = collect([]);
$networks->map(function ($network) use ($array_of_networks) {
$array_of_networks[$network] = [

View File

@@ -1330,7 +1330,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($value?->startsWith('$')) {
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
'application_id' => $resource->id,
'is_preview' => false,
])->first();
$value = Str::of(replaceVariables($value));
$key = $value;
@@ -1397,14 +1398,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$defaultValue = data_get($foundEnv, 'value');
}
$isBuildTime = data_get($foundEnv, 'is_build_time', false);
EnvironmentVariable::updateOrCreate([
'key' => $key->value(),
'application_id' => $resource->id,
], [
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
]);
if ($foundEnv) {
$foundEnv->update([
'key' => $key,
'application_id' => $resource->id,
'is_build_time' => $isBuildTime,
'value' => $defaultValue,
]);
} else {
EnvironmentVariable::create([
'key' => $key,
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
'is_preview' => false,
]);
}
}
}
}

991
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -216,7 +216,7 @@ return [
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
@@ -231,7 +231,7 @@ return [
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],

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

View File

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

View File

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

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

View File

@@ -1,17 +1,19 @@
<div class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
<a wire:navigate class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
<a wire:navigate class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
@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 wire:navigate class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
href="{{ route('project.application.deployments', $parameters) }}">
<button>Deployments</button>
</a>
@@ -19,37 +21,54 @@
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
Swarm Deployments requires a Docker Image in a Registry.
@else
<x-applications.advanced :application="$application" />
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
<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>
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
@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 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 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>
Restart
Redeploy
</button>
@if (isDev())
@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">
@@ -61,7 +80,7 @@
</svg>
Restart (new)
</button>
@endif
@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"
@@ -83,7 +102,7 @@
</svg>
Deploy
</button>
@if (isDev())
{{-- @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"
@@ -94,7 +113,7 @@
</svg>
Deploy (new)
</button>
@endif
@endif --}}
@endif
@endif
</div>

View File

@@ -1,13 +1,13 @@
<div class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('project.database.configuration') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.configuration') ? 'text-white' : '' }}"
href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button>
</a>
<a wire:navigate class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Execute Command</button>
</a>
<a wire:navigate class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button>
</a>
@@ -16,7 +16,7 @@
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a wire:navigate class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}">
<button>Backups</button>
</a>

View File

@@ -2,7 +2,7 @@
<label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit">
@if ($label)
{{ $label }}
{!! $label !!}
@else
{{ $id }}
@endif

View File

@@ -4,7 +4,7 @@
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a wire:navigate class="hover:bg-transparent" href="/">
<a class="hover:bg-transparent" href="/">
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -13,7 +13,7 @@
</a>
</li>
<li title="Servers">
<a wire:navigate class="hover:bg-transparent" href="/servers">
<a class="hover:bg-transparent" href="/servers">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -28,7 +28,7 @@
</a>
</li>
<li title="Projects">
<a wire:navigate class="hover:bg-transparent" href="/projects">
<a class="hover:bg-transparent" href="/projects">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -41,7 +41,7 @@
</a>
</li>
<li title="Command Center">
<a wire:navigate class="hover:bg-transparent" href="/command-center">
<a class="hover:bg-transparent" href="/command-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -53,7 +53,7 @@
</a>
</li>
<li title="Source">
<a wire:navigate class="hover:bg-transparent" href="{{ route('source.all') }}">
<a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
@@ -61,7 +61,7 @@
</a>
</li>
<li title="Security">
<a wire:navigate class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
@@ -70,7 +70,7 @@
</a>
</li>
<li title="Teams">
<a wire:navigate class="hover:bg-transparent" href="{{ route('team.index') }}">
<a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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" />
@@ -103,7 +103,7 @@
</a>
</li>
<li title="Profile">
<a wire:navigate class="hover:bg-transparent" href="/profile">
<a class="hover:bg-transparent" href="/profile">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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" />
@@ -116,7 +116,7 @@
@if (isInstanceAdmin())
<li title="Settings" class="mt-auto">
<a wire:navigate class="hover:bg-transparent" href="/settings">
<a class="hover:bg-transparent" href="/settings">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"

View File

@@ -13,7 +13,7 @@
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<a wire:navigate class="text-xs truncate lg:text-sm"
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resources', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
</div>
</li>

View File

@@ -10,10 +10,10 @@
</ol>
</nav>
<nav class="navbar-main">
<a wire:navigate href="{{ route('security.private-key.index') }}">
<a href="{{ route('security.private-key.index') }}">
<button>Private Keys</button>
</a>
<a wire:navigate href="{{ route('security.api-tokens') }}">
<a href="{{ route('security.api-tokens') }}">
<button>API tokens</button>
</a>
<div class="flex-1"></div>

View File

@@ -2,43 +2,48 @@
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxyType() !== 'NONE')
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.status :server="$server" />
@endif
</div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main">
<a wire:navigate 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 wire:navigate 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>
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@if (!$server->isSwarmWorker())
<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' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@endif
<div class="flex-1"></div>
<livewire:server.proxy.deploy :server="$server" />
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.deploy :server="$server" />
@endif
</nav>
</div>

View File

@@ -1,5 +1,5 @@
<div class="navbar-main" x-data>
<a wire:navigate class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
</a>

View File

@@ -1,6 +1,6 @@
@if (Str::of($status)->startsWith('running'))
<x-status.running :status="$status" />
@elseif(Str::of($status)->startsWith('restarting'))
@elseif(Str::of($status)->startsWith('restarting') || Str::of($status)->startsWith('starting'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped :status="$status" />

View File

@@ -1,7 +1,7 @@
<div class="pb-6">
<div class="flex items-end gap-2">
<h1>Team</h1>
<a wire:navigate href="/team/new"><x-forms.button>+ New Team</x-forms.button></a>
<a href="/team/new"><x-forms.button>+ New Team</x-forms.button></a>
</div>
<nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center">
@@ -14,17 +14,17 @@
</ol>
</nav>
<nav class="navbar-main">
<a wire:navigate 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 wire:navigate class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
<a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
<button>Members</button>
</a>
<a wire:navigate class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}"
href="{{ route('team.storages.all') }}">
<button>S3 Storages</button>
</a>
<a wire:navigate class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
href="{{ route('team.notifications') }}">
<button>Notifications</button>
</a>

View File

@@ -1,3 +1,3 @@
<x-layout>
<livewire:destination.new.standalone-docker :servers="$servers" :server_id="$server_id" />
<livewire:destination.new.docker :servers="$servers" :server_id="$server_id" />
</x-layout>

View File

@@ -29,14 +29,14 @@
@foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group">
@if (data_get($project, 'environments.0.name'))
<a wire:navigate class="flex flex-col flex-1 mx-6 hover:no-underline"
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="description">
{{ $project->description }}</div>
</a>
@else
<a wire:navigate class="flex flex-col flex-1 mx-6 hover:no-underline"
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="description">
@@ -44,11 +44,11 @@
</a>
@endif
<div class="flex items-center">
<a wire:navigate class="mx-4 rounded group-hover:text-white hover:no-underline"
<a class="mx-4 rounded group-hover:text-white hover:no-underline"
href="{{ route('project.resources.new', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="font-bold hover:text-warning">+ New Resource</span>
</a>
<a wire:navigate class="mx-4 rounded group-hover:text-white"
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -72,7 +72,7 @@
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@endif
@foreach ($servers as $server)
<a wire:navigate href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([
'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable,

View File

@@ -12,6 +12,7 @@
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endforeach
</x-forms.select>
{{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}}
<x-forms.button type="submit">
Save Destination
</x-forms.button>

View File

@@ -2,7 +2,7 @@
@if ($server->isFunctional())
<div class="flex items-end gap-2">
<h2>Destinations</h2>
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server->id]) }}">
<a href="{{ route('destination.new', ['server_id' => $server->id]) }}">
<x-forms.button>Add a new destination</x-forms.button>
</a>
<x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button>
@@ -11,7 +11,16 @@
<div class="flex gap-2 ">
Available for using:
@forelse ($server->standaloneDockers as $docker)
<a wire:navigate href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<a
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
<div class="">N/A</div>
@endforelse
@forelse ($server->swarmDockers as $docker)
<a
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@@ -25,7 +34,7 @@
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network)
<div>
<a wire:navigate
<a
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
<x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" />
</x-forms.button>

View File

@@ -5,6 +5,11 @@
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($application->destination->server->isSwarm())
<a :class="activeTab === 'swarm' && 'text-white'"
@click.prevent="activeTab = 'swarm'; window.location.hash = 'swarm'" href="#">Swarm
Configuration</a>
@endif
<a :class="activeTab === 'advanced' && 'text-white'"
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced</a>
@if ($application->build_pack !== 'static')
@@ -13,6 +18,7 @@
href="#">Environment
Variables</a>
@endif
@if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@@ -56,6 +62,9 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.application.general :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'swarm'" class="h-full">
<livewire:project.application.swarm :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
<livewire:project.application.advanced :application="$application" />
</div>

View File

@@ -11,7 +11,7 @@
<x-forms.button type="submit">Filter</x-forms.button>
</form>
@forelse ($deployments as $deployment)
<a wire:navigate @class([
<a @class([
'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline',
'hover:bg-coolgray-200' => data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' =>

View File

@@ -70,8 +70,11 @@
@if ($application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3>
@if ($application->destination->server->isSwarm())
<div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@if ($application->build_pack !== 'dockerimage')
<div>Docker Swarm requires the image to be available in a registry. More info <a
class="underline" href="https://coolify.io/docs/docker-registries"
target="_blank">here</a>.</div>
@endif
@else
@if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline"
@@ -126,37 +129,58 @@
</div>
@endif
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@if ($application->build_pack === 'dockercompose')
<span wire:init='loadComposeFile(true)'></span>
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
@endif
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory"
label="Publish Directory" required />
@else
<x-forms.input placeholder="/" id="application.publish_directory"
label="Publish Directory" />
@if ($application->build_pack === 'dockercompose')
<div class="flex flex-col gap-2" wire:init='loadComposeFile(true)'>
<div class="flex gap-2">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
</div>
<div class="pt-4">The following commands are for advanced use cases. Only modify them if you
know what are
you doing.</div>
<div class="flex gap-2">
<x-forms.input placeholder="docker compose build"
id="application.docker_compose_custom_build_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
label="Custom Build Command" />
<x-forms.input placeholder="docker compose up -d"
id="application.docker_compose_custom_start_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
label="Custom Start Command" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
</div>
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@endif
</div>
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory"
label="Publish Directory" required />
@else
<x-forms.input placeholder="/" id="application.publish_directory"
label="Publish Directory" />
@endif
@endif
</div>
@endif
@endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@@ -179,8 +203,10 @@
required
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@if (!$application->destination->server->isSwarm())
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@endif
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>

View File

@@ -0,0 +1,24 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Swarm Configuration</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
{{-- <div>Advanced Swarm Configuration</div> --}}
<div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.swarm_replicas" label="Replicas" required />
<x-forms.checkbox instantSave helper="If turned off, this resource will start on manager nodes too."
id="application.settings.is_swarm_only_worker_nodes" label="Only Start on Worker nodes" />
</div>
<x-forms.textarea id="swarm_placement_constraints" rows="7"
label="Custom Placement Constraints"
placeholder="placement:
constraints:
- 'node.role == worker'" />
</div>
</form>
</div>

View File

@@ -202,6 +202,18 @@
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
@if ($isDatabase)
<div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will
be activated soon. Stay tuned.</div>
@endif
{{-- @if ($isDatabase)
<div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm"
helper="Swarm clusters are excluded from this list by default. For database, services or complex compose deployments with databases to work with Swarm,
you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/swarm#database-requirements' target='_blank'>here</a>."
label="Include Swarm Clusters" />
</div>
@endif --}}
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server)
<div class="box group" wire:click="setServer({{ $server }})">
@@ -232,28 +244,30 @@
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
</div>
<div class="text-xs group-hover:text-white">
network: {{ $standaloneDocker->network }}</div>
</div>
</div>
@endforeach
@foreach ($swarmDockers as $swarmDocker)
<div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
@if ($server->isSwarm())
@foreach ($swarmDockers as $swarmDocker)
<div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
</div>
</div>
</div>
</div>
@endforeach
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server_id]) }}"
@endforeach
@else
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
</div>
<div class="text-xs group-hover:text-white">
network: {{ $standaloneDocker->network }}</div>
</div>
</div>
@endforeach
@endif
<a href="{{ route('destination.new', ['server_id' => $server_id]) }}"
class="items-center justify-center pb-10 text-center box-without-bg group bg-coollabs hover:bg-coollabs-100">
<div class="flex flex-col mx-6 ">
<div class="font-bold text-white">

View File

@@ -68,7 +68,7 @@
<div class="text-xs">{{ $application->status }}</div>
</div>
<div class="flex items-center px-4">
<a wire:navigate
<a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
@@ -115,7 +115,7 @@
<div class="text-xs">{{ $database->status }}</div>
</div>
<div class="flex items-center px-4">
<a wire:navigate
<a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"

View File

@@ -2,7 +2,7 @@
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a wire:navigate class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}">
<button><- Back</button>
</a>

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