Merge branch 'next' into feat/deployment-token

This commit is contained in:
Andras Bacsai
2024-12-09 09:16:59 +01:00
449 changed files with 21219 additions and 5891 deletions

View File

@@ -24,3 +24,4 @@ yarn-error.log
/.ssh
.ignition.json
.env.dusk.local
docker/coolify-realtime/node_modules

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- templates/service-templates.json
- templates/**
env:
GITHUB_REGISTRY: ghcr.io
@@ -41,7 +41,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -76,7 +76,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6

View File

@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:

View File

@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- templates/service-templates.json
- templates/**
env:
GITHUB_REGISTRY: ghcr.io

2
.gitignore vendored
View File

@@ -34,3 +34,5 @@ _ide_helper_models.php
scripts/load-test/*
.ignition.json
.env.dusk.local
docker/coolify-realtime/node_modules
.DS_Store

View File

@@ -1,65 +0,0 @@
tasks:
- name: Setup Spin environment and Composer dependencies
# Fix because of https://github.com/gitpod-io/gitpod/issues/16614
before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
init: |
cp .env.development.example .env &&
sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
sed -i "s#USERID=#USERID=33333#g" .env
sed -i "s#GROUPID=#GROUPID=33333#g" .env
composer install --ignore-platform-reqs
./vendor/bin/spin up -d
./vendor/bin/spin exec -u webuser coolify php artisan key:generate
./vendor/bin/spin exec -u webuser coolify php artisan storage:link
./vendor/bin/spin exec -u webuser coolify php artisan migrate:fresh --seed
cat .coolify-logo
gp sync-done spin-is-ready
- name: Install Node dependencies and run Vite
command: |
echo "Waiting for Sail environment to boot up."
gp sync-await spin-is-ready
./vendor/bin/spin exec vite npm install
./vendor/bin/spin exec vite npm run dev -- --host
- name: Laravel Queue Worker, listening to code changes
command: |
echo "Waiting for Sail environment to boot up."
gp sync-await spin-is-ready
./vendor/bin/spin exec -u webuser coolify php artisan queue:listen
ports:
- port: 5432
onOpen: ignore
name: PostgreSQL
visibility: public
- port: 5173
onOpen: ignore
visibility: public
name: Node Server for Vite
- port: 8000
onOpen: ignore
visibility: public
name: Coolify
# Configure vscode
vscode:
extensions:
- bmewburn.vscode-intelephense-client
- ikappas.composer
- ms-azuretools.vscode-docker
- ecmel.vscode-html-css
- MehediDracula.php-namespace-resolver
- wmaurer.change-case
- Equinusocio.vsc-community-material-theme
- EditorConfig.EditorConfig
- streetsidesoftware.code-spell-checker
- rangav.vscode-thunder-client
- PKief.material-icon-theme
- cierra.livewire-vscode
- lennardv.livewire-goto-updated
- bradlc.vscode-tailwindcss
- heybourn.headwind
- adrianwilczynski.alpine-js-intellisense
- amiralizadeh9480.laravel-extra-intellisense
- shufo.vscode-blade-formatter

View File

@@ -22,6 +22,9 @@ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
> [!NOTE]
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
# Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
@@ -37,21 +40,21 @@ Special thanks to our biggest sponsors!
### Special Sponsors
![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1)
![image](https://github.com/user-attachments/assets/726fb63e-c3b8-4260-b3ac-06780605ec5d)
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
@@ -60,6 +63,7 @@ Special thanks to our biggest sponsors!
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider.
## Github Sponsors ($40+)
@@ -87,7 +91,11 @@ Special thanks to our biggest sponsors!
<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://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a>
<a href="https://x.com/adithsuhas17?utm_source=coolify.io"><img src="https://github.com/adith-suhas-sv.png" width="60px" alt="Adith Suhas" /></a>
<a href="https://startupfa.me?utm_source=coolify.io"><img src="https://github.com/startupfame.png" width="60px" alt="StartupFame" /></a>
<a href="https://jonasjaeger.com?utm_source=coolify.io"><img src="https://github.com/toxin20.png" width="60px" alt="Jonas Jaeger" /></a>
<a href="https://github.com/therealjp?utm_source=coolify.io"><img src="https://github.com/therealjp.png" width="60px" alt="JP" /></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img src="https://github.com/evercam.png" width="60px" alt="Evercam" /></a>
<a href="https://web3.career/?utm_source=coolify.io"><img src="https://web3.career/favicon1.png" width="60px" alt="Web3 Career" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
@@ -121,7 +129,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
# Recognitions
<p>
@@ -138,6 +145,13 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
<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>
# Core Maintainers
| Andras Bacsai | 🏔️ Peak |
|------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="peaklabs-dev" /> |
| <a href="https://github.com/andrasbacsai"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/heyandras"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/heyandras.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> | <a href="https://github.com/peaklabs-dev"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/peaklabs_dev"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/peaklabs.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> |
# Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image")

View File

@@ -1,6 +1,6 @@
# Coolify Release Guide
This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
This guide outlines the release process for Coolify, intended for developers and those interested in understanding how Coolify releases are managed and deployed.
## Table of Contents
- [Release Process](#release-process)
@@ -19,19 +19,19 @@ This guide outlines the release process for Coolify, intended for developers and
- Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- Once ready, changes are merged from the `next` branch into the `main` branch.
- Once ready, changes are merged from the `next` branch into the `main` branch (via a pull request).
3. **Building the Release**
- After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
- After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry and Docker Hub with the specific version tag and the `latest` tag.
4. **Creating a GitHub Release**
- A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
- To make a new version publicly available, the version information on the CDN needs to be updated manually. After that the new version number will be available at [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json).
> [!NOTE]
> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated. After the CDN is updated, a discord announcement will be made in the Production Release channel.**
## Version Types
@@ -39,10 +39,10 @@ This guide outlines the release process for Coolify, intended for developers and
<summary><strong>Stable (coming soon)</strong></summary>
- **Stable**
- The production version suitable for stable, production environments (generally recommended).
- **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
- The production version suitable for stable, production environments (recommended).
- **Update Frequency:** Every 2 to 4 weeks, with more frequent possible fixes.
- **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
- **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
- **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`, `4.1.0`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -57,7 +57,7 @@ This guide outlines the release process for Coolify, intended for developers and
- The latest development version, suitable for testing the latest changes and experimenting with new features.
- **Update Frequency:** Daily or bi-weekly updates.
- **Release Size:** Smaller, more frequent releases.
- **Versioning Scheme:** TO BE DETERMINED
- **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-nightly.1`, `4.1.0-nightly.2`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
@@ -73,7 +73,7 @@ This guide outlines the release process for Coolify, intended for developers and
- **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
- **Update Frequency:** Available if we think beta testing is necessary.
- **Release Size:** Same size as stable release as it will become the next stabe release after some time.
- **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
- **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`, `4.1.0-beta.2`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -117,12 +117,15 @@ When a new version is released and a new GitHub release is created, it doesn't i
> [!IMPORTANT]
> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
## Manually Update to Specific Versions
## Manually Update/ Downgrade to Specific Versions
> [!CAUTION]
> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
> Updating to unreleased versions is not recommended and can cause issues.
To update your Coolify instance to a specific (unreleased) version, use the following command:
> [!IMPORTANT]
> Downgrading is supported but not recommended and can cause issues because of database migrations and other changes.
To update your Coolify instance to a specific version, use the following command:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s <version>

View File

@@ -2,15 +2,24 @@
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
Currently supported, maintained and updated versions:
| Version | Supported |
| ------- | ------------------ |
| > 4 | :white_check_mark: |
| 3 | :x: |
| Version | Supported | Support Status |
| ------- | ------------------ | -------------- |
| 4.x | :white_check_mark: | Active Development & Security Updates |
| < 4.0 | :x: | End of Life (no security updates) |
## Security Updates
We take security seriously. Security updates are released as soon as possible after a vulnerability is discovered and verified.
## Reporting a Vulnerability
If you have any vulnerability please report at hi@coollabs.io
If you discover a security vulnerability, please follow these steps:
1. **DO NOT** disclose the vulnerability publicly.
2. Send a detailed report to: `hi@coollabs.io`.
3. Include in your report:
- A description of the vulnerability
- Steps to reproduce the issue
- Potential impact

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Application;
use Laravel\Horizon\Contracts\JobRepository;
use Lorisleiva\Actions\Concerns\AsAction;
class IsHorizonQueueEmpty
{
use AsAction;
public function handle()
{
$hostname = gethostname();
$recent = app(JobRepository::class)->getRecent();
if ($recent) {
$running = $recent->filter(function ($job) use ($hostname) {
$payload = json_decode($job->payload);
$tags = data_get($payload, 'tags');
return $job->status != 'completed' &&
$job->status != 'failed' &&
isset($tags) &&
is_array($tags) &&
in_array('server:'.$hostname, $tags);
});
if ($running->count() > 0) {
echo 'false';
return false;
}
}
echo 'true';
return true;
}
}

View File

@@ -10,6 +10,8 @@ class StopApplication
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
try {

View File

@@ -3,7 +3,6 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -47,11 +46,7 @@ class PrepareCoolifyTask
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
call_event_data: $this->remoteProcessArgs->call_event_data,
);
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
dispatch($job)->onQueue('high');
} else {
dispatch($job);
}
$this->activity->refresh();
return $this->activity;

View File

@@ -9,6 +9,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
@@ -124,6 +125,7 @@ class RunRemoteProcess
]));
}
} catch (\Throwable $e) {
Log::error('Error calling event: '.$e->getMessage());
}
}

View File

@@ -24,7 +24,7 @@ class StartClickhouse
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -99,8 +99,8 @@ class StartClickhouse
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -16,6 +16,8 @@ class StartDatabase
{
use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;

View File

@@ -18,6 +18,8 @@ class StartDatabaseProxy
{
use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;

View File

@@ -26,7 +26,7 @@ class StartDragonfly
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -96,8 +96,8 @@ class StartDragonfly
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -27,7 +27,7 @@ class StartKeydb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -107,8 +107,8 @@ class StartKeydb
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";

View File

@@ -24,7 +24,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -101,8 +101,8 @@ class StartMariadb
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -25,8 +25,12 @@ class StartMongodb
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
}
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -117,8 +121,8 @@ class StartMongodb
];
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -24,7 +24,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -101,8 +101,8 @@ class StartMysql
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -25,7 +25,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
];
@@ -122,8 +122,8 @@ class StartPostgresql
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -25,7 +25,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -110,8 +110,8 @@ class StartRedis
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);

View File

@@ -2,7 +2,7 @@
namespace App\Actions\Database;
use App\Events\DatabaseStatusChanged;
use App\Events\DatabaseProxyStopped;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -18,6 +18,8 @@ class StopDatabaseProxy
{
use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = data_get($database, 'destination.server');
@@ -27,7 +29,11 @@ class StopDatabaseProxy
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
$database->is_public = false;
$database->save();
DatabaseStatusChanged::dispatch();
DatabaseProxyStopped::dispatch();
}
}

View File

@@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -16,6 +15,8 @@ class GetContainersStatus
{
use AsAction;
public string $jobQueue = 'high';
public $applications;
public ?Collection $containers;
@@ -107,6 +108,8 @@ class GetContainersStatus
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
} else {
$preview->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -118,6 +121,8 @@ class GetContainersStatus
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
} else {
$application->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -160,7 +165,10 @@ class GetContainersStatus
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
} else {
$database->update(['last_online_at' => now()]);
}
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
@@ -171,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
} else {
@@ -202,6 +210,8 @@ class GetContainersStatus
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
} else {
$service->update(['last_online_at' => now()]);
}
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Actions\License;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckResaleLicense
{
use AsAction;
public function handle()
{
try {
$settings = instanceSettings();
if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
// if (!$settings->resale_license) {
// return;
// }
$base_url = config('coolify.license_url');
$instance_id = config('app.id');
$data = Http::withHeaders([
'Accept' => 'application/json',
])->get("$base_url/lemon/validate", [
'license_key' => $settings->resale_license,
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
$data = Http::withHeaders([
'Accept' => 'application/json',
])->get("$base_url/lemon/activate", [
'license_key' => $settings->resale_license,
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'activated') === true) {
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
if (data_get($data, 'license_key.status') === 'active') {
throw new \Exception('Invalid license key.');
}
throw new \Exception('Cannot activate license key.');
} catch (\Throwable $e) {
$settings->update([
'resale_license' => null,
'is_resale_license_active' => false,
]);
throw $e;
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -29,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
@@ -88,6 +89,7 @@ class CheckProxy
$portsToCheck = [];
}
} catch (\Exception $e) {
Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
if (isDev()) {
if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',

View File

@@ -9,11 +9,13 @@ class CleanupDocker
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server)
{
$settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image');
$helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = [

View File

@@ -12,11 +12,11 @@ class InstallDocker
public function handle(Server $server)
{
$dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
}
$dockerVersion = '26.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {

View File

@@ -14,7 +14,7 @@ use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Arr;
use Illuminate\Support\Arr;
use Lorisleiva\Actions\Concerns\AsAction;
class ServerCheck
@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null;
$this->isSentinel = true;
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
@@ -148,7 +147,6 @@ class ServerCheck
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
}

View File

@@ -9,6 +9,8 @@ class StartLogDrain
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
@@ -169,7 +171,7 @@ Files:
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
$base_path = config('coolify.base_config_path');
$base_path = config('constants.coolify.base_config_path');
$config_path = $base_path.'/log-drains';
$fluent_bit_config = $config_path.'/fluent-bit.conf';

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -18,14 +19,19 @@ class UpdateCoolify
public function handle($manual_update = false)
{
if (isDev()) {
Sleep::for(10)->seconds();
return;
}
$settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
}
CleanupDocker::dispatch($this->server)->onQueue('high');
CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
$this->currentVersion = config('constants.coolify.version');
if (! $manual_update) {
if (! $settings->is_auto_update_enabled) {
return;
@@ -44,19 +50,7 @@ class UpdateCoolify
private function update()
{
if (isDev()) {
remote_process([
'sleep 10',
], $this->server);
return;
}
$all_servers = Server::all();
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
PullHelperImageJob::dispatch($this->server);
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);

View File

@@ -9,6 +9,8 @@ class ValidateServer
{
use AsAction;
public string $jobQueue = 'high';
public ?string $uptime = null;
public ?string $error = null;

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService
@@ -39,7 +40,8 @@ class DeleteService
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
if ($result !== 0) {
if ($result !== null && $result !== 0) {
Log::error('Error deleting volumes: '.$result);
}
}
}

View File

@@ -9,6 +9,8 @@ class RestartService
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service)
{
StopService::run($service);

View File

@@ -10,6 +10,8 @@ class StartService
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service)
{
$service->saveComposeConfigs();

View File

@@ -10,6 +10,8 @@ class StopService
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {

View File

@@ -13,7 +13,6 @@ class CleanupRedis extends Command
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');

View File

@@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command
public function handle()
{
echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources();
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
class CloudCheckSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:check-subscription';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check Cloud subscriptions';
/**
* Execute the console command.
*/
public function handle()
{
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
foreach ($activeSubscribers as $team) {
$stripeSubscriptionId = $team->subscription->stripe_subscription_id;
$stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
$stripeCustomerId = $team->subscription->stripe_customer_id;
if (! $stripeSubscriptionId) {
echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
continue;
}
$subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
if ($subscription->status === 'active') {
continue;
}
echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
}
}
}

View File

@@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
$this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}");
$this->info("Skipping team {$team->id}");
} else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
$this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,

View File

@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
@@ -31,19 +32,32 @@ class Dev extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
// https://github.com/OAI/OpenAPI-Specification/releases
$process = Process::run([
'/var/www/html/vendor/bin/openapi',
'app',
'-o',
'openapi.yaml',
'--version',
'3.1.0',
]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
// Convert YAML to JSON
$yaml = file_get_contents('openapi.yaml');
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
file_put_contents('openapi.json', $json);
echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()
{
// Generate APP_KEY if not exists
if (empty(env('APP_KEY'))) {
if (empty(config('app.key'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}

View File

@@ -187,7 +187,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
$this->mail = (new BackupSuccess($backup, $db))->toMail();
// $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':

View File

@@ -12,8 +12,8 @@ class Horizon extends Command
public function handle()
{
if (config('coolify.is_horizon_enabled')) {
$this->info('Horizon is enabled. Starting.');
if (config('constants.horizon.is_horizon_enabled')) {
$this->info('[x]: Horizon is enabled. Starting.');
$this->call('horizon');
exit(0);
} else {

View File

@@ -2,9 +2,9 @@
namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CheckHelperImageJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
@@ -12,6 +12,7 @@ use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
@@ -25,6 +26,8 @@ class Init extends Command
public function handle()
{
$this->optimize();
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
@@ -39,7 +42,6 @@ class Init extends Command
}
// Backward compatibility
$this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
$this->update_user_emails();
@@ -53,16 +55,32 @@ class Init extends Command
} else {
$this->cleanup_in_progress_application_deployments();
}
echo "[3]: Cleanup Redis keys.\n";
$this->call('cleanup:redis');
echo "[4]: Cleanup stucked resources.\n";
$this->call('cleanup:stucked-resources');
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
try {
$this->pullHelperImage();
} catch (\Throwable $e) {
//
}
if (isCloud()) {
try {
$this->pullTemplatesFromCDN();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
}
if (! isCloud()) {
try {
$this->pullTemplatesFromCDN();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
} else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
@@ -70,8 +88,8 @@ class Init extends Command
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = instanceSettings();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
if (! is_null(config('constants.coolify.autoupdate', null))) {
if (config('constants.coolify.autoupdate') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
@@ -80,18 +98,25 @@ class Init extends Command
}
}
private function disable_metrics()
private function pullHelperImage()
{
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
$server->settings->update(['is_metrics_enabled' => false]);
}
if ($server->isFunctional()) {
StopSentinel::dispatch($server);
CheckHelperImageJob::dispatch();
}
private function pullTemplatesFromCDN()
{
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
private function optimize()
{
echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
Artisan::call('optimize:clear');
Artisan::call('optimize');
}
private function update_user_emails()
@@ -175,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -203,19 +228,19 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
$version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
echo "I am alive!\n";
echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
}
}
@@ -239,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {

View File

@@ -15,7 +15,15 @@ class OpenApi extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
// https://github.com/OAI/OpenAPI-Specification/releases
$process = Process::run([
'/var/www/html/vendor/bin/openapi',
'app',
'-o',
'openapi.yaml',
'--version',
'3.1.0',
]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);

View File

@@ -12,8 +12,8 @@ class Scheduler extends Command
public function handle()
{
if (config('coolify.is_scheduler_enabled')) {
$this->info('Scheduler is enabled. Starting.');
if (config('constants.horizon.is_scheduler_enabled')) {
$this->info('[x]: Scheduler is enabled. Starting.');
$this->call('schedule:work');
exit(0);
} else {

View File

@@ -20,7 +20,10 @@ class ServicesGenerate extends Command
public function handle(): int
{
$serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml')))
$serviceTemplatesJson = collect(array_merge(
glob(base_path('templates/compose/*.yaml')),
glob(base_path('templates/compose/*.yml'))
))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
'logo' => $data->get('logo', 'svgs/coolify.png'),
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];

View File

@@ -57,7 +57,7 @@ class SyncBunny extends Command
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'AccessKey' => config('constants.bunny.storage_api_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream',
];
@@ -69,7 +69,7 @@ class SyncBunny extends Command
});
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_API_KEY'),
'AccessKey' => config('constants.bunny.api_key'),
'Accept' => 'application/json',
];
$that->info('Purging: '.$url);

View File

@@ -28,85 +28,100 @@ class Kernel extends ConsoleKernel
{
private $allServers;
private Schedule $scheduleInstance;
private InstanceSettings $settings;
private string $updateCheckFrequency;
private string $instanceTimezone;
protected function schedule(Schedule $schedule): void
{
$this->scheduleInstance = $schedule;
$this->allServers = Server::where('ip', '!=', '1.2.3.4');
$this->settings = instanceSettings();
$this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
$this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
if (validate_timezone($this->instanceTimezone) === false) {
$this->instanceTimezone = config('app.timezone');
}
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
$this->scheduleInstance->command('horizon:snapshot')->everyMinute();
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
// Server Jobs
$this->checkResources($schedule);
$this->checkResources();
$this->checkScheduledBackups($schedule);
$this->checkScheduledTasks($schedule);
$this->checkScheduledBackups();
$this->checkScheduledTasks();
$schedule->command('uploads:clear')->everyTwoMinutes();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->scheduleUpdates($schedule);
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->scheduleUpdates();
// Server Jobs
$this->checkResources($schedule);
$this->checkResources();
$this->pullImages($schedule);
$this->pullImages();
$this->checkScheduledBackups($schedule);
$this->checkScheduledTasks($schedule);
$this->checkScheduledBackups();
$this->checkScheduledTasks();
$schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
}
private function pullImages($schedule): void
private function pullImages(): void
{
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) {
$this->scheduleInstance->job(function () use ($server) {
CheckAndStartSentinelJob::dispatch($server);
})->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
$schedule->job(new CheckHelperImageJob)
->cron($this->settings->update_check_frequency)
->timezone($this->settings->instance_timezone)
$this->scheduleInstance->job(new CheckHelperImageJob)
->cron($this->updateCheckFrequency)
->timezone($this->instanceTimezone)
->onOneServer();
}
private function scheduleUpdates($schedule): void
private function scheduleUpdates(): void
{
$updateCheckFrequency = $this->settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)
->cron($updateCheckFrequency)
->timezone($this->settings->instance_timezone)
$this->scheduleInstance->job(new CheckForUpdatesJob)
->cron($this->updateCheckFrequency)
->timezone($this->instanceTimezone)
->onOneServer();
if ($this->settings->is_auto_update_enabled) {
$autoUpdateFrequency = $this->settings->auto_update_frequency;
$schedule->job(new UpdateCoolifyJob)
$this->scheduleInstance->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
->timezone($this->settings->instance_timezone)
->timezone($this->instanceTimezone)
->onOneServer();
}
}
private function checkResources($schedule): void
private function checkResources(): void
{
if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get();
@@ -115,40 +130,46 @@ class Kernel extends ConsoleKernel
} else {
$servers = $this->allServers->get();
}
// $schedule->job(new \App\Jobs\ResourcesCheck)->everyMinute()->onOneServer();
foreach ($servers as $server) {
$serverTimezone = $server->settings->server_timezone;
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
// Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
// $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
if (isCloud()) {
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
} else {
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
}
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated
$schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
}
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
$this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
}
// Cleanup multiplexed connections every hour
$schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer();
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
// Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) {
$this->scheduleInstance->job(function () use ($server) {
$server->restartContainer('coolify-sentinel');
})->daily()->onOneServer();
}
}
}
private function checkScheduledBackups($schedule): void
private function checkScheduledBackups(): void
{
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) {
@@ -166,27 +187,23 @@ class Kernel extends ConsoleKernel
if (is_null($server)) {
continue;
}
$serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
$this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
private function checkScheduledTasks($schedule): void
private function checkScheduledTasks(): void
{
$scheduled_tasks = ScheduledTask::all();
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
if ($scheduled_tasks->isEmpty()) {
return;
}
foreach ($scheduled_tasks as $scheduled_task) {
if ($scheduled_task->enabled === false) {
continue;
}
$service = $scheduled_task->service;
$application = $scheduled_task->application;
@@ -210,14 +227,13 @@ class Kernel extends ConsoleKernel
if (! $server) {
continue;
}
$serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
$this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}

View File

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

View File

@@ -7,27 +7,29 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?string $userId = null;
public $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
$userId = auth()->user()->id ?? null;
$userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): ?array
{
if ($this->userId) {
if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];

View File

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

View File

@@ -7,6 +7,7 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
class ServiceStatusChanged implements ShouldBroadcast
{
@@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast
public function __construct($userId = null)
{
if (is_null($userId)) {
$userId = auth()->user()->id ?? null;
$userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;

View File

@@ -21,17 +21,14 @@ class SshMultiplexingHelper
];
}
public static function ensureMultiplexedConnection(Server $server)
public static function ensureMultiplexedConnection(Server $server): bool
{
if (! self::isMultiplexingEnabled()) {
return;
return false;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
self::establishNewMultiplexedConnection($server);
}
return self::establishNewMultiplexedConnection($server);
}
public static function establishNewMultiplexedConnection(Server $server)
return true;
}
public static function establishNewMultiplexedConnection(Server $server): bool
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
return false;
}
return true;
}
public static function removeMuxFile(Server $server)
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) {
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($server->privateKey);
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
$ssh_command = "timeout $timeout ssh ";
if (self::isMultiplexingEnabled()) {
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -151,16 +149,17 @@ class SshMultiplexingHelper
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
}
private static function validateSshKey(string $sshKeyLocation): void
private static function validateSshKey(PrivateKey $privateKey): void
{
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
$keyLocation = $privateKey->getKeyLocation();
$checkKeyCommand = "ls $keyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
$privateKey->storeInFileSystem();
}
}

View File

@@ -70,7 +70,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/Application')
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -180,8 +181,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -284,8 +287,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -388,8 +393,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -476,8 +483,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -561,8 +570,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -612,8 +623,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -636,7 +649,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -676,6 +689,27 @@ class ApplicationsController extends Controller
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
$customNginxConfiguration = $request->custom_nginx_configuration;
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
],
], 422);
}
$customNginxConfiguration = base64_decode($customNginxConfiguration);
if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
],
], 422);
}
}
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -1247,7 +1281,8 @@ class ApplicationsController extends Controller
ref: '#/components/schemas/Application'
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1319,7 +1354,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1445,8 +1481,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -1461,7 +1499,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1500,7 +1539,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
$validationRules = [
'name' => 'string|max:255',
@@ -1512,6 +1551,7 @@ class ApplicationsController extends Controller
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
'custom_nginx_configuration' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
@@ -1530,6 +1570,25 @@ class ApplicationsController extends Controller
}
}
}
if ($request->has('custom_nginx_configuration')) {
if (! isBase64Encoded($request->custom_nginx_configuration)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
],
], 422);
}
$customNginxConfiguration = base64_decode($request->custom_nginx_configuration);
if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
],
], 422);
}
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -1550,16 +1609,33 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
$errors = [];
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$application->fqdn = $fqdn;
if (! $application->settings->is_container_label_readonly_enabled) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->custom_labels = base64_encode($customLabels);
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
$domain = trim($domain);
if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
$errors[] = 'Invalid domain: '.$domain;
}
return $domain;
});
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'One of the domain is already used.',
],
], 422);
}
$request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -1649,7 +1725,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1755,7 +1832,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1943,7 +2021,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2124,7 +2203,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2273,7 +2353,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2365,9 +2446,11 @@ class ApplicationsController extends Controller
properties: [
'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2453,7 +2536,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2527,7 +2611,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,

View File

@@ -211,8 +211,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +242,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -413,12 +414,12 @@ class DatabasesController extends Controller
}
break;
case 'standalone-mongodb':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +444,10 @@ class DatabasesController extends Controller
break;
case 'standalone-mysql':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -909,6 +911,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1016,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1220,9 +1223,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -1456,12 +1460,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1557,7 +1561,8 @@ class DatabasesController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1632,9 +1637,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1708,9 +1715,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1784,9 +1793,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',

View File

@@ -37,7 +37,7 @@ class OtherController extends Controller
)]
public function version(Request $request)
{
return response(config('version'));
return response(config('constants.coolify.version'));
}
#[OA\Get(
@@ -147,7 +147,7 @@ class OtherController extends Controller
public function feedback(Request $request)
{
$content = $request->input('content');
$webhook_url = config('coolify.feedback_discord_webhook');
$webhook_url = config('constants.webhooks.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,

View File

@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [
new OA\Response(
response: 200,
description: 'Project details',
description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response(
response: 401,
@@ -422,7 +422,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
if ($project->resource_count() > 0) {
if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}

View File

@@ -81,15 +81,8 @@ class SecurityController extends Controller
new OA\Response(
response: 200,
description: 'Get all private keys.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',

View File

@@ -426,6 +426,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
],
),
),
@@ -461,7 +462,7 @@ class ServersController extends Controller
)]
public function create_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -481,6 +482,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +514,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
return response()->json(['message' => 'Invalid proxy type.'], 422);
}
}
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
@@ -521,6 +531,8 @@ class ServersController extends Controller
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
$proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
@@ -530,7 +542,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
'type' => ProxyTypes::TRAEFIK->value,
'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]);
@@ -555,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -571,6 +586,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
],
),
),
@@ -583,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
ref: '#/components/schemas/Server'
)
),
]),
@@ -604,7 +619,7 @@ class ServersController extends Controller
)]
public function update_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -624,6 +639,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +660,16 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
$server->changeProxy($request->proxy_type, async: true);
} else {
return response()->json(['message' => 'Invalid proxy type.'], 422);
}
}
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
@@ -654,7 +680,9 @@ class ServersController extends Controller
ValidateServer::dispatch($server);
}
return response()->json(serializeApiResponse($server))->setStatusCode(201);
return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201);
}
#[OA\Delete(

View File

@@ -110,13 +110,19 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function accept_invitation()
public function acceptInvitation()
{
$resetPassword = request()->query('reset-password');
$invitationUuid = request()->route('uuid');
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
if (Auth::id() !== $user->id) {
abort(400, 'You are not allowed to accept this invitation.');
}
$invitationValid = $invitation->isValid();
if ($invitationValid) {
if ($resetPassword) {
$user->update([
@@ -131,14 +137,12 @@ class Controller extends BaseController
}
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
if (auth()->user()?->id !== $user->id) {
return redirect()->route('login');
}
refreshSession($invitation->team);
return redirect()->route('team.index');
} else {
abort(401);
abort(400, 'Invitation expired.');
}
}
@@ -146,10 +150,10 @@ class Controller extends BaseController
{
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
if (is_null(auth()->user())) {
if (is_null(Auth::user())) {
return redirect()->route('login');
}
if (auth()->user()->id !== $user->id) {
if (Auth::id() !== $user->id) {
abort(401);
}
$invitation->delete();

View File

@@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
'name' => $slug,
'name' => "github-app-{$slug}",
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,

View File

@@ -33,6 +33,7 @@ class Gitlab extends Controller
return;
}
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
@@ -48,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads);
}
if (empty($x_gitlab_token)) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Invalid signature.',
]);
return response($return_payloads);
}
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');

View File

@@ -3,23 +3,26 @@
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Subscription;
use App\Models\Team;
use App\Jobs\StripeProcessJob;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Stripe extends Controller
{
protected $webhook;
public function events(Request $request)
{
try {
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf();
$data = [
@@ -35,276 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
return;
return response('Webhook received. Cool cool cool cool cool.', 200);
}
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
$webhook = Webhook::create([
$this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
StripeProcessJob::dispatch($event);
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
// send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
// send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
// send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
return response("No subscription found for customer: {$customerId}", 400);
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
$team = data_get($subscription, 'team');
if (! $team) {
// send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
return response('No team found in Coolify.');
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
// send_internal_notification('Invoice payment failed: '.$customerId);
} else {
// send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
if ($subscription->stripe_invoice_paid) {
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
return response("Subscription already exists for customer: {$customerId}", 200);
}
return response('No team id or user id found', 400);
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
return response("Subscription already exists for team: {$teamId}", 200);
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
return response('Subscription created');
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
// send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
return response('Subscription incomplete expired', 200);
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
return response('No subscription and team id found', 400);
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
}
}
break;
case 'customer.subscription.deleted':
// End subscription
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if ($team) {
$team->trialEnded();
}
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
]);
// send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
break;
case 'customer.subscription.trial_will_end':
// Not used for now
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (! $team) {
return response('No team found for subscription: '.$subscription->id, 400);
}
SubscriptionTrialEndsSoonJob::dispatch($team);
break;
case 'customer.subscription.paused':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (! $team) {
return response('No team found for subscription: '.$subscription->id, 400);
}
$team->trialEnded();
$subscription->update([
'stripe_trial_already_ended' => true,
'stripe_invoice_paid' => false,
]);
SubscriptionTrialEndedJob::dispatch($team);
// send_internal_notification('Subscription paused for customer: '.$customerId);
break;
default:
// Unhandled event type
}
return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
if ($type !== 'payment_intent.payment_failed') {
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
}
$webhook->update([
$this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);

View File

@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
private bool $disableBuildCache = false;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(int $application_deployment_queue_id)
{
$this->onQueue('high');
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
@@ -176,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
if ($this->disableBuildCache) {
$this->force_rebuild = true;
}
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -225,6 +233,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
public function tags(): array
{
return ['server:'.gethostname()];
}
public function handle(): void
{
$this->application_deployment_queue->update([
@@ -344,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
GetContainersStatus::dispatch($this->server)->onQueue('high');
// dispatch(new ContainerStatusJob($this->server));
GetContainersStatus::dispatch($this->server);
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
@@ -457,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
$services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
@@ -1318,7 +1330,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$helperImage = config('constants.coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
@@ -1836,7 +1848,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->pull_request_id === 0) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
$custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options);
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
if (! $this->application->settings->custom_internal_name) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
@@ -1970,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->disableBuildCache) {
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
}
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {
@@ -1990,22 +2005,11 @@ COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode('server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}');
} else {
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
@@ -2068,23 +2072,11 @@ WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode('server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}');
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
@@ -2417,7 +2409,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
//$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}

View File

@@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
) {}
) {
$this->onQueue('high');
}
public function handle()
{

View File

@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version');
$current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available

View File

@@ -3,14 +3,15 @@
namespace App\Jobs;
use App\Models\TeamInvitation;
use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
@@ -18,34 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
public function __construct() {}
// public function uniqueId(): string
// {
// return $this->container_name;
// }
public function middleware(): array
{
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
}
public function handle(): void
{
try {
// $this->cleanup_waitlist();
$this->cleanupInvitationLink();
} catch (\Throwable $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
}
try {
$this->cleanup_invitation_link();
} catch (\Throwable $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
}
}
private function cleanup_waitlist()
{
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
foreach ($waitlist as $item) {
$item->delete();
}
}
private function cleanup_invitation_link()
private function cleanupInvitationLink()
{
$invitation = TeamInvitation::all();
foreach ($invitation as $item) {

View File

@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
public bool $ignore_errors,
public $call_event_on_finish,
public $call_event_data,
) {}
) {
$this->onQueue('high');
}
/**
* Execute the job.

View File

@@ -60,12 +60,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
$this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
{
try {
$databasesToBackup = null;
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -197,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
if (is_null($databasesToBackup)) {
if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -304,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
//$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
@@ -319,12 +321,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null,
]);
}
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
if ($this->team) {
@@ -524,7 +524,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string
{
$settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";

View File

@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public bool $deleteVolumes = true,
public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true
) {}
) {
$this->onQueue('high');
}
public function handle()
{
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();

View File

@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
@@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void

View File

@@ -16,11 +16,14 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function __construct(public Server $server) {}
public function __construct(public Server $server)
{
$this->onQueue('high');
}
public function handle(): void
{
$helperImage = config('coolify.helper_image');
$helperImage = config('constants.coolify.helper_image');
$latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}

View File

@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10;
public function __construct() {}
public function __construct()
{
$this->onQueue('high');
}
public function handle(): void
{

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Events\ScheduledTaskDone;
use App\Models\Application;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
@@ -19,7 +20,7 @@ class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?Team $team = null;
public Team $team;
public Server $server;
@@ -39,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue
public function __construct($task)
{
$this->onQueue('high');
$this->task = $task;
if ($service = $task->service()->first()) {
$this->resource = $service;
@@ -47,7 +50,7 @@ class ScheduledTaskJob implements ShouldQueue
} else {
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
$this->team = Team::findOrFail($task->team_id);
$this->server_timezone = $this->getServerTimezone();
}
@@ -125,6 +128,7 @@ class ScheduledTaskJob implements ShouldQueue
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
ScheduledTaskDone::dispatch($this->team->id);
}
}
}

View File

@@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public DiscordMessage $message,
public string $webhookUrl
) {}
) {
$this->onQueue('high');
}
/**
* Execute the job.

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToSlackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private SlackMessage $message,
private string $webhookUrl
) {
$this->onQueue('high');
}
public function handle(): void
{
Http::post($this->webhookUrl, [
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'plain_text',
'text' => 'Coolify Notification',
],
],
],
'attachments' => [
[
'color' => $this->message->color,
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => $this->message->title,
],
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $this->message->description,
],
],
],
],
],
]);
}
}

View File

@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token,
public string $chatId,
public ?string $topicId = null,
) {}
) {
$this->onQueue('high');
}
/**
* Execute the job.
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
}
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
}
}
}

View File

@@ -13,6 +13,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,7 +26,17 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public $containers;
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server)
{
if (isDev()) {
$this->handle();
}
}
public function handle()
{

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Actions\Server\ResourcesCheck;
use App\Actions\Server\ServerCheck;
use App\Models\Server;
use Illuminate\Bus\Queueable;
@@ -25,6 +26,7 @@ class ServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue
{
try {
ServerCheck::run($this->server);
ResourcesCheck::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e);
}

View File

@@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {}
public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
{
$this->onQueue('high');
}
public function handle()
{

View File

@@ -30,8 +30,7 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
$limit = data_get($this->team->limits, 'serverLimit', 2);
$number_of_servers_to_disable = $servers_count - $limit;
$number_of_servers_to_disable = $servers_count - $this->team->limits;
if ($number_of_servers_to_disable > 0) {
$servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable);

View File

@@ -25,7 +25,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
public function __construct(public Server $server, public ?int $percentage = null) {}
public function __construct(public Server $server, public int|string|null $percentage = null) {}
public function handle()
{

View File

@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public LocalFileVolume $localFileVolume) {}
public function __construct(public LocalFileVolume $localFileVolume)
{
$this->onQueue('high');
}
public function handle()
{

View File

@@ -0,0 +1,246 @@
<?php
namespace App\Jobs;
use App\Models\Subscription;
use App\Models\Team;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Str;
class StripeProcessJob implements ShouldQueue
{
use Queueable;
public $type;
public $webhook;
public $tries = 3;
public function __construct(public $event)
{
$this->onQueue('high');
}
public function handle(): void
{
try {
$excludedPlans = config('subscription.stripe_excluded_plans');
$type = data_get($this->event, 'type');
$this->type = $type;
$data = data_get($this->event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
}
throw new \RuntimeException('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired');
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
send_internal_notification('No subscription and team id found');
throw new \RuntimeException('No subscription and team id found');
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
}
}
break;
case 'customer.subscription.deleted':
// End subscription
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
$team?->subscriptionEnded();
break;
default:
throw new \RuntimeException("Unhandled event type: {$type}");
}
} catch (\Exception $e) {
send_internal_notification('StripeProcessJob error: '.$e->getMessage());
}
}
}

View File

@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Team $team) {}
public function __construct(protected Team $team)
{
$this->onQueue('high');
}
public function handle()
{

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage;
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to '.$member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage;
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to '.$member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 600;
public function __construct()
{
$this->onQueue('high');
}
public function handle(): void
{
try {

View File

@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();

View File

@@ -2,8 +2,10 @@
namespace App\Livewire\Admin;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
@@ -23,7 +25,7 @@ class Index extends Component
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0) {
if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
@@ -41,23 +43,19 @@ class Index extends Component
public function getSubscribers()
{
$this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->count();
$this->activeSubscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->count();
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
public function switchUser(int $user_id)
{
if (auth()->user()->id !== 0) {
if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
auth()->login($user);
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));

View File

@@ -66,11 +66,15 @@ class Index extends Component
public bool $serverReachable = true;
public ?string $minDockerVersion = null;
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard');
}
$this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
@@ -168,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {
// $this->currentState = 'select-proxy';
// return;
// }
$this->getProjects();
}
@@ -185,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}

View File

@@ -6,7 +6,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -18,16 +18,16 @@ class Docker extends Component
#[Locked]
public Server $selectedServer;
#[Rule(['required', 'string'])]
#[Validate(['required', 'string'])]
public string $name;
#[Rule(['required', 'string'])]
#[Validate(['required', 'string'])]
public string $network;
#[Rule(['required', 'string'])]
#[Validate(['required', 'string'])]
public string $serverId;
#[Rule(['required', 'boolean'])]
#[Validate(['required', 'boolean'])]
public bool $isSwarm = false;
public function mount(?string $server_id = null)
@@ -35,9 +35,11 @@ class Docker extends Component
$this->network = new Cuid2;
$this->servers = Server::isUsable()->get();
if ($server_id) {
$this->selectedServer = $this->servers->find($server_id);
$this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
$this->serverId = $this->selectedServer->id;
} else {
$this->selectedServer = $this->servers->first();
$this->serverId = $this->selectedServer->id;
}
$this->generateName();
}

View File

@@ -6,7 +6,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Show extends Component
@@ -14,13 +14,13 @@ class Show extends Component
#[Locked]
public $destination;
#[Rule(['string', 'required'])]
#[Validate(['string', 'required'])]
public string $name;
#[Rule(['string', 'required'])]
#[Validate(['string', 'required'])]
public string $network;
#[Rule(['string', 'required'])]
#[Validate(['string', 'required'])]
public string $serverIp;
public function mount(string $destination_uuid)

View File

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

View File

@@ -5,17 +5,17 @@ namespace App\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\Rule;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Help extends Component
{
use WithRateLimiting;
#[Rule(['required', 'min:10', 'max:1000'])]
#[Validate(['required', 'min:10', 'max:1000'])]
public string $description;
#[Rule(['required', 'min:3'])]
#[Validate(['required', 'min:3'])]
public string $subject;
public function submit()

View File

@@ -31,7 +31,7 @@ class NavbarDeleteTeam extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === auth()->user()->id) {
if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);

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