2023-05-24 12:26:50 +00:00
< ? php
2024-06-11 10:37:39 +00:00
use App\Enums\ApplicationDeploymentStatus ;
2024-08-23 18:54:38 +00:00
use App\Enums\ProxyTypes ;
2024-03-13 17:26:30 +00:00
use App\Jobs\ServerFilesFromServerJob ;
2023-10-20 12:51:01 +00:00
use App\Models\Application ;
2024-06-12 09:31:14 +00:00
use App\Models\ApplicationDeploymentQueue ;
2024-08-23 18:54:38 +00:00
use App\Models\ApplicationPreview ;
use App\Models\EnvironmentVariable ;
2024-11-12 10:32:18 +00:00
use App\Models\GithubApp ;
2026-03-11 14:30:46 +00:00
use App\Models\GitlabApp ;
2023-06-12 10:00:01 +00:00
use App\Models\InstanceSettings ;
2023-11-24 14:48:23 +00:00
use App\Models\LocalFileVolume ;
use App\Models\LocalPersistentVolume ;
2023-09-28 08:53:00 +00:00
use App\Models\Server ;
2023-10-20 12:51:01 +00:00
use App\Models\Service ;
2023-11-24 14:48:23 +00:00
use App\Models\ServiceApplication ;
2024-08-23 18:54:38 +00:00
use App\Models\ServiceDatabase ;
2024-04-10 13:00:46 +00:00
use App\Models\StandaloneClickhouse ;
use App\Models\StandaloneDragonfly ;
use App\Models\StandaloneKeydb ;
2023-10-25 08:43:07 +00:00
use App\Models\StandaloneMariadb ;
2023-10-20 12:51:01 +00:00
use App\Models\StandaloneMongodb ;
2023-10-25 08:43:07 +00:00
use App\Models\StandaloneMysql ;
2023-10-20 12:51:01 +00:00
use App\Models\StandalonePostgresql ;
use App\Models\StandaloneRedis ;
2023-08-16 14:03:30 +00:00
use App\Models\Team ;
2023-09-14 16:22:08 +00:00
use App\Models\User ;
2024-10-31 15:47:08 +00:00
use Carbon\CarbonImmutable ;
2023-08-16 14:03:30 +00:00
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException ;
2024-01-30 08:48:51 +00:00
use Illuminate\Database\UniqueConstraintViolationException ;
2024-04-25 11:52:52 +00:00
use Illuminate\Process\Pool ;
2024-04-16 18:57:54 +00:00
use Illuminate\Support\Collection ;
2024-11-04 13:18:16 +00:00
use Illuminate\Support\Facades\Auth ;
2023-09-15 09:19:36 +00:00
use Illuminate\Support\Facades\Cache ;
2023-10-04 12:40:26 +00:00
use Illuminate\Support\Facades\File ;
2025-12-12 13:12:02 +00:00
use Illuminate\Support\Facades\Hash ;
2023-05-24 12:26:50 +00:00
use Illuminate\Support\Facades\Http ;
2024-04-25 11:52:52 +00:00
use Illuminate\Support\Facades\Process ;
2024-10-25 13:13:23 +00:00
use Illuminate\Support\Facades\RateLimiter ;
2023-12-11 19:01:54 +00:00
use Illuminate\Support\Facades\Request ;
2023-05-24 12:26:50 +00:00
use Illuminate\Support\Facades\Route ;
2024-06-26 11:32:36 +00:00
use Illuminate\Support\Facades\Validator ;
2023-05-24 12:26:50 +00:00
use Illuminate\Support\Str ;
2023-09-27 10:45:53 +00:00
use Illuminate\Support\Stringable ;
2025-01-10 17:27:48 +00:00
use Laravel\Horizon\Contracts\JobRepository ;
2024-02-28 12:48:39 +00:00
use Lcobucci\JWT\Encoding\ChainedFormatter ;
use Lcobucci\JWT\Encoding\JoseEncoder ;
use Lcobucci\JWT\Signer\Hmac\Sha256 ;
2024-06-10 20:43:34 +00:00
use Lcobucci\JWT\Signer\Key\InMemory ;
2024-02-28 12:48:39 +00:00
use Lcobucci\JWT\Token\Builder ;
2024-04-03 11:45:49 +00:00
use phpseclib3\Crypt\EC ;
2023-08-22 15:44:49 +00:00
use phpseclib3\Crypt\RSA ;
2024-06-10 20:43:34 +00:00
use Poliander\Cron\CronExpression ;
use PurplePixie\PhpDns\DNSQuery ;
2023-09-22 12:47:25 +00:00
use Spatie\Url\Url ;
2023-11-24 14:48:23 +00:00
use Symfony\Component\Yaml\Yaml ;
2024-06-10 20:43:34 +00:00
use Visus\Cuid2\Cuid2 ;
2023-05-24 12:26:50 +00:00
2023-09-21 15:48:31 +00:00
function base_configuration_dir () : string
{
return '/data/coolify' ;
}
2023-08-09 13:57:53 +00:00
function application_configuration_dir () : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . '/applications' ;
2023-09-21 15:48:31 +00:00
}
function service_configuration_dir () : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . '/services' ;
2023-08-09 12:44:36 +00:00
}
2023-08-09 13:57:53 +00:00
function database_configuration_dir () : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . '/databases' ;
2023-08-09 12:44:36 +00:00
}
2023-09-07 11:23:34 +00:00
function database_proxy_dir ( $uuid ) : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . " /databases/ $uuid /proxy " ;
2023-09-07 11:23:34 +00:00
}
2023-08-09 13:57:53 +00:00
function backup_dir () : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . '/backups' ;
2023-08-09 12:44:36 +00:00
}
2024-06-20 08:44:31 +00:00
function metrics_dir () : string
{
2024-12-09 14:38:21 +00:00
return base_configuration_dir () . '/metrics' ;
2024-06-20 08:44:31 +00:00
}
2023-08-09 13:57:53 +00:00
2024-12-03 14:39:24 +00:00
function sanitize_string ( ? string $input = null ) : ? string
2024-12-02 21:49:41 +00:00
{
2024-12-03 14:39:24 +00:00
if ( is_null ( $input )) {
return null ;
}
2024-12-02 21:49:41 +00:00
// Remove any HTML/PHP tags
$sanitized = strip_tags ( $input );
// Convert special characters to HTML entities
$sanitized = htmlspecialchars ( $sanitized , ENT_QUOTES | ENT_HTML5 , 'UTF-8' );
// Remove any control characters
$sanitized = preg_replace ( '/[\x00-\x1F\x7F]/u' , '' , $sanitized );
// Trim whitespace
$sanitized = trim ( $sanitized );
return $sanitized ;
2024-06-20 08:44:31 +00:00
}
2023-08-09 13:57:53 +00:00
fix: prevent command injection in Docker Compose parsing - add pre-save validation
This commit addresses a critical security issue where malicious Docker Compose
data was being saved to the database before validation occurred.
Problem:
- Service models were saved to database first
- Validation ran afterwards during parse()
- Malicious data persisted even when validation failed
- User saw error but damage was already done
Solution:
1. Created validateDockerComposeForInjection() to validate YAML before save
2. Added pre-save validation to all Service creation/update points:
- Livewire: DockerCompose.php, StackForm.php
- API: ServicesController.php (create, update, one-click)
3. Validates service names and volume paths (string + array formats)
4. Blocks shell metacharacters: backticks, $(), |, ;, &, >, <, newlines
Security fixes:
- Volume source paths (string format) - validated before save
- Volume source paths (array format) - validated before save
- Service names - validated before save
- Environment variable patterns - safe ${VAR} allowed, ${VAR:-$(cmd)} blocked
Testing:
- 60 security tests pass (176 assertions)
- PreSaveValidationTest.php: 15 tests for pre-save validation
- ValidateShellSafePathTest.php: 15 tests for core validation
- VolumeSecurityTest.php: 15 tests for volume parsing
- ServiceNameSecurityTest.php: 15 tests for service names
Related commits:
- Previous: Added validation during parse() phase
- This commit: Moves validation before database save
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 19:46:26 +00:00
/**
* Validate that a path or identifier is safe for use in shell commands .
*
* This function prevents command injection by rejecting strings that contain
* shell metacharacters or command substitution patterns .
*
* @ param string $input The path or identifier to validate
* @ param string $context Descriptive name for error messages ( e . g . , 'volume source' , 'service name' )
* @ return string The validated input ( unchanged if valid )
*
* @ throws \Exception If dangerous characters are detected
*/
function validateShellSafePath ( string $input , string $context = 'path' ) : string
{
// List of dangerous shell metacharacters that enable command injection
$dangerousChars = [
'`' => 'backtick (command substitution)' ,
'$(' => 'command substitution' ,
'${' => 'variable substitution with potential command injection' ,
'|' => 'pipe operator' ,
'&' => 'background/AND operator' ,
';' => 'command separator' ,
" \n " => 'newline (command separator)' ,
" \r " => 'carriage return' ,
2025-10-16 07:03:53 +00:00
" \t " => 'tab (token separator)' ,
fix: prevent command injection in Docker Compose parsing - add pre-save validation
This commit addresses a critical security issue where malicious Docker Compose
data was being saved to the database before validation occurred.
Problem:
- Service models were saved to database first
- Validation ran afterwards during parse()
- Malicious data persisted even when validation failed
- User saw error but damage was already done
Solution:
1. Created validateDockerComposeForInjection() to validate YAML before save
2. Added pre-save validation to all Service creation/update points:
- Livewire: DockerCompose.php, StackForm.php
- API: ServicesController.php (create, update, one-click)
3. Validates service names and volume paths (string + array formats)
4. Blocks shell metacharacters: backticks, $(), |, ;, &, >, <, newlines
Security fixes:
- Volume source paths (string format) - validated before save
- Volume source paths (array format) - validated before save
- Service names - validated before save
- Environment variable patterns - safe ${VAR} allowed, ${VAR:-$(cmd)} blocked
Testing:
- 60 security tests pass (176 assertions)
- PreSaveValidationTest.php: 15 tests for pre-save validation
- ValidateShellSafePathTest.php: 15 tests for core validation
- VolumeSecurityTest.php: 15 tests for volume parsing
- ServiceNameSecurityTest.php: 15 tests for service names
Related commits:
- Previous: Added validation during parse() phase
- This commit: Moves validation before database save
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 19:46:26 +00:00
'>' => 'output redirection' ,
'<' => 'input redirection' ,
];
// Check for dangerous characters
foreach ( $dangerousChars as $char => $description ) {
if ( str_contains ( $input , $char )) {
throw new \Exception (
" Invalid { $context } : contains forbidden character ' { $char } ' ( { $description } ). " .
'Shell metacharacters are not allowed for security reasons.'
);
}
}
2026-03-10 21:22:48 +00:00
return $input ;
}
/**
* Validate that a string is a safe git ref ( commit SHA , branch name , tag , or HEAD ) .
*
* Prevents command injection by enforcing an allowlist of characters valid for git refs .
* Valid : hex SHAs , HEAD , branch / tag names ( alphanumeric , dots , hyphens , underscores , slashes ) .
*
* @ param string $input The git ref to validate
* @ param string $context Descriptive name for error messages
* @ return string The validated input ( trimmed )
*
* @ throws \Exception If the input contains disallowed characters
*/
function validateGitRef ( string $input , string $context = 'git ref' ) : string
{
$input = trim ( $input );
if ( $input === '' || $input === 'HEAD' ) {
return $input ;
}
// Must not start with a hyphen (git flag injection)
if ( str_starts_with ( $input , '-' )) {
throw new \Exception ( " Invalid { $context } : must not start with a hyphen. " );
}
// Allow only alphanumeric characters, dots, hyphens, underscores, and slashes
if ( ! preg_match ( '/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/' , $input )) {
throw new \Exception ( " Invalid { $context } : contains disallowed characters. Only alphanumeric characters, dots, hyphens, underscores, and slashes are allowed. " );
}
fix: prevent command injection in Docker Compose parsing - add pre-save validation
This commit addresses a critical security issue where malicious Docker Compose
data was being saved to the database before validation occurred.
Problem:
- Service models were saved to database first
- Validation ran afterwards during parse()
- Malicious data persisted even when validation failed
- User saw error but damage was already done
Solution:
1. Created validateDockerComposeForInjection() to validate YAML before save
2. Added pre-save validation to all Service creation/update points:
- Livewire: DockerCompose.php, StackForm.php
- API: ServicesController.php (create, update, one-click)
3. Validates service names and volume paths (string + array formats)
4. Blocks shell metacharacters: backticks, $(), |, ;, &, >, <, newlines
Security fixes:
- Volume source paths (string format) - validated before save
- Volume source paths (array format) - validated before save
- Service names - validated before save
- Environment variable patterns - safe ${VAR} allowed, ${VAR:-$(cmd)} blocked
Testing:
- 60 security tests pass (176 assertions)
- PreSaveValidationTest.php: 15 tests for pre-save validation
- ValidateShellSafePathTest.php: 15 tests for core validation
- VolumeSecurityTest.php: 15 tests for volume parsing
- ServiceNameSecurityTest.php: 15 tests for service names
Related commits:
- Previous: Added validation during parse() phase
- This commit: Moves validation before database save
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 19:46:26 +00:00
return $input ;
}
2023-08-09 13:57:53 +00:00
function generate_readme_file ( string $name , string $updated_at ) : string
{
2024-12-02 21:49:41 +00:00
$name = sanitize_string ( $name );
$updated_at = sanitize_string ( $updated_at );
2023-08-09 13:57:53 +00:00
return " Resource name: $name\nLatest Deployment Date: $updated_at " ;
2023-08-09 12:44:36 +00:00
}
2023-08-22 15:44:49 +00:00
function isInstanceAdmin ()
2023-08-14 14:56:13 +00:00
{
2023-08-22 15:44:49 +00:00
return auth () ? -> user () ? -> isInstanceAdmin () ? ? false ;
2023-08-14 14:56:13 +00:00
}
2023-08-22 15:44:49 +00:00
function currentTeam ()
{
2024-11-04 13:18:16 +00:00
return Auth :: user () ? -> currentTeam () ? ? null ;
2023-08-22 15:44:49 +00:00
}
function showBoarding () : bool
{
2026-02-11 15:37:40 +00:00
if ( isDev ()) {
return false ;
}
2024-11-05 10:53:11 +00:00
if ( Auth :: user () ? -> isMember ()) {
2024-05-21 12:29:06 +00:00
return false ;
}
2024-06-10 20:43:34 +00:00
2023-08-27 13:44:36 +00:00
return currentTeam () -> show_boarding ? ? false ;
2023-08-22 15:44:49 +00:00
}
2023-09-08 16:33:26 +00:00
function refreshSession ( ? Team $team = null ) : void
2023-08-22 15:44:49 +00:00
{
2024-12-09 14:38:21 +00:00
if ( ! $team ) {
2024-11-04 13:18:16 +00:00
if ( Auth :: user () -> currentTeam ()) {
$team = Team :: find ( Auth :: user () -> currentTeam () -> id );
2023-09-15 09:28:44 +00:00
} else {
2024-11-04 13:18:16 +00:00
$team = User :: find ( Auth :: id ()) -> teams -> first ();
2023-09-15 09:28:44 +00:00
}
2023-09-08 16:33:26 +00:00
}
2025-12-28 13:02:41 +00:00
// Clear old cache key format for backwards compatibility
2024-12-09 14:38:21 +00:00
Cache :: forget ( 'team:' . Auth :: id ());
2025-12-28 13:02:41 +00:00
// Use new cache key format that includes team ID
Cache :: forget ( 'user:' . Auth :: id () . ':team:' . $team -> id );
Cache :: remember ( 'user:' . Auth :: id () . ':team:' . $team -> id , 3600 , function () use ( $team ) {
2023-09-08 16:33:26 +00:00
return $team ;
});
2023-08-30 14:01:38 +00:00
session ([ 'currentTeam' => $team ]);
2023-08-22 15:44:49 +00:00
}
2023-09-15 13:34:25 +00:00
function handleError ( ? Throwable $error = null , ? Livewire\Component $livewire = null , ? string $customErrorMessage = null )
{
2023-11-20 09:32:06 +00:00
if ( $error instanceof TooManyRequestsException ) {
if ( isset ( $livewire )) {
2024-03-27 10:35:57 +00:00
return $livewire -> dispatch ( 'error' , " Too many requests. Please try again in { $error -> secondsUntilAvailable } seconds. " );
2023-11-20 09:32:06 +00:00
}
2024-06-10 20:43:34 +00:00
2023-11-20 09:32:06 +00:00
return " Too many requests. Please try again in { $error -> secondsUntilAvailable } seconds. " ;
}
2024-01-30 08:48:51 +00:00
if ( $error instanceof UniqueConstraintViolationException ) {
if ( isset ( $livewire )) {
2024-02-22 13:56:41 +00:00
return $livewire -> dispatch ( 'error' , 'Duplicate entry found. Please use a different name.' );
2024-01-30 08:48:51 +00:00
}
2024-06-10 20:43:34 +00:00
return 'Duplicate entry found. Please use a different name.' ;
2024-01-30 08:48:51 +00:00
}
2023-11-20 09:32:06 +00:00
2024-10-17 19:48:47 +00:00
if ( $error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException ) {
abort ( 404 );
}
2023-09-15 13:34:25 +00:00
if ( $error instanceof Throwable ) {
$message = $error -> getMessage ();
} else {
$message = null ;
}
if ( $customErrorMessage ) {
2024-12-09 14:38:21 +00:00
$message = $customErrorMessage . ' ' . $message ;
2023-09-15 13:34:25 +00:00
}
2023-11-20 09:32:06 +00:00
2023-09-15 13:34:25 +00:00
if ( isset ( $livewire )) {
2023-12-07 18:06:32 +00:00
return $livewire -> dispatch ( 'error' , $message );
2023-09-15 13:34:25 +00:00
}
2023-11-21 11:07:06 +00:00
throw new Exception ( $message );
2023-05-24 12:26:50 +00:00
}
2023-08-09 13:57:53 +00:00
function get_route_parameters () : array
2023-05-24 12:26:50 +00:00
{
return Route :: current () -> parameters ();
}
2024-05-08 17:19:32 +00:00
function get_latest_sentinel_version () : string
{
try {
2025-11-28 14:20:13 +00:00
$response = Http :: get ( config ( 'constants.coolify.versions_url' ));
2024-05-08 17:19:32 +00:00
$versions = $response -> json ();
2024-06-18 14:43:18 +00:00
2024-10-15 12:03:10 +00:00
return data_get ( $versions , 'coolify.sentinel.version' );
2024-10-31 14:19:37 +00:00
} catch ( \Throwable ) {
2024-05-08 17:19:32 +00:00
return '0.0.0' ;
}
}
2023-08-09 13:57:53 +00:00
function get_latest_version_of_coolify () : string
2023-05-24 12:26:50 +00:00
{
2023-06-19 13:43:53 +00:00
try {
2025-11-17 13:53:28 +00:00
$versions = get_versions_data ();
2024-06-10 20:43:34 +00:00
2025-11-17 13:53:28 +00:00
return data_get ( $versions , 'coolify.v4.version' , '0.0.0' );
2023-09-15 13:34:25 +00:00
} catch ( \Throwable $e ) {
2024-06-10 20:43:34 +00:00
2023-06-19 13:43:53 +00:00
return '0.0.0' ;
}
2023-05-24 12:26:50 +00:00
}
2023-09-19 13:51:13 +00:00
function generate_random_name ( ? string $cuid = null ) : string
2023-05-24 12:26:50 +00:00
{
2023-11-13 10:44:13 +00:00
$generator = new \Nubs\RandomNameGenerator\All (
[
2024-07-24 19:11:12 +00:00
new \Nubs\RandomNameGenerator\Alliteration ,
2023-11-13 10:44:13 +00:00
]
);
2023-09-19 13:51:13 +00:00
if ( is_null ( $cuid )) {
2024-07-25 11:31:59 +00:00
$cuid = new Cuid2 ;
2023-09-19 13:51:13 +00:00
}
2024-06-10 20:43:34 +00:00
2023-08-09 13:57:53 +00:00
return Str :: kebab ( " { $generator -> getName () } - $cuid " );
2023-05-24 12:26:50 +00:00
}
2024-04-03 11:45:49 +00:00
function generateSSHKey ( string $type = 'rsa' )
{
if ( $type === 'rsa' ) {
$key = RSA :: createKey ();
2024-06-10 20:43:34 +00:00
2024-04-03 11:45:49 +00:00
return [
'private' => $key -> toString ( 'PKCS1' ),
2024-06-10 20:43:34 +00:00
'public' => $key -> getPublicKey () -> toString ( 'OpenSSH' , [ 'comment' => 'coolify-generated-ssh-key' ]),
2024-04-03 11:45:49 +00:00
];
2024-06-10 20:43:34 +00:00
} elseif ( $type === 'ed25519' ) {
2024-04-03 11:45:49 +00:00
$key = EC :: createKey ( 'Ed25519' );
2024-06-10 20:43:34 +00:00
2024-04-03 11:45:49 +00:00
return [
'private' => $key -> toString ( 'OpenSSH' ),
2024-06-10 20:43:34 +00:00
'public' => $key -> getPublicKey () -> toString ( 'OpenSSH' , [ 'comment' => 'coolify-generated-ssh-key' ]),
2024-04-03 11:45:49 +00:00
];
}
throw new Exception ( 'Invalid key type' );
2023-08-22 15:44:49 +00:00
}
2023-08-31 13:00:59 +00:00
function formatPrivateKey ( string $privateKey )
{
2023-08-22 15:44:49 +00:00
$privateKey = trim ( $privateKey );
2024-12-09 14:38:21 +00:00
if ( ! str_ends_with ( $privateKey , " \n " )) {
2023-08-22 15:44:49 +00:00
$privateKey .= " \n " ;
}
2024-06-10 20:43:34 +00:00
2023-08-22 15:44:49 +00:00
return $privateKey ;
}
2023-09-19 13:51:13 +00:00
function generate_application_name ( string $git_repository , string $git_branch , ? string $cuid = null ) : string
2023-05-24 13:47:04 +00:00
{
2023-09-19 13:51:13 +00:00
if ( is_null ( $cuid )) {
2024-07-25 11:31:59 +00:00
$cuid = new Cuid2 ;
2023-09-19 13:51:13 +00:00
}
2024-06-10 20:43:34 +00:00
2023-08-09 13:57:53 +00:00
return Str :: kebab ( " $git_repository : $git_branch - $cuid " );
2023-05-24 13:47:04 +00:00
}
2023-06-12 10:00:01 +00:00
2025-12-06 08:35:14 +00:00
/**
* Sort branches by priority : main first , master second , then alphabetically .
*
* @ param Collection $branches Collection of branch objects with 'name' key
*/
function sortBranchesByPriority ( Collection $branches ) : Collection
{
return $branches -> sortBy ( function ( $branch ) {
$name = data_get ( $branch , 'name' );
return match ( $name ) {
'main' => '0_main' ,
'master' => '1_master' ,
default => '2_' . $name ,
};
}) -> values ();
}
2023-08-09 13:57:53 +00:00
function base_ip () : string
2023-06-16 11:37:02 +00:00
{
2023-08-27 13:23:47 +00:00
if ( isDev ()) {
2024-06-10 20:43:34 +00:00
return 'localhost' ;
2023-07-28 11:40:47 +00:00
}
2024-10-01 08:37:40 +00:00
$settings = instanceSettings ();
2023-08-15 13:39:15 +00:00
if ( $settings -> public_ipv4 ) {
return " $settings->public_ipv4 " ;
}
if ( $settings -> public_ipv6 ) {
return " $settings->public_ipv6 " ;
}
2024-06-10 20:43:34 +00:00
return 'localhost' ;
2023-06-16 11:35:35 +00:00
}
2024-06-10 20:43:34 +00:00
function getFqdnWithoutPort ( string $fqdn )
2023-09-24 10:10:36 +00:00
{
2023-12-28 16:53:47 +00:00
try {
$url = Url :: fromString ( $fqdn );
$host = $url -> getHost ();
$scheme = $url -> getScheme ();
$path = $url -> getPath ();
2024-06-10 20:43:34 +00:00
2023-12-28 16:53:47 +00:00
return " $scheme :// $host $path " ;
2024-10-31 14:19:37 +00:00
} catch ( \Throwable ) {
2023-12-28 16:53:47 +00:00
return $fqdn ;
}
2023-09-22 12:47:25 +00:00
}
2023-08-09 13:57:53 +00:00
/**
* If fqdn is set , return it , otherwise return public ip .
*/
function base_url ( bool $withPort = true ) : string
2023-06-13 13:01:11 +00:00
{
2024-10-01 08:37:40 +00:00
$settings = instanceSettings ();
2023-06-13 13:01:11 +00:00
if ( $settings -> fqdn ) {
return $settings -> fqdn ;
}
2023-06-14 07:05:13 +00:00
$port = config ( 'app.port' );
if ( $settings -> public_ipv4 ) {
2023-06-16 09:01:27 +00:00
if ( $withPort ) {
2023-08-27 13:23:47 +00:00
if ( isDev ()) {
2023-08-09 13:57:53 +00:00
return " http://localhost: $port " ;
2023-06-16 09:01:27 +00:00
}
2024-06-10 20:43:34 +00:00
2023-08-09 13:57:53 +00:00
return " http:// $settings->public_ipv4 : $port " ;
2023-06-16 09:01:27 +00:00
}
2023-08-27 13:23:47 +00:00
if ( isDev ()) {
2024-06-10 20:43:34 +00:00
return 'http://localhost' ;
2023-06-16 09:01:27 +00:00
}
2024-06-10 20:43:34 +00:00
2023-08-09 13:57:53 +00:00
return " http:// $settings->public_ipv4 " ;
2023-06-14 07:05:13 +00:00
}
if ( $settings -> public_ipv6 ) {
2023-06-16 09:01:27 +00:00
if ( $withPort ) {
2023-08-09 13:57:53 +00:00
return " http:// $settings->public_ipv6 : $port " ;
2023-06-16 09:01:27 +00:00
}
2024-06-10 20:43:34 +00:00
2023-08-09 13:57:53 +00:00
return " http:// $settings->public_ipv6 " ;
2023-06-14 07:05:13 +00:00
}
2024-06-10 20:43:34 +00:00
2023-06-14 07:05:13 +00:00
return url ( '/' );
2023-06-13 13:01:11 +00:00
}
2023-06-16 09:01:27 +00:00
2024-03-21 13:30:35 +00:00
function isSubscribed ()
{
2025-12-27 15:17:56 +00:00
return isSubscriptionActive ();
2024-03-21 13:30:35 +00:00
}
2024-10-01 07:31:01 +00:00
function isProduction () : bool
{
2024-12-09 14:38:21 +00:00
return ! isDev ();
2024-10-01 07:31:01 +00:00
}
2023-08-27 13:23:47 +00:00
function isDev () : bool
2023-06-16 09:01:27 +00:00
{
return config ( 'app.env' ) === 'local' ;
}
2023-08-08 09:51:36 +00:00
2023-08-31 07:56:37 +00:00
function isCloud () : bool
2023-07-13 13:07:42 +00:00
{
2024-11-13 17:41:23 +00:00
return ! config ( 'constants.coolify.self_hosted' );
2023-08-08 09:51:36 +00:00
}
2023-08-09 13:57:53 +00:00
2024-08-26 13:26:08 +00:00
function translate_cron_expression ( $expression_to_validate ) : string
{
if ( isset ( VALID_CRON_STRINGS [ $expression_to_validate ])) {
return VALID_CRON_STRINGS [ $expression_to_validate ];
}
return $expression_to_validate ;
}
2023-08-10 13:52:54 +00:00
function validate_cron_expression ( $expression_to_validate ) : bool
{
2024-10-30 13:54:27 +00:00
if ( empty ( $expression_to_validate )) {
return false ;
}
2023-08-10 13:52:54 +00:00
$isValid = false ;
$expression = new CronExpression ( $expression_to_validate );
$isValid = $expression -> isValid ();
if ( isset ( VALID_CRON_STRINGS [ $expression_to_validate ])) {
$isValid = true ;
}
2024-06-10 20:43:34 +00:00
2023-08-10 13:52:54 +00:00
return $isValid ;
}
2024-11-14 09:02:37 +00:00
function validate_timezone ( string $timezone ) : bool
{
return in_array ( $timezone , timezone_identifiers_list ());
}
2024-06-10 20:43:34 +00:00
2023-09-15 13:34:25 +00:00
function parseEnvFormatToArray ( $env_file_contents )
{
2024-06-10 20:43:34 +00:00
$env_array = [];
2023-09-08 14:16:59 +00:00
$lines = explode ( " \n " , $env_file_contents );
foreach ( $lines as $line ) {
if ( $line === '' || substr ( $line , 0 , 1 ) === '#' ) {
continue ;
}
$equals_pos = strpos ( $line , '=' );
if ( $equals_pos !== false ) {
$key = substr ( $line , 0 , $equals_pos );
2025-11-18 09:10:29 +00:00
$value_and_comment = substr ( $line , $equals_pos + 1 );
$comment = null ;
$remainder = '' ;
// Check if value starts with quotes
2025-11-25 08:32:12 +00:00
$firstChar = $value_and_comment [ 0 ] ? ? '' ;
2025-11-18 09:10:29 +00:00
$isDoubleQuoted = $firstChar === '"' ;
$isSingleQuoted = $firstChar === " ' " ;
if ( $isDoubleQuoted ) {
// Find the closing double quote
$closingPos = strpos ( $value_and_comment , '"' , 1 );
if ( $closingPos !== false ) {
// Extract quoted value and remove quotes
$value = substr ( $value_and_comment , 1 , $closingPos - 1 );
// Everything after closing quote (including comments)
$remainder = substr ( $value_and_comment , $closingPos + 1 );
} else {
// No closing quote - treat as unquoted
$value = substr ( $value_and_comment , 1 );
}
} elseif ( $isSingleQuoted ) {
// Find the closing single quote
$closingPos = strpos ( $value_and_comment , " ' " , 1 );
if ( $closingPos !== false ) {
// Extract quoted value and remove quotes
$value = substr ( $value_and_comment , 1 , $closingPos - 1 );
// Everything after closing quote (including comments)
$remainder = substr ( $value_and_comment , $closingPos + 1 );
} else {
// No closing quote - treat as unquoted
$value = substr ( $value_and_comment , 1 );
}
} else {
// Unquoted value - strip inline comments
// Only treat # as comment if preceded by whitespace
if ( preg_match ( '/\s+#/' , $value_and_comment , $matches , PREG_OFFSET_CAPTURE )) {
// Found whitespace followed by #, extract comment
$remainder = substr ( $value_and_comment , $matches [ 0 ][ 1 ]);
$value = substr ( $value_and_comment , 0 , $matches [ 0 ][ 1 ]);
$value = rtrim ( $value );
} else {
$value = $value_and_comment ;
}
2023-09-08 14:16:59 +00:00
}
2025-11-18 09:10:29 +00:00
// Extract comment from remainder (if any)
if ( $remainder !== '' ) {
// Look for # in remainder
$hashPos = strpos ( $remainder , '#' );
if ( $hashPos !== false ) {
// Extract everything after the # and trim
$comment = substr ( $remainder , $hashPos + 1 );
$comment = trim ( $comment );
// Set to null if empty after trimming
if ( $comment === '' ) {
$comment = null ;
}
}
}
$env_array [ $key ] = [
'value' => $value ,
'comment' => $comment ,
];
2023-09-08 14:16:59 +00:00
}
}
2024-06-10 20:43:34 +00:00
2023-09-08 14:16:59 +00:00
return $env_array ;
}
2023-09-27 10:45:53 +00:00
2025-11-25 09:57:07 +00:00
/**
* Extract inline comments from environment variables in raw docker - compose YAML .
*
* Parses raw docker - compose YAML to extract inline comments from environment sections .
* Standard YAML parsers discard comments , so this pre - processes the raw text .
*
* Handles both formats :
* - Map format : `KEY: "value" # comment` or `KEY: value # comment`
* - Array format : `- KEY=value # comment`
*
* @ param string $rawYaml The raw docker - compose . yml content
* @ return array Map of environment variable keys to their inline comments
*/
function extractYamlEnvironmentComments ( string $rawYaml ) : array
{
$comments = [];
$lines = explode ( " \n " , $rawYaml );
$inEnvironmentBlock = false ;
$environmentIndent = 0 ;
foreach ( $lines as $line ) {
// Skip empty lines
if ( trim ( $line ) === '' ) {
continue ;
}
// Calculate current line's indentation (number of leading spaces)
$currentIndent = strlen ( $line ) - strlen ( ltrim ( $line ));
// Check if this line starts an environment block
if ( preg_match ( '/^(\s*)environment\s*:\s*$/' , $line , $matches )) {
$inEnvironmentBlock = true ;
$environmentIndent = strlen ( $matches [ 1 ]);
continue ;
}
// Check if this line starts an environment block with inline content (rare but possible)
if ( preg_match ( '/^(\s*)environment\s*:\s*\{/' , $line )) {
// Inline object format - not supported for comment extraction
continue ;
}
// If we're in an environment block, check if we've exited it
if ( $inEnvironmentBlock ) {
// If we hit a line with same or less indentation that's not empty, we've left the block
// Unless it's a continuation of the environment block
$trimmedLine = ltrim ( $line );
// Check if this is a new top-level key (same indent as 'environment:' or less)
if ( $currentIndent <= $environmentIndent && ! str_starts_with ( $trimmedLine , '-' ) && ! str_starts_with ( $trimmedLine , '#' )) {
// Check if it looks like a YAML key (contains : not inside quotes)
if ( preg_match ( '/^[a-zA-Z_][a-zA-Z0-9_]*\s*:/' , $trimmedLine )) {
$inEnvironmentBlock = false ;
continue ;
}
}
// Skip comment-only lines
if ( str_starts_with ( $trimmedLine , '#' )) {
continue ;
}
// Try to extract environment variable and comment from this line
$extracted = extractEnvVarCommentFromYamlLine ( $trimmedLine );
if ( $extracted !== null && $extracted [ 'comment' ] !== null ) {
$comments [ $extracted [ 'key' ]] = $extracted [ 'comment' ];
}
}
}
return $comments ;
}
/**
* Extract environment variable key and inline comment from a single YAML line .
*
* @ param string $line A trimmed line from the environment section
* @ return array | null Array with 'key' and 'comment' , or null if not an env var line
*/
function extractEnvVarCommentFromYamlLine ( string $line ) : ? array
{
$key = null ;
$comment = null ;
// Handle array format: `- KEY=value # comment` or `- KEY # comment`
if ( str_starts_with ( $line , '-' )) {
$content = ltrim ( substr ( $line , 1 ));
// Check for KEY=value format
if ( preg_match ( '/^([A-Za-z_][A-Za-z0-9_]*)/' , $content , $keyMatch )) {
$key = $keyMatch [ 1 ];
// Find comment - need to handle quoted values
$comment = extractCommentAfterValue ( $content );
}
}
// Handle map format: `KEY: "value" # comment` or `KEY: value # comment`
elseif ( preg_match ( '/^([A-Za-z_][A-Za-z0-9_]*)\s*:/' , $line , $keyMatch )) {
$key = $keyMatch [ 1 ];
// Get everything after the key and colon
$afterKey = substr ( $line , strlen ( $keyMatch [ 0 ]));
$comment = extractCommentAfterValue ( $afterKey );
}
if ( $key === null ) {
return null ;
}
return [
'key' => $key ,
'comment' => $comment ,
];
}
/**
* Extract inline comment from a value portion of a YAML line .
*
* Handles quoted values ( where # inside quotes is not a comment).
*
* @ param string $valueAndComment The value portion ( may include comment )
* @ return string | null The comment text , or null if no comment
*/
function extractCommentAfterValue ( string $valueAndComment ) : ? string
{
$valueAndComment = ltrim ( $valueAndComment );
if ( $valueAndComment === '' ) {
return null ;
}
$firstChar = $valueAndComment [ 0 ] ? ? '' ;
// Handle case where value is empty and line starts directly with comment
// e.g., `KEY: # comment` becomes `# comment` after ltrim
if ( $firstChar === '#' ) {
$comment = trim ( substr ( $valueAndComment , 1 ));
return $comment !== '' ? $comment : null ;
}
// Handle double-quoted value
if ( $firstChar === '"' ) {
// Find closing quote (handle escaped quotes)
$pos = 1 ;
$len = strlen ( $valueAndComment );
while ( $pos < $len ) {
if ( $valueAndComment [ $pos ] === '\\' && $pos + 1 < $len ) {
$pos += 2 ; // Skip escaped character
continue ;
}
if ( $valueAndComment [ $pos ] === '"' ) {
// Found closing quote
$remainder = substr ( $valueAndComment , $pos + 1 );
return extractCommentFromRemainder ( $remainder );
}
$pos ++ ;
}
// No closing quote found
return null ;
}
// Handle single-quoted value
if ( $firstChar === " ' " ) {
// Find closing quote (single quotes don't have escapes in YAML)
$closingPos = strpos ( $valueAndComment , " ' " , 1 );
if ( $closingPos !== false ) {
$remainder = substr ( $valueAndComment , $closingPos + 1 );
return extractCommentFromRemainder ( $remainder );
}
// No closing quote found
return null ;
}
// Unquoted value - find # that's preceded by whitespace
// Be careful not to match # at the start of a value like color codes
if ( preg_match ( '/\s+#\s*(.*)$/' , $valueAndComment , $matches )) {
$comment = trim ( $matches [ 1 ]);
return $comment !== '' ? $comment : null ;
}
return null ;
}
/**
* Extract comment from the remainder of a line after a quoted value .
*
* @ param string $remainder Text after the closing quote
* @ return string | null The comment text , or null if no comment
*/
function extractCommentFromRemainder ( string $remainder ) : ? string
{
// Look for # in remainder
$hashPos = strpos ( $remainder , '#' );
if ( $hashPos !== false ) {
$comment = trim ( substr ( $remainder , $hashPos + 1 ));
return $comment !== '' ? $comment : null ;
}
return null ;
}
2023-09-27 10:45:53 +00:00
function data_get_str ( $data , $key , $default = null ) : Stringable
{
$str = data_get ( $data , $key , $default ) ? ? $default ;
2024-06-10 20:43:34 +00:00
2024-06-25 08:34:56 +00:00
return str ( $str );
2023-09-27 10:45:53 +00:00
}
2023-09-28 08:53:00 +00:00
2025-08-04 08:46:59 +00:00
function generateUrl ( Server $server , string $random , bool $forceHttps = false ) : string
2023-09-30 13:39:40 +00:00
{
$wildcard = data_get ( $server , 'settings.wildcard_domain' );
if ( is_null ( $wildcard ) || $wildcard === '' ) {
$wildcard = sslip ( $server );
}
$url = Url :: fromString ( $wildcard );
$host = $url -> getHost ();
$path = $url -> getPath () === '/' ? '' : $url -> getPath ();
$scheme = $url -> getScheme ();
2024-09-16 14:35:47 +00:00
if ( $forceHttps ) {
$scheme = 'https' ;
}
2024-06-10 20:43:34 +00:00
2024-10-31 17:20:11 +00:00
return " $scheme :// { $random } . $host $path " ;
2023-09-30 13:39:40 +00:00
}
2025-08-27 15:02:38 +00:00
function generateFqdn ( Server $server , string $random , bool $forceHttps = false , int $parserVersion = 5 ) : string
2025-08-04 08:46:59 +00:00
{
2025-08-27 15:02:38 +00:00
2025-08-04 08:46:59 +00:00
$wildcard = data_get ( $server , 'settings.wildcard_domain' );
if ( is_null ( $wildcard ) || $wildcard === '' ) {
$wildcard = sslip ( $server );
}
$url = Url :: fromString ( $wildcard );
$host = $url -> getHost ();
$path = $url -> getPath () === '/' ? '' : $url -> getPath ();
$scheme = $url -> getScheme ();
if ( $forceHttps ) {
$scheme = 'https' ;
}
if ( $parserVersion >= 5 && version_compare ( config ( 'constants.coolify.version' ), '4.0.0-beta.420.7' , '>=' )) {
return " { $random } . $host $path " ;
}
return " $scheme :// { $random } . $host $path " ;
}
2023-09-28 08:53:00 +00:00
function sslip ( Server $server )
{
2024-01-09 11:48:46 +00:00
if ( isDev () && $server -> id === 0 ) {
2024-06-10 20:43:34 +00:00
return 'http://127.0.0.1.sslip.io' ;
2023-09-28 08:53:00 +00:00
}
if ( $server -> ip === 'host.docker.internal' ) {
$baseIp = base_ip ();
2024-06-10 20:43:34 +00:00
2023-09-30 13:39:40 +00:00
return " http:// $baseIp .sslip.io " ;
2023-09-28 08:53:00 +00:00
}
2024-09-26 11:47:13 +00:00
// ipv6
if ( str ( $server -> ip ) -> contains ( ':' )) {
$ipv6 = str ( $server -> ip ) -> replace ( ':' , '-' );
return " http:// { $ipv6 } .sslip.io " ;
}
2024-06-10 20:43:34 +00:00
2023-09-30 13:39:40 +00:00
return " http:// { $server -> ip } .sslip.io " ;
2023-09-28 08:53:00 +00:00
}
2023-09-28 20:20:49 +00:00
2024-05-27 08:27:18 +00:00
function get_service_templates ( bool $force = false ) : Collection
{
2024-10-07 09:02:01 +00:00
2024-05-27 08:27:18 +00:00
if ( $force ) {
try {
2024-08-07 07:50:29 +00:00
$response = Http :: retry ( 3 , 1000 ) -> get ( config ( 'constants.services.official' ));
2024-05-27 08:27:18 +00:00
if ( $response -> failed ()) {
return collect ([]);
}
$services = $response -> json ();
2024-06-10 20:43:34 +00:00
2024-05-27 08:27:18 +00:00
return collect ( $services );
2024-10-31 14:19:37 +00:00
} catch ( \Throwable ) {
2025-08-10 08:10:14 +00:00
$services = File :: get ( base_path ( 'templates/' . config ( 'constants.services.file_name' )));
2024-06-10 20:43:34 +00:00
2024-05-27 08:27:18 +00:00
return collect ( json_decode ( $services )) -> sortKeys ();
}
} else {
2025-08-10 08:10:14 +00:00
$services = File :: get ( base_path ( 'templates/' . config ( 'constants.services.file_name' )));
2024-06-10 20:43:34 +00:00
2024-05-27 08:27:18 +00:00
return collect ( json_decode ( $services )) -> sortKeys ();
}
2023-09-28 20:20:49 +00:00
}
2023-10-20 12:51:01 +00:00
function getResourceByUuid ( string $uuid , ? int $teamId = null )
{
2024-04-10 13:00:46 +00:00
if ( is_null ( $teamId )) {
2023-10-20 12:51:01 +00:00
return null ;
2024-04-10 13:00:46 +00:00
}
$resource = queryResourcesByUuid ( $uuid );
2026-01-02 15:29:48 +00:00
if ( is_null ( $resource )) {
return null ;
}
// ServiceDatabase has a different relationship path: service->environment->project->team_id
if ( $resource instanceof \App\Models\ServiceDatabase ) {
if ( $resource -> service ? -> environment ? -> project ? -> team_id === $teamId ) {
return $resource ;
}
return null ;
}
// Standard resources: environment->project->team_id
if ( $resource -> environment -> project -> team_id === $teamId ) {
2023-10-20 12:51:01 +00:00
return $resource ;
}
2024-06-10 20:43:34 +00:00
2024-04-10 13:00:46 +00:00
return null ;
2023-10-20 12:51:01 +00:00
}
2024-07-01 14:26:50 +00:00
function queryDatabaseByUuidWithinTeam ( string $uuid , string $teamId )
{
$postgresql = StandalonePostgresql :: whereUuid ( $uuid ) -> first ();
if ( $postgresql && $postgresql -> team () -> id == $teamId ) {
2025-05-20 13:08:04 +00:00
return $postgresql -> unsetRelation ( 'environment' );
2024-07-01 14:26:50 +00:00
}
$redis = StandaloneRedis :: whereUuid ( $uuid ) -> first ();
if ( $redis && $redis -> team () -> id == $teamId ) {
return $redis -> unsetRelation ( 'environment' );
}
$mongodb = StandaloneMongodb :: whereUuid ( $uuid ) -> first ();
if ( $mongodb && $mongodb -> team () -> id == $teamId ) {
return $mongodb -> unsetRelation ( 'environment' );
}
$mysql = StandaloneMysql :: whereUuid ( $uuid ) -> first ();
if ( $mysql && $mysql -> team () -> id == $teamId ) {
return $mysql -> unsetRelation ( 'environment' );
}
$mariadb = StandaloneMariadb :: whereUuid ( $uuid ) -> first ();
if ( $mariadb && $mariadb -> team () -> id == $teamId ) {
return $mariadb -> unsetRelation ( 'environment' );
}
$keydb = StandaloneKeydb :: whereUuid ( $uuid ) -> first ();
if ( $keydb && $keydb -> team () -> id == $teamId ) {
return $keydb -> unsetRelation ( 'environment' );
}
$dragonfly = StandaloneDragonfly :: whereUuid ( $uuid ) -> first ();
if ( $dragonfly && $dragonfly -> team () -> id == $teamId ) {
return $dragonfly -> unsetRelation ( 'environment' );
}
$clickhouse = StandaloneClickhouse :: whereUuid ( $uuid ) -> first ();
if ( $clickhouse && $clickhouse -> team () -> id == $teamId ) {
return $clickhouse -> unsetRelation ( 'environment' );
}
return null ;
}
2023-10-20 12:51:01 +00:00
function queryResourcesByUuid ( string $uuid )
{
$resource = null ;
$application = Application :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $application ) {
return $application ;
}
2023-10-20 12:51:01 +00:00
$service = Service :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $service ) {
return $service ;
}
2023-10-20 12:51:01 +00:00
$postgresql = StandalonePostgresql :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $postgresql ) {
return $postgresql ;
}
2023-10-20 12:51:01 +00:00
$redis = StandaloneRedis :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $redis ) {
return $redis ;
}
2023-10-20 12:51:01 +00:00
$mongodb = StandaloneMongodb :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $mongodb ) {
return $mongodb ;
}
2023-10-25 08:43:07 +00:00
$mysql = StandaloneMysql :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $mysql ) {
return $mysql ;
}
2023-10-25 08:43:07 +00:00
$mariadb = StandaloneMariadb :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $mariadb ) {
return $mariadb ;
}
2024-04-10 13:00:46 +00:00
$keydb = StandaloneKeydb :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $keydb ) {
return $keydb ;
}
2024-04-10 13:00:46 +00:00
$dragonfly = StandaloneDragonfly :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $dragonfly ) {
return $dragonfly ;
}
2024-04-10 13:00:46 +00:00
$clickhouse = StandaloneClickhouse :: whereUuid ( $uuid ) -> first ();
2024-06-10 20:43:34 +00:00
if ( $clickhouse ) {
return $clickhouse ;
}
2026-01-02 15:29:48 +00:00
// Check for ServiceDatabase by its own UUID
$serviceDatabase = ServiceDatabase :: whereUuid ( $uuid ) -> first ();
if ( $serviceDatabase ) {
return $serviceDatabase ;
}
2023-10-20 12:51:01 +00:00
return $resource ;
}
2024-10-25 08:59:12 +00:00
function generateTagDeployWebhook ( $tag_name )
2024-02-01 14:38:12 +00:00
{
$baseUrl = base_url ();
2024-12-09 14:38:21 +00:00
$api = Url :: fromString ( $baseUrl ) . '/api/v1' ;
2024-02-02 10:50:28 +00:00
$endpoint = " /deploy?tag= $tag_name " ;
2024-06-10 20:43:34 +00:00
2024-12-09 14:38:21 +00:00
return $api . $endpoint ;
2024-02-01 14:38:12 +00:00
}
2023-11-13 10:44:13 +00:00
function generateDeployWebhook ( $resource )
{
2023-10-25 08:43:07 +00:00
$baseUrl = base_url ();
2024-12-09 14:38:21 +00:00
$api = Url :: fromString ( $baseUrl ) . '/api/v1' ;
2023-10-25 08:43:07 +00:00
$endpoint = '/deploy' ;
$uuid = data_get ( $resource , 'uuid' );
2024-06-10 20:43:34 +00:00
2024-12-09 14:38:21 +00:00
return $api . $endpoint . " ?uuid= $uuid &force=false " ;
2023-10-25 08:43:07 +00:00
}
2023-11-24 14:48:23 +00:00
function generateGitManualWebhook ( $resource , $type )
{
2024-12-09 14:38:21 +00:00
if ( $resource -> source_id !== 0 && ! is_null ( $resource -> source_id )) {
2023-11-14 13:14:21 +00:00
return null ;
}
2024-10-28 13:56:13 +00:00
if ( $resource -> getMorphClass () === \App\Models\Application :: class ) {
2023-11-14 12:26:14 +00:00
$baseUrl = base_url ();
2024-06-10 20:43:34 +00:00
2024-12-09 14:38:21 +00:00
return Url :: fromString ( $baseUrl ) . " /webhooks/source/ $type /events/manual " ;
2023-11-14 12:26:14 +00:00
}
2024-06-10 20:43:34 +00:00
2023-11-14 12:26:14 +00:00
return null ;
}
2023-11-13 10:44:13 +00:00
function removeAnsiColors ( $text )
{
2023-11-07 13:40:58 +00:00
return preg_replace ( '/\e[[][A-Za-z0-9];?[0-9]*m?/' , '' , $text );
}
2023-11-24 14:48:23 +00:00
2025-12-16 02:43:18 +00:00
function sanitizeLogsForExport ( string $text ) : string
{
Enhance log sanitization with GitHub, GitLab, AWS, and generic URL passwords
Consolidate all PII/secret sanitization into remove_iip() to protect real-time logs in addition to exported logs. Add detection for GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_), GitLab tokens (glpat-, glcbt-, glrt-), AWS credentials (AKIA/ABIA/ACCA/ASIA access keys and secret keys), and generic URL passwords for FTP, SSH, AMQP, LDAP, and S3 protocols.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-17 16:59:10 +00:00
// All sanitization is now handled by remove_iip()
return remove_iip ( $text );
2025-12-16 02:43:18 +00:00
}
2023-11-27 08:58:31 +00:00
function getTopLevelNetworks ( Service | Application $resource )
{
2024-10-28 13:56:13 +00:00
if ( $resource -> getMorphClass () === \App\Models\Service :: class ) {
2023-11-27 08:58:31 +00:00
if ( $resource -> docker_compose_raw ) {
try {
$yaml = Yaml :: parse ( $resource -> docker_compose_raw );
} catch ( \Exception $e ) {
2025-07-07 08:20:54 +00:00
// If the docker-compose.yml file is not valid, we will return the network name as the key
$topLevelNetworks = collect ([
$resource -> uuid => [
'name' => $resource -> uuid ,
'external' => true ,
],
]);
return $topLevelNetworks -> keys ();
2023-11-27 08:58:31 +00:00
}
$services = data_get ( $yaml , 'services' );
$topLevelNetworks = collect ( data_get ( $yaml , 'networks' , []));
$definedNetwork = collect ([ $resource -> uuid ]);
$services = collect ( $services ) -> map ( function ( $service , $_ ) use ( $topLevelNetworks , $definedNetwork ) {
$serviceNetworks = collect ( data_get ( $service , 'networks' , []));
2025-07-20 20:15:42 +00:00
$networkMode = data_get ( $service , 'network_mode' );
2023-11-27 08:58:31 +00:00
2025-07-20 20:15:42 +00:00
$hasValidNetworkMode =
$networkMode === 'host' ||
( is_string ( $networkMode ) && ( str_starts_with ( $networkMode , 'service:' ) || str_starts_with ( $networkMode , 'container:' )));
// Only add 'networks' key if 'network_mode' is not 'host' or does not start with 'service:' or 'container:'
if ( ! $hasValidNetworkMode ) {
2024-02-11 15:22:09 +00:00
// Collect/create/update networks
if ( $serviceNetworks -> count () > 0 ) {
foreach ( $serviceNetworks as $networkName => $networkDetails ) {
2024-05-31 08:21:38 +00:00
if ( $networkName === 'default' ) {
continue ;
}
// ignore alias
if ( $networkDetails [ 'aliases' ] ? ? false ) {
continue ;
}
2024-02-11 15:22:09 +00:00
$networkExists = $topLevelNetworks -> contains ( function ( $value , $key ) use ( $networkName ) {
return $value == $networkName || $key == $networkName ;
});
2024-12-09 14:38:21 +00:00
if ( ! $networkExists ) {
2024-10-03 13:04:40 +00:00
if ( is_string ( $networkDetails ) || is_int ( $networkDetails )) {
$topLevelNetworks -> put ( $networkDetails , null );
}
2024-02-11 15:22:09 +00:00
}
2023-11-27 08:58:31 +00:00
}
}
2024-02-11 15:22:09 +00:00
$definedNetworkExists = $topLevelNetworks -> contains ( function ( $value , $_ ) use ( $definedNetwork ) {
return $value == $definedNetwork ;
});
2024-12-09 14:38:21 +00:00
if ( ! $definedNetworkExists ) {
2024-02-11 15:22:09 +00:00
foreach ( $definedNetwork as $network ) {
2024-06-10 20:43:34 +00:00
$topLevelNetworks -> put ( $network , [
2024-02-11 15:22:09 +00:00
'name' => $network ,
2024-06-10 20:43:34 +00:00
'external' => true ,
2024-02-11 15:22:09 +00:00
]);
}
2023-11-27 08:58:31 +00:00
}
}
return $service ;
});
2024-06-10 20:43:34 +00:00
2023-11-27 08:58:31 +00:00
return $topLevelNetworks -> keys ();
}
2024-10-28 13:56:13 +00:00
} elseif ( $resource -> getMorphClass () === \App\Models\Application :: class ) {
2023-11-27 08:58:31 +00:00
try {
$yaml = Yaml :: parse ( $resource -> docker_compose_raw );
} catch ( \Exception $e ) {
2025-07-07 08:20:54 +00:00
// If the docker-compose.yml file is not valid, we will return the network name as the key
$topLevelNetworks = collect ([
$resource -> uuid => [
'name' => $resource -> uuid ,
'external' => true ,
],
]);
return $topLevelNetworks -> keys ();
2023-11-27 08:58:31 +00:00
}
$topLevelNetworks = collect ( data_get ( $yaml , 'networks' , []));
$services = data_get ( $yaml , 'services' );
$definedNetwork = collect ([ $resource -> uuid ]);
$services = collect ( $services ) -> map ( function ( $service , $_ ) use ( $topLevelNetworks , $definedNetwork ) {
$serviceNetworks = collect ( data_get ( $service , 'networks' , []));
// Collect/create/update networks
if ( $serviceNetworks -> count () > 0 ) {
foreach ( $serviceNetworks as $networkName => $networkDetails ) {
2024-05-31 08:21:38 +00:00
if ( $networkName === 'default' ) {
continue ;
}
// ignore alias
if ( $networkDetails [ 'aliases' ] ? ? false ) {
continue ;
}
2023-11-27 08:58:31 +00:00
$networkExists = $topLevelNetworks -> contains ( function ( $value , $key ) use ( $networkName ) {
return $value == $networkName || $key == $networkName ;
});
2024-12-09 14:38:21 +00:00
if ( ! $networkExists ) {
2024-10-03 13:04:40 +00:00
if ( is_string ( $networkDetails ) || is_int ( $networkDetails )) {
$topLevelNetworks -> put ( $networkDetails , null );
}
2023-11-27 08:58:31 +00:00
}
}
}
$definedNetworkExists = $topLevelNetworks -> contains ( function ( $value , $_ ) use ( $definedNetwork ) {
return $value == $definedNetwork ;
});
2024-12-09 14:38:21 +00:00
if ( ! $definedNetworkExists ) {
2023-11-27 08:58:31 +00:00
foreach ( $definedNetwork as $network ) {
2024-06-10 20:43:34 +00:00
$topLevelNetworks -> put ( $network , [
2023-11-27 08:58:31 +00:00
'name' => $network ,
2024-06-10 20:43:34 +00:00
'external' => true ,
2023-11-27 08:58:31 +00:00
]);
}
}
2024-06-10 20:43:34 +00:00
2023-11-27 08:58:31 +00:00
return $service ;
});
2024-06-10 20:43:34 +00:00
2023-11-27 08:58:31 +00:00
return $topLevelNetworks -> keys ();
}
}
2024-08-21 18:32:02 +00:00
function sourceIsLocal ( Stringable $source )
{
if ( $source -> startsWith ( './' ) || $source -> startsWith ( '/' ) || $source -> startsWith ( '~' ) || $source -> startsWith ( '..' ) || $source -> startsWith ( '~/' ) || $source -> startsWith ( '../' )) {
return true ;
}
return false ;
}
function replaceLocalSource ( Stringable $source , Stringable $replacedWith )
{
if ( $source -> startsWith ( '.' )) {
$source = $source -> replaceFirst ( '.' , $replacedWith -> value ());
}
if ( $source -> startsWith ( '~' )) {
$source = $source -> replaceFirst ( '~' , $replacedWith -> value ());
}
if ( $source -> startsWith ( '..' )) {
$source = $source -> replaceFirst ( '..' , $replacedWith -> value ());
}
2024-09-13 09:12:28 +00:00
if ( $source -> endsWith ( '/' ) && $source -> value () !== '/' ) {
2024-08-23 12:21:12 +00:00
$source = $source -> replaceLast ( '/' , '' );
2024-08-21 18:32:02 +00:00
}
2024-08-22 13:05:04 +00:00
2024-08-23 12:21:12 +00:00
return $source ;
2024-08-21 18:32:02 +00:00
}
2024-08-22 13:05:04 +00:00
function convertToArray ( $collection )
{
if ( $collection instanceof Collection ) {
return $collection -> map ( function ( $item ) {
return convertToArray ( $item );
}) -> toArray ();
} elseif ( $collection instanceof Stringable ) {
return ( string ) $collection ;
} elseif ( is_array ( $collection )) {
return array_map ( function ( $item ) {
return convertToArray ( $item );
}, $collection );
}
return $collection ;
}
2023-11-28 11:05:04 +00:00
2024-10-03 20:02:18 +00:00
function parseCommandFromMagicEnvVariable ( Str | string $key ) : Stringable
{
$value = str ( $key );
$count = substr_count ( $value -> value (), '_' );
2025-02-28 19:28:35 +00:00
$command = null ;
2024-10-03 20:02:18 +00:00
if ( $count === 2 ) {
if ( $value -> startsWith ( 'SERVICE_FQDN' ) || $value -> startsWith ( 'SERVICE_URL' )) {
// SERVICE_FQDN_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
} else {
// SERVICE_BASE64_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
}
}
if ( $count === 3 ) {
if ( $value -> startsWith ( 'SERVICE_FQDN' ) || $value -> startsWith ( 'SERVICE_URL' )) {
// SERVICE_FQDN_UMAMI_1000
$command = $value -> after ( 'SERVICE_' ) -> before ( '_' );
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
}
}
return str ( $command );
}
2023-11-28 11:05:04 +00:00
function parseEnvVariable ( Str | string $value )
{
$value = str ( $value );
$count = substr_count ( $value -> value (), '_' );
$command = null ;
$forService = null ;
$generatedValue = null ;
$port = null ;
2024-03-21 14:36:37 +00:00
if ( $value -> startsWith ( 'SERVICE' )) {
if ( $count === 2 ) {
if ( $value -> startsWith ( 'SERVICE_FQDN' ) || $value -> startsWith ( 'SERVICE_URL' )) {
// SERVICE_FQDN_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
$forService = $value -> afterLast ( '_' );
} else {
// SERVICE_BASE64_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
}
2023-11-28 11:05:04 +00:00
}
2024-03-21 14:36:37 +00:00
if ( $count === 3 ) {
if ( $value -> startsWith ( 'SERVICE_FQDN' ) || $value -> startsWith ( 'SERVICE_URL' )) {
// SERVICE_FQDN_UMAMI_1000
$command = $value -> after ( 'SERVICE_' ) -> before ( '_' );
$forService = $value -> after ( 'SERVICE_' ) -> after ( '_' ) -> before ( '_' );
$port = $value -> afterLast ( '_' );
if ( filter_var ( $port , FILTER_VALIDATE_INT ) === false ) {
$port = null ;
}
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value -> after ( 'SERVICE_' ) -> beforeLast ( '_' );
2023-12-28 16:53:47 +00:00
}
2023-11-28 11:05:04 +00:00
}
}
2024-06-10 20:43:34 +00:00
2023-11-28 11:05:04 +00:00
return [
'command' => $command ,
'forService' => $forService ,
'generatedValue' => $generatedValue ,
'port' => $port ,
];
}
2024-08-27 19:48:25 +00:00
function generateEnvValue ( string $command , Service | Application | null $service = null )
2023-11-28 11:05:04 +00:00
{
switch ( $command ) {
case 'PASSWORD' :
$generatedValue = Str :: password ( symbols : false );
break ;
case 'PASSWORD_64' :
$generatedValue = Str :: password ( length : 64 , symbols : false );
break ;
2024-12-12 10:06:00 +00:00
case 'PASSWORDWITHSYMBOLS' :
$generatedValue = Str :: password ( symbols : true );
break ;
case 'PASSWORDWITHSYMBOLS_64' :
$generatedValue = Str :: password ( length : 64 , symbols : true );
break ;
2024-12-09 14:38:21 +00:00
// This is not base64, it's just a random string
2023-11-28 11:05:04 +00:00
case 'BASE64_64' :
2024-03-04 11:50:56 +00:00
$generatedValue = Str :: random ( 64 );
2023-11-28 11:05:04 +00:00
break ;
case 'BASE64_128' :
2024-03-04 11:50:56 +00:00
$generatedValue = Str :: random ( 128 );
2023-11-28 11:05:04 +00:00
break ;
case 'BASE64' :
2024-03-04 11:46:37 +00:00
case 'BASE64_32' :
2024-03-04 11:50:56 +00:00
$generatedValue = Str :: random ( 32 );
break ;
2024-12-09 14:38:21 +00:00
// This is base64,
2024-03-04 11:50:56 +00:00
case 'REALBASE64_64' :
$generatedValue = base64_encode ( Str :: random ( 64 ));
break ;
case 'REALBASE64_128' :
$generatedValue = base64_encode ( Str :: random ( 128 ));
break ;
case 'REALBASE64' :
case 'REALBASE64_32' :
2024-03-04 11:46:37 +00:00
$generatedValue = base64_encode ( Str :: random ( 32 ));
2023-11-28 11:05:04 +00:00
break ;
2024-11-26 12:01:42 +00:00
case 'HEX_32' :
$generatedValue = bin2hex ( Str :: random ( 32 ));
break ;
case 'HEX_64' :
$generatedValue = bin2hex ( Str :: random ( 64 ));
break ;
case 'HEX_128' :
$generatedValue = bin2hex ( Str :: random ( 128 ));
break ;
2023-11-28 11:05:04 +00:00
case 'USER' :
$generatedValue = Str :: random ( 16 );
break ;
2026-01-04 17:57:04 +00:00
case 'LOWERCASEUSER' :
$generatedValue = Str :: lower ( Str :: random ( 16 ));
break ;
2024-02-28 12:48:39 +00:00
case 'SUPABASEANON' :
$signingKey = $service -> environment_variables () -> where ( 'key' , 'SERVICE_PASSWORD_JWT' ) -> first ();
if ( is_null ( $signingKey )) {
return ;
} else {
$signingKey = $signingKey -> value ;
}
$key = InMemory :: plainText ( $signingKey );
2024-07-24 19:11:12 +00:00
$algorithm = new Sha256 ;
$tokenBuilder = ( new Builder ( new JoseEncoder , ChainedFormatter :: default ()));
2024-10-31 15:47:08 +00:00
$now = CarbonImmutable :: now ();
2024-02-28 12:48:39 +00:00
$now = $now -> setTime ( $now -> format ( 'H' ), $now -> format ( 'i' ));
$token = $tokenBuilder
-> issuedBy ( 'supabase' )
-> issuedAt ( $now )
-> expiresAt ( $now -> modify ( '+100 year' ))
-> withClaim ( 'role' , 'anon' )
-> getToken ( $algorithm , $key );
$generatedValue = $token -> toString ();
break ;
case 'SUPABASESERVICE' :
$signingKey = $service -> environment_variables () -> where ( 'key' , 'SERVICE_PASSWORD_JWT' ) -> first ();
if ( is_null ( $signingKey )) {
return ;
} else {
$signingKey = $signingKey -> value ;
}
$key = InMemory :: plainText ( $signingKey );
2024-07-24 19:11:12 +00:00
$algorithm = new Sha256 ;
$tokenBuilder = ( new Builder ( new JoseEncoder , ChainedFormatter :: default ()));
2024-10-31 15:47:08 +00:00
$now = CarbonImmutable :: now ();
2024-02-28 12:48:39 +00:00
$now = $now -> setTime ( $now -> format ( 'H' ), $now -> format ( 'i' ));
$token = $tokenBuilder
-> issuedBy ( 'supabase' )
-> issuedAt ( $now )
-> expiresAt ( $now -> modify ( '+100 year' ))
-> withClaim ( 'role' , 'service_role' )
-> getToken ( $algorithm , $key );
$generatedValue = $token -> toString ();
break ;
2023-12-14 14:34:05 +00:00
default :
2024-08-27 19:48:25 +00:00
// $generatedValue = Str::random(16);
$generatedValue = null ;
2023-12-14 14:34:05 +00:00
break ;
2023-11-28 11:05:04 +00:00
}
2024-06-10 20:43:34 +00:00
2023-11-28 11:05:04 +00:00
return $generatedValue ;
}
2023-12-11 19:01:54 +00:00
2023-12-11 19:13:41 +00:00
function getRealtime ()
{
2024-11-12 14:53:05 +00:00
$envDefined = config ( 'constants.pusher.port' );
2023-12-11 19:22:31 +00:00
if ( empty ( $envDefined )) {
2023-12-11 19:13:41 +00:00
$url = Url :: fromString ( Request :: getSchemeAndHttpHost ());
$port = $url -> getPort ();
if ( $port ) {
return '6001' ;
} else {
return null ;
}
2023-12-11 19:01:54 +00:00
} else {
2023-12-11 19:13:41 +00:00
return $envDefined ;
2023-12-11 19:01:54 +00:00
}
}
2024-01-15 09:03:15 +00:00
2025-09-09 07:00:35 +00:00
function validateDNSEntry ( string $fqdn , Server $server )
2024-01-15 09:03:15 +00:00
{
2024-06-10 20:43:34 +00:00
// https://www.cloudflare.com/ips-v4/#
2024-01-15 10:37:26 +00:00
$cloudflare_ips = collect ([ '173.245.48.0/20' , '103.21.244.0/22' , '103.22.200.0/22' , '103.31.4.0/22' , '141.101.64.0/18' , '108.162.192.0/18' , '190.93.240.0/20' , '188.114.96.0/20' , '197.234.240.0/22' , '198.41.128.0/17' , '162.158.0.0/15' , '104.16.0.0/13' , '172.64.0.0/13' , '131.0.72.0/22' ]);
2024-01-15 09:03:15 +00:00
$url = Url :: fromString ( $fqdn );
$host = $url -> getHost ();
if ( str ( $host ) -> contains ( 'sslip.io' )) {
return true ;
}
2024-10-01 08:37:40 +00:00
$settings = instanceSettings ();
2024-01-15 09:03:15 +00:00
$is_dns_validation_enabled = data_get ( $settings , 'is_dns_validation_enabled' );
2024-12-09 14:38:21 +00:00
if ( ! $is_dns_validation_enabled ) {
2024-01-15 09:03:15 +00:00
return true ;
}
2024-01-15 10:37:26 +00:00
$dns_servers = data_get ( $settings , 'custom_dns_servers' );
$dns_servers = str ( $dns_servers ) -> explode ( ',' );
2024-01-15 09:03:15 +00:00
if ( $server -> id === 0 ) {
2024-05-06 10:33:22 +00:00
$ip = data_get ( $settings , 'public_ipv4' , data_get ( $settings , 'public_ipv6' , $server -> ip ));
2024-01-15 09:03:15 +00:00
} else {
$ip = $server -> ip ;
}
2024-01-15 10:37:26 +00:00
$found_matching_ip = false ;
2024-01-15 09:03:15 +00:00
$type = \PurplePixie\PhpDns\DNSTypes :: NAME_A ;
2024-01-15 10:37:26 +00:00
foreach ( $dns_servers as $dns_server ) {
2024-01-15 09:03:15 +00:00
try {
2024-01-15 10:37:26 +00:00
$query = new DNSQuery ( $dns_server );
2024-01-15 09:03:15 +00:00
$results = $query -> query ( $host , $type );
if ( $results === false || $query -> hasError ()) {
2024-12-09 14:38:21 +00:00
ray ( 'Error: ' . $query -> getLasterror ());
2024-01-15 09:03:15 +00:00
} else {
foreach ( $results as $result ) {
if ( $result -> getType () == $type ) {
2025-09-09 07:00:35 +00:00
if ( ipMatch ( $result -> getData (), $cloudflare_ips -> toArray (), $match )) {
2024-01-15 10:37:26 +00:00
$found_matching_ip = true ;
break ;
}
2024-01-15 09:03:15 +00:00
if ( $result -> getData () === $ip ) {
2024-01-15 10:37:26 +00:00
$found_matching_ip = true ;
2024-01-15 09:03:15 +00:00
break ;
}
}
}
}
2024-10-31 14:19:37 +00:00
} catch ( \Exception ) {
2024-01-15 09:03:15 +00:00
}
}
2024-06-10 20:43:34 +00:00
2024-01-15 10:37:26 +00:00
return $found_matching_ip ;
}
2025-09-09 07:00:35 +00:00
function ipMatch ( $ip , $cidrs , & $match = null )
2024-01-15 10:37:26 +00:00
{
foreach (( array ) $cidrs as $cidr ) {
2024-06-10 20:43:34 +00:00
[ $subnet , $mask ] = explode ( '/' , $cidr );
2024-01-15 10:37:26 +00:00
if ((( ip2long ( $ip ) & ( $mask = ~ (( 1 << ( 32 - $mask )) - 1 ))) == ( ip2long ( $subnet ) & $mask ))) {
$match = $cidr ;
2024-06-10 20:43:34 +00:00
2024-01-15 10:37:26 +00:00
return true ;
}
}
2024-06-10 20:43:34 +00:00
2024-01-15 10:37:26 +00:00
return false ;
2024-01-15 09:03:15 +00:00
}
2025-08-26 08:26:39 +00:00
2025-09-09 07:00:35 +00:00
function checkIPAgainstAllowlist ( $ip , $allowlist )
2024-07-01 09:39:10 +00:00
{
2025-08-26 08:26:39 +00:00
if ( empty ( $allowlist )) {
return false ;
2024-07-01 09:39:10 +00:00
}
2025-08-26 08:26:39 +00:00
foreach (( array ) $allowlist as $allowed ) {
$allowed = trim ( $allowed );
2024-07-01 09:39:10 +00:00
2025-08-26 08:26:39 +00:00
if ( empty ( $allowed )) {
2024-07-01 09:39:10 +00:00
continue ;
}
2024-06-10 20:43:34 +00:00
2025-08-26 08:26:39 +00:00
// Check if it's a CIDR notation
if ( str_contains ( $allowed , '/' )) {
[ $subnet , $mask ] = explode ( '/' , $allowed );
2024-04-16 18:57:54 +00:00
2025-08-26 08:26:39 +00:00
// Special case: 0.0.0.0 with any subnet means allow all
if ( $subnet === '0.0.0.0' ) {
return true ;
}
2024-06-10 20:43:34 +00:00
2025-08-26 08:26:39 +00:00
$mask = ( int ) $mask ;
2026-03-03 16:03:46 +00:00
$isIpv6Subnet = filter_var ( $subnet , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 ) !== false ;
$maxMask = $isIpv6Subnet ? 128 : 32 ;
2024-09-30 21:21:58 +00:00
2026-03-03 16:03:46 +00:00
// Validate mask for address family
if ( $mask < 0 || $mask > $maxMask ) {
2025-08-26 08:26:39 +00:00
continue ;
}
2024-09-30 21:21:58 +00:00
2026-03-03 16:03:46 +00:00
if ( $isIpv6Subnet ) {
// IPv6 CIDR matching using binary string comparison
$ipBin = inet_pton ( $ip );
$subnetBin = inet_pton ( $subnet );
2024-06-10 20:43:34 +00:00
2026-03-03 16:03:46 +00:00
if ( $ipBin === false || $subnetBin === false ) {
continue ;
}
2024-09-30 21:21:58 +00:00
2026-03-03 16:03:46 +00:00
// Build a 128-bit mask from $mask prefix bits
$maskBin = str_repeat ( " \xff " , ( int ) ( $mask / 8 ));
$remainder = $mask % 8 ;
if ( $remainder > 0 ) {
$maskBin .= chr ( 0xFF & ( 0xFF << ( 8 - $remainder )));
}
$maskBin = str_pad ( $maskBin , 16 , " \x00 " );
2024-06-10 20:43:34 +00:00
2026-03-03 16:03:46 +00:00
if (( $ipBin & $maskBin ) === ( $subnetBin & $maskBin )) {
return true ;
}
} else {
// IPv4 CIDR matching
$ip_long = ip2long ( $ip );
$subnet_long = ip2long ( $subnet );
if ( $ip_long === false || $subnet_long === false ) {
continue ;
}
$mask_long = ~ (( 1 << ( 32 - $mask )) - 1 );
if (( $ip_long & $mask_long ) == ( $subnet_long & $mask_long )) {
return true ;
}
2025-08-26 08:26:39 +00:00
}
} else {
// Special case: 0.0.0.0 means allow all
if ( $allowed === '0.0.0.0' ) {
return true ;
}
2024-04-16 18:57:54 +00:00
2025-08-26 08:26:39 +00:00
// Direct IP comparison
if ( $ip === $allowed ) {
return true ;
}
}
2024-04-16 19:22:57 +00:00
}
2025-08-26 08:26:39 +00:00
return false ;
2024-04-16 19:22:57 +00:00
}
2026-03-03 16:03:46 +00:00
function deduplicateAllowlist ( array $entries ) : array
{
if ( count ( $entries ) <= 1 ) {
return array_values ( $entries );
}
// Normalize each entry into [original, ip, mask]
$parsed = [];
foreach ( $entries as $entry ) {
$entry = trim ( $entry );
if ( empty ( $entry )) {
continue ;
}
if ( $entry === '0.0.0.0' ) {
// Special case: bare 0.0.0.0 means "allow all" — treat as /0
$parsed [] = [ 'original' => $entry , 'ip' => '0.0.0.0' , 'mask' => 0 ];
} elseif ( str_contains ( $entry , '/' )) {
[ $ip , $mask ] = explode ( '/' , $entry );
$parsed [] = [ 'original' => $entry , 'ip' => $ip , 'mask' => ( int ) $mask ];
} else {
$ip = $entry ;
$isIpv6 = filter_var ( $ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 ) !== false ;
$parsed [] = [ 'original' => $entry , 'ip' => $ip , 'mask' => $isIpv6 ? 128 : 32 ];
}
}
$count = count ( $parsed );
$redundant = array_fill ( 0 , $count , false );
for ( $i = 0 ; $i < $count ; $i ++ ) {
if ( $redundant [ $i ]) {
continue ;
}
for ( $j = 0 ; $j < $count ; $j ++ ) {
if ( $i === $j || $redundant [ $j ]) {
continue ;
}
// Entry $j is redundant if its mask is narrower/equal (>=) than $i's mask
// AND $j's network IP falls within $i's CIDR range
if ( $parsed [ $j ][ 'mask' ] >= $parsed [ $i ][ 'mask' ]) {
$cidr = $parsed [ $i ][ 'ip' ] . '/' . $parsed [ $i ][ 'mask' ];
if ( checkIPAgainstAllowlist ( $parsed [ $j ][ 'ip' ], [ $cidr ])) {
$redundant [ $j ] = true ;
}
}
}
}
$result = [];
for ( $i = 0 ; $i < $count ; $i ++ ) {
if ( ! $redundant [ $i ]) {
$result [] = $parsed [ $i ][ 'original' ];
}
}
return $result ;
}
2024-04-25 11:52:52 +00:00
function get_public_ips ()
{
try {
[ $first , $second ] = Process :: concurrently ( function ( Pool $pool ) {
$pool -> path ( __DIR__ ) -> command ( 'curl -4s https://ifconfig.io' );
$pool -> path ( __DIR__ ) -> command ( 'curl -6s https://ifconfig.io' );
});
$ipv4 = $first -> output ();
if ( $ipv4 ) {
$ipv4 = trim ( $ipv4 );
2026-01-05 21:12:19 +00:00
$validate_ipv4 = filter_var ( $ipv4 , FILTER_VALIDATE_IP , FILTER_FLAG_IPV4 );
2024-04-25 11:52:52 +00:00
if ( $validate_ipv4 == false ) {
echo " Invalid ipv4: $ipv4\n " ;
2024-06-10 20:43:34 +00:00
2024-04-25 11:52:52 +00:00
return ;
}
2024-09-29 18:12:30 +00:00
InstanceSettings :: get () -> update ([ 'public_ipv4' => $ipv4 ]);
2024-04-25 11:52:52 +00:00
}
2024-09-23 18:31:50 +00:00
} catch ( \Exception $e ) {
echo " Error: { $e -> getMessage () } \n " ;
}
try {
2024-04-25 11:52:52 +00:00
$ipv6 = $second -> output ();
if ( $ipv6 ) {
$ipv6 = trim ( $ipv6 );
2026-01-05 21:12:19 +00:00
$validate_ipv6 = filter_var ( $ipv6 , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 );
2024-04-25 11:52:52 +00:00
if ( $validate_ipv6 == false ) {
echo " Invalid ipv6: $ipv6\n " ;
2024-06-10 20:43:34 +00:00
2024-04-25 11:52:52 +00:00
return ;
}
2024-09-29 18:12:30 +00:00
InstanceSettings :: get () -> update ([ 'public_ipv6' => $ipv6 ]);
2024-04-25 11:52:52 +00:00
}
} catch ( \Throwable $e ) {
echo " Error: { $e -> getMessage () } \n " ;
}
}
2024-06-11 10:37:39 +00:00
2024-06-11 10:38:24 +00:00
function isAnyDeploymentInprogress ()
{
2025-01-10 17:34:16 +00:00
$runningJobs = ApplicationDeploymentQueue :: where ( 'horizon_job_worker' , gethostname ()) -> where ( 'status' , ApplicationDeploymentStatus :: IN_PROGRESS -> value ) -> get ();
2025-09-15 12:10:20 +00:00
if ( $runningJobs -> isEmpty ()) {
echo " No deployments in progress. \n " ;
exit ( 0 );
}
2025-01-10 17:34:16 +00:00
$horizonJobIds = [];
2025-09-15 12:10:20 +00:00
$deploymentDetails = [];
2025-01-10 17:34:16 +00:00
foreach ( $runningJobs as $runningJob ) {
2025-01-10 18:53:13 +00:00
$horizonJobStatus = getJobStatus ( $runningJob -> horizon_job_id );
2025-03-28 14:42:25 +00:00
if ( $horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved' ) {
2025-03-28 14:09:38 +00:00
$horizonJobIds [] = $runningJob -> horizon_job_id ;
2025-09-15 12:10:20 +00:00
// Get application and team information
$application = Application :: find ( $runningJob -> application_id );
$teamMembers = [];
$deploymentUrl = '' ;
if ( $application ) {
// Get team members through the application's project
$team = $application -> team ();
if ( $team ) {
$teamMembers = $team -> members () -> pluck ( 'email' ) -> toArray ();
}
// Construct the full deployment URL
if ( $runningJob -> deployment_url ) {
$baseUrl = base_url ();
$deploymentUrl = $baseUrl . $runningJob -> deployment_url ;
}
}
$deploymentDetails [] = [
'id' => $runningJob -> id ,
'application_name' => $runningJob -> application_name ? ? 'Unknown' ,
'server_name' => $runningJob -> server_name ? ? 'Unknown' ,
'deployment_url' => $deploymentUrl ,
'team_members' => $teamMembers ,
'created_at' => $runningJob -> created_at -> format ( 'Y-m-d H:i:s' ),
'horizon_job_id' => $runningJob -> horizon_job_id ,
];
2025-01-10 17:34:16 +00:00
}
}
2025-09-15 12:10:20 +00:00
2025-01-10 17:34:16 +00:00
if ( count ( $horizonJobIds ) === 0 ) {
2025-09-15 12:10:20 +00:00
echo " No active deployments in progress (all jobs completed or failed). \n " ;
2025-01-10 17:34:16 +00:00
exit ( 0 );
}
2025-09-15 12:10:20 +00:00
// Display enhanced deployment information
echo " \n === Running Deployments === \n " ;
echo 'Total active deployments: ' . count ( $horizonJobIds ) . " \n \n " ;
foreach ( $deploymentDetails as $index => $deployment ) {
echo 'Deployment #' . ( $index + 1 ) . " : \n " ;
echo ' Application: ' . $deployment [ 'application_name' ] . " \n " ;
echo ' Server: ' . $deployment [ 'server_name' ] . " \n " ;
echo ' Started: ' . $deployment [ 'created_at' ] . " \n " ;
if ( $deployment [ 'deployment_url' ]) {
echo ' URL: ' . $deployment [ 'deployment_url' ] . " \n " ;
}
if ( ! empty ( $deployment [ 'team_members' ])) {
echo ' Team members: ' . implode ( ', ' , $deployment [ 'team_members' ]) . " \n " ;
} else {
echo " Team members: No team members found \n " ;
}
echo ' Horizon Job ID: ' . $deployment [ 'horizon_job_id' ] . " \n " ;
echo " \n " ;
}
2025-01-10 17:34:16 +00:00
exit ( 1 );
2024-06-11 10:37:39 +00:00
}
2024-06-18 14:42:42 +00:00
2024-06-25 19:22:23 +00:00
function isBase64Encoded ( $strValue )
{
return base64_encode ( base64_decode ( $strValue , true )) === $strValue ;
}
2024-06-26 11:32:36 +00:00
function customApiValidator ( Collection | array $item , array $rules )
{
if ( is_array ( $item )) {
$item = collect ( $item );
}
return Validator :: make ( $item -> toArray (), $rules , [
'required' => 'This field is required.' ,
]);
}
2024-08-23 18:54:38 +00:00
function parseDockerComposeFile ( Service | Application $resource , bool $isNew = false , int $pull_request_id = 0 , ? int $preview_id = null )
{
2024-10-28 13:56:13 +00:00
if ( $resource -> getMorphClass () === \App\Models\Service :: class ) {
2024-08-23 18:54:38 +00:00
if ( $resource -> docker_compose_raw ) {
2025-11-25 09:57:07 +00:00
// Extract inline comments from raw YAML before Symfony parser discards them
$envComments = extractYamlEnvironmentComments ( $resource -> docker_compose_raw );
2024-08-23 18:54:38 +00:00
try {
$yaml = Yaml :: parse ( $resource -> docker_compose_raw );
} catch ( \Exception $e ) {
2025-05-29 08:49:55 +00:00
throw new \RuntimeException ( $e -> getMessage ());
2024-08-23 18:54:38 +00:00
}
$allServices = get_service_templates ();
$topLevelVolumes = collect ( data_get ( $yaml , 'volumes' , []));
$topLevelNetworks = collect ( data_get ( $yaml , 'networks' , []));
$topLevelConfigs = collect ( data_get ( $yaml , 'configs' , []));
$topLevelSecrets = collect ( data_get ( $yaml , 'secrets' , []));
$services = data_get ( $yaml , 'services' );
$generatedServiceFQDNS = collect ([]);
if ( is_null ( $resource -> destination )) {
$destination = $resource -> server -> destinations () -> first ();
if ( $destination ) {
$resource -> destination () -> associate ( $destination );
$resource -> save ();
}
}
$definedNetwork = collect ([ $resource -> uuid ]);
if ( $topLevelVolumes -> count () > 0 ) {
$tempTopLevelVolumes = collect ([]);
foreach ( $topLevelVolumes as $volumeName => $volume ) {
if ( is_null ( $volume )) {
continue ;
}
$tempTopLevelVolumes -> put ( $volumeName , $volume );
}
$topLevelVolumes = collect ( $tempTopLevelVolumes );
}
2025-11-25 09:57:07 +00:00
$services = collect ( $services ) -> map ( function ( $service , $serviceName ) use ( $topLevelVolumes , $topLevelNetworks , $definedNetwork , $isNew , $generatedServiceFQDNS , $resource , $allServices , $envComments ) {
2024-08-23 18:54:38 +00:00
// Workarounds for beta users.
if ( $serviceName === 'registry' ) {
$tempServiceName = 'docker-registry' ;
} else {
$tempServiceName = $serviceName ;
}
if ( str ( data_get ( $service , 'image' )) -> contains ( 'glitchtip' )) {
$tempServiceName = 'glitchtip' ;
}
if ( $serviceName === 'supabase-kong' ) {
$tempServiceName = 'supabase' ;
}
$serviceDefinition = data_get ( $allServices , $tempServiceName );
$predefinedPort = data_get ( $serviceDefinition , 'port' );
if ( $serviceName === 'plausible' ) {
$predefinedPort = '8000' ;
}
// End of workarounds for beta users.
$serviceVolumes = collect ( data_get ( $service , 'volumes' , []));
$servicePorts = collect ( data_get ( $service , 'ports' , []));
$serviceNetworks = collect ( data_get ( $service , 'networks' , []));
$serviceVariables = collect ( data_get ( $service , 'environment' , []));
$serviceLabels = collect ( data_get ( $service , 'labels' , []));
2025-07-20 20:15:42 +00:00
$networkMode = data_get ( $service , 'network_mode' );
$hasValidNetworkMode =
$networkMode === 'host' ||
( is_string ( $networkMode ) && ( str_starts_with ( $networkMode , 'service:' ) || str_starts_with ( $networkMode , 'container:' )));
2024-08-23 18:54:38 +00:00
if ( $serviceLabels -> count () > 0 ) {
$removedLabels = collect ([]);
$serviceLabels = $serviceLabels -> filter ( function ( $serviceLabel , $serviceLabelName ) use ( $removedLabels ) {
2025-10-16 06:51:15 +00:00
// Handle array values from YAML (e.g., "traefik.enable: true" becomes an array)
if ( is_array ( $serviceLabel )) {
$removedLabels -> put ( $serviceLabelName , $serviceLabel );
return false ;
}
2024-12-09 14:38:21 +00:00
if ( ! str ( $serviceLabel ) -> contains ( '=' )) {
2024-08-23 18:54:38 +00:00
$removedLabels -> put ( $serviceLabelName , $serviceLabel );
return false ;
}
return $serviceLabel ;
});
foreach ( $removedLabels as $removedLabelName => $removedLabel ) {
2025-10-16 06:51:15 +00:00
// Convert array values to strings
if ( is_array ( $removedLabel )) {
$removedLabel = ( string ) collect ( $removedLabel ) -> first ();
}
2024-08-23 18:54:38 +00:00
$serviceLabels -> push ( " $removedLabelName = $removedLabel " );
}
}
$containerName = " $serviceName - { $resource -> uuid } " ;
// Decide if the service is a database
$image = data_get_str ( $service , 'image' );
2025-11-11 22:24:53 +00:00
// Check for manually migrated services first (respects user's conversion choice)
$migratedApp = ServiceApplication :: where ( 'name' , $serviceName )
-> where ( 'service_id' , $resource -> id )
-> where ( 'is_migrated' , true )
-> first ();
$migratedDb = ServiceDatabase :: where ( 'name' , $serviceName )
-> where ( 'service_id' , $resource -> id )
-> where ( 'is_migrated' , true )
-> first ();
if ( $migratedApp || $migratedDb ) {
// Use the migrated service type, ignoring image detection
$isDatabase = ( bool ) $migratedDb ;
$savedService = $migratedApp ? : $migratedDb ;
} else {
// Use image detection for non-migrated services
$isDatabase = isDatabaseImage ( $image , $service );
// Create new serviceApplication or serviceDatabase
if ( $isDatabase ) {
if ( $isNew ) {
2025-10-14 08:12:36 +00:00
$savedService = ServiceDatabase :: create ([
'name' => $serviceName ,
'image' => $image ,
'service_id' => $resource -> id ,
]);
2025-11-11 22:24:53 +00:00
} else {
$savedService = ServiceDatabase :: where ([
'name' => $serviceName ,
'service_id' => $resource -> id ,
]) -> first ();
if ( is_null ( $savedService )) {
$savedService = ServiceDatabase :: create ([
'name' => $serviceName ,
'image' => $image ,
'service_id' => $resource -> id ,
]);
}
2025-10-14 08:12:36 +00:00
}
2024-08-23 18:54:38 +00:00
} else {
2025-11-11 22:24:53 +00:00
if ( $isNew ) {
2025-10-14 08:12:36 +00:00
$savedService = ServiceApplication :: create ([
'name' => $serviceName ,
'image' => $image ,
'service_id' => $resource -> id ,
]);
2025-11-11 22:24:53 +00:00
} else {
$savedService = ServiceApplication :: where ([
'name' => $serviceName ,
'service_id' => $resource -> id ,
]) -> first ();
if ( is_null ( $savedService )) {
$savedService = ServiceApplication :: create ([
'name' => $serviceName ,
'image' => $image ,
'service_id' => $resource -> id ,
]);
}
2025-10-14 08:12:36 +00:00
}
2024-08-23 18:54:38 +00:00
}
}
2025-11-11 22:24:53 +00:00
data_set ( $service , 'is_database' , $isDatabase );
2024-08-23 18:54:38 +00:00
// Check if image changed
if ( $savedService -> image !== $image ) {
$savedService -> image = $image ;
$savedService -> save ();
}
// Collect/create/update networks
if ( $serviceNetworks -> count () > 0 ) {
foreach ( $serviceNetworks as $networkName => $networkDetails ) {
if ( $networkName === 'default' ) {
continue ;
}
// ignore alias
if ( $networkDetails [ 'aliases' ] ? ? false ) {
continue ;
}
$networkExists = $topLevelNetworks -> contains ( function ( $value , $key ) use ( $networkName ) {
return $value == $networkName || $key == $networkName ;
});
2024-12-09 14:38:21 +00:00
if ( ! $networkExists ) {
2024-10-03 13:04:40 +00:00
if ( is_string ( $networkDetails ) || is_int ( $networkDetails )) {
$topLevelNetworks -> put ( $networkDetails , null );
}
2024-08-23 18:54:38 +00:00
}
}
}
// Collect/create/update ports
$collectedPorts = collect ([]);
if ( $servicePorts -> count () > 0 ) {
foreach ( $servicePorts as $sport ) {
if ( is_string ( $sport ) || is_numeric ( $sport )) {
$collectedPorts -> push ( $sport );
}
if ( is_array ( $sport )) {
$target = data_get ( $sport , 'target' );
$published = data_get ( $sport , 'published' );
$protocol = data_get ( $sport , 'protocol' );
$collectedPorts -> push ( " $target : $published / $protocol " );
}
}
}
$savedService -> ports = $collectedPorts -> implode ( ',' );
$savedService -> save ();
2025-07-20 20:15:42 +00:00
if ( ! $hasValidNetworkMode ) {
2024-08-23 18:54:38 +00:00
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks -> contains ( function ( $value , $_ ) use ( $definedNetwork ) {
return $value == $definedNetwork ;
});
2024-12-09 14:38:21 +00:00
if ( ! $definedNetworkExists ) {
2024-08-23 18:54:38 +00:00
foreach ( $definedNetwork as $network ) {
$topLevelNetworks -> put ( $network , [
'name' => $network ,
'external' => true ,
]);
}
}
$networks = collect ();
foreach ( $serviceNetworks as $key => $serviceNetwork ) {
if ( gettype ( $serviceNetwork ) === 'string' ) {
// networks:
// - appwrite
$networks -> put ( $serviceNetwork , null );
} elseif ( gettype ( $serviceNetwork ) === 'array' ) {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
$networks -> put ( $key , $serviceNetwork );
}
}
foreach ( $definedNetwork as $key => $network ) {
$networks -> put ( $network , null );
}
data_set ( $service , 'networks' , $networks -> toArray ());
}
// Collect/create/update volumes
if ( $serviceVolumes -> count () > 0 ) {
$serviceVolumes = $serviceVolumes -> map ( function ( $volume ) use ( $savedService , $topLevelVolumes ) {
$type = null ;
$source = null ;
$target = null ;
$content = null ;
$isDirectory = false ;
if ( is_string ( $volume )) {
$source = str ( $volume ) -> before ( ':' );
$target = str ( $volume ) -> after ( ':' ) -> beforeLast ( ':' );
if ( $source -> startsWith ( './' ) || $source -> startsWith ( '/' ) || $source -> startsWith ( '~' )) {
$type = str ( 'bind' );
// By default, we cannot determine if the bind is a directory or not, so we set it to directory
$isDirectory = true ;
} else {
$type = str ( 'volume' );
}
} elseif ( is_array ( $volume )) {
$type = data_get_str ( $volume , 'type' );
$source = data_get_str ( $volume , 'source' );
$target = data_get_str ( $volume , 'target' );
$content = data_get ( $volume , 'content' );
$isDirectory = ( bool ) data_get ( $volume , 'isDirectory' , null ) || ( bool ) data_get ( $volume , 'is_directory' , null );
$foundConfig = $savedService -> fileStorages () -> whereMountPath ( $target ) -> first ();
if ( $foundConfig ) {
$contentNotNull = data_get ( $foundConfig , 'content' );
if ( $contentNotNull ) {
$content = $contentNotNull ;
}
$isDirectory = ( bool ) data_get ( $volume , 'isDirectory' , null ) || ( bool ) data_get ( $volume , 'is_directory' , null );
}
if ( is_null ( $isDirectory ) && is_null ( $content )) {
// if isDirectory is not set & content is also not set, we assume it is a directory
$isDirectory = true ;
}
}
if ( $type ? -> value () === 'bind' ) {
if ( $source -> value () === '/var/run/docker.sock' ) {
return $volume ;
}
if ( $source -> value () === '/tmp' || $source -> value () === '/tmp/' ) {
return $volume ;
}
2025-03-28 21:10:33 +00:00
2025-03-29 21:16:12 +00:00
LocalFileVolume :: updateOrCreate (
[
2024-08-23 18:54:38 +00:00
'mount_path' => $target ,
'resource_id' => $savedService -> id ,
'resource_type' => get_class ( $savedService ),
2025-03-29 21:16:12 +00:00
],
[
2024-08-23 18:54:38 +00:00
'fs_path' => $source ,
'mount_path' => $target ,
'content' => $content ,
'is_directory' => $isDirectory ,
'resource_id' => $savedService -> id ,
'resource_type' => get_class ( $savedService ),
2025-03-29 21:16:12 +00:00
]
);
2024-08-23 18:54:38 +00:00
} elseif ( $type -> value () === 'volume' ) {
if ( $topLevelVolumes -> has ( $source -> value ())) {
$v = $topLevelVolumes -> get ( $source -> value ());
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
return $volume ;
}
}
$slugWithoutUuid = Str :: slug ( $source , '-' );
$name = " { $savedService -> service -> uuid } _ { $slugWithoutUuid } " ;
if ( is_string ( $volume )) {
$source = str ( $volume ) -> before ( ':' );
$target = str ( $volume ) -> after ( ':' ) -> beforeLast ( ':' );
$source = $name ;
$volume = " $source : $target " ;
} elseif ( is_array ( $volume )) {
data_set ( $volume , 'source' , $name );
}
$topLevelVolumes -> put ( $name , [
'name' => $name ,
]);
LocalPersistentVolume :: updateOrCreate (
[
'mount_path' => $target ,
'resource_id' => $savedService -> id ,
'resource_type' => get_class ( $savedService ),
],
[
'name' => $name ,
'mount_path' => $target ,
'resource_id' => $savedService -> id ,
'resource_type' => get_class ( $savedService ),
]
);
}
dispatch ( new ServerFilesFromServerJob ( $savedService ));
return $volume ;
});
data_set ( $service , 'volumes' , $serviceVolumes -> toArray ());
}
2024-08-24 09:00:27 +00:00
// convert - SESSION_SECRET: 123 to - SESSION_SECRET=123
$convertedServiceVariables = collect ([]);
foreach ( $serviceVariables as $variableName => $variable ) {
if ( is_numeric ( $variableName )) {
if ( is_array ( $variable )) {
$key = str ( collect ( $variable ) -> keys () -> first ());
$value = str ( collect ( $variable ) -> values () -> first ());
$variable = " $key = $value " ;
$convertedServiceVariables -> put ( $variableName , $variable );
} elseif ( is_string ( $variable )) {
$convertedServiceVariables -> put ( $variableName , $variable );
}
2024-08-27 14:02:52 +00:00
} elseif ( is_string ( $variableName )) {
$convertedServiceVariables -> put ( $variableName , $variable );
2024-08-24 09:00:27 +00:00
}
}
$serviceVariables = $convertedServiceVariables ;
2024-08-23 18:54:38 +00:00
// Get variables from the service
foreach ( $serviceVariables as $variableName => $variable ) {
if ( is_numeric ( $variableName )) {
if ( is_array ( $variable )) {
// - SESSION_SECRET: 123
// - SESSION_SECRET:
$key = str ( collect ( $variable ) -> keys () -> first ());
$value = str ( collect ( $variable ) -> values () -> first ());
} else {
$variable = str ( $variable );
if ( $variable -> contains ( '=' )) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable -> before ( '=' );
$value = $variable -> after ( '=' );
} else {
// - SESSION_SECRET
$key = $variable ;
$value = null ;
}
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
$key = str ( $variableName );
$value = str ( $variable );
}
2025-11-25 09:57:07 +00:00
// Preserve original key for comment lookup before $key might be reassigned
$originalKey = $key -> value ();
2024-08-23 18:54:38 +00:00
if ( $key -> startsWith ( 'SERVICE_FQDN' )) {
if ( $isNew || $savedService -> fqdn === null ) {
$name = $key -> after ( 'SERVICE_FQDN_' ) -> beforeLast ( '_' ) -> lower ();
$fqdn = generateFqdn ( $resource -> server , " { $name -> value () } - { $resource -> uuid } " );
if ( substr_count ( $key -> value (), '_' ) === 3 ) {
// SERVICE_FQDN_UMAMI_1000
$port = $key -> afterLast ( '_' );
} else {
$last = $key -> afterLast ( '_' );
if ( is_numeric ( $last -> value ())) {
// SERVICE_FQDN_3001
$port = $last ;
} else {
// SERVICE_FQDN_UMAMI
$port = null ;
}
}
if ( $port ) {
$fqdn = " $fqdn : $port " ;
}
if ( substr_count ( $key -> value (), '_' ) >= 2 ) {
if ( $value ) {
$path = $value -> value ();
} else {
$path = null ;
}
if ( $generatedServiceFQDNS -> count () > 0 ) {
$alreadyGenerated = $generatedServiceFQDNS -> has ( $key -> value ());
if ( $alreadyGenerated ) {
$fqdn = $generatedServiceFQDNS -> get ( $key -> value ());
} else {
$generatedServiceFQDNS -> put ( $key -> value (), $fqdn );
}
} else {
$generatedServiceFQDNS -> put ( $key -> value (), $fqdn );
}
$fqdn = " $fqdn $path " ;
}
2024-12-09 14:38:21 +00:00
if ( ! $isDatabase ) {
2024-08-23 18:54:38 +00:00
if ( $savedService -> fqdn ) {
2024-12-09 14:38:21 +00:00
data_set ( $savedService , 'fqdn' , $savedService -> fqdn . ',' . $fqdn );
2024-08-23 18:54:38 +00:00
} else {
data_set ( $savedService , 'fqdn' , $fqdn );
}
$savedService -> save ();
}
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $fqdn ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
2025-11-25 09:57:07 +00:00
'comment' => $envComments [ $originalKey ] ? ? null ,
2024-08-23 18:54:38 +00:00
]);
}
// Caddy needs exact port in some cases.
2024-12-09 14:38:21 +00:00
if ( $predefinedPort && ! $key -> endsWith ( " _ { $predefinedPort } " )) {
2024-08-23 18:54:38 +00:00
$fqdns_exploded = str ( $savedService -> fqdn ) -> explode ( ',' );
if ( $fqdns_exploded -> count () > 1 ) {
continue ;
}
$env = EnvironmentVariable :: where ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
if ( $env ) {
$env_url = Url :: fromString ( $savedService -> fqdn );
$env_port = $env_url -> getPort ();
if ( $env_port !== $predefinedPort ) {
$env_url = $env_url -> withPort ( $predefinedPort );
$savedService -> fqdn = $env_url -> __toString ();
$savedService -> save ();
}
}
}
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue ;
}
if ( $value ? -> startsWith ( '$' )) {
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
2024-09-09 13:04:51 +00:00
$value = replaceVariables ( $value );
2024-08-23 18:54:38 +00:00
$key = $value ;
if ( $value -> startsWith ( 'SERVICE_' )) {
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
[ 'command' => $command , 'forService' => $forService , 'generatedValue' => $generatedValue , 'port' => $port ] = parseEnvVariable ( $value );
2024-12-09 14:38:21 +00:00
if ( ! is_null ( $command )) {
2024-08-23 18:54:38 +00:00
if ( $command ? -> value () === 'FQDN' || $command ? -> value () === 'URL' ) {
if ( Str :: lower ( $forService ) === $serviceName ) {
$fqdn = generateFqdn ( $resource -> server , $containerName );
} else {
2024-12-09 14:38:21 +00:00
$fqdn = generateFqdn ( $resource -> server , Str :: lower ( $forService ) . '-' . $resource -> uuid );
2024-08-23 18:54:38 +00:00
}
if ( $port ) {
$fqdn = " $fqdn : $port " ;
}
if ( $foundEnv ) {
$fqdn = data_get ( $foundEnv , 'value' );
// if ($savedService->fqdn) {
// $savedServiceFqdn = Url::fromString($savedService->fqdn);
// $parsedFqdn = Url::fromString($fqdn);
// $savedServicePath = $savedServiceFqdn->getPath();
// $parsedFqdnPath = $parsedFqdn->getPath();
// if ($savedServicePath != $parsedFqdnPath) {
// $fqdn = $parsedFqdn->withPath($savedServicePath)->__toString();
// $foundEnv->value = $fqdn;
// $foundEnv->save();
// }
// }
} else {
if ( $command -> value () === 'URL' ) {
$fqdn = str ( $fqdn ) -> after ( '://' ) -> value ();
}
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $fqdn ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
2025-11-25 09:57:07 +00:00
'comment' => $envComments [ $originalKey ] ? ? null ,
2024-08-23 18:54:38 +00:00
]);
}
2024-12-09 14:38:21 +00:00
if ( ! $isDatabase ) {
if ( $command -> value () === 'FQDN' && is_null ( $savedService -> fqdn ) && ! $foundEnv ) {
2024-08-23 18:54:38 +00:00
$savedService -> fqdn = $fqdn ;
$savedService -> save ();
}
// Caddy needs exact port in some cases.
2024-12-09 14:38:21 +00:00
if ( $predefinedPort && ! $key -> endsWith ( " _ { $predefinedPort } " ) && $command ? -> value () === 'FQDN' && $resource -> server -> proxyType () === 'CADDY' ) {
2024-08-23 18:54:38 +00:00
$fqdns_exploded = str ( $savedService -> fqdn ) -> explode ( ',' );
if ( $fqdns_exploded -> count () > 1 ) {
continue ;
}
$env = EnvironmentVariable :: where ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
if ( $env ) {
$env_url = Url :: fromString ( $env -> value );
$env_port = $env_url -> getPort ();
if ( $env_port !== $predefinedPort ) {
$env_url = $env_url -> withPort ( $predefinedPort );
$savedService -> fqdn = $env_url -> __toString ();
$savedService -> save ();
}
}
}
}
} else {
$generatedValue = generateEnvValue ( $command , $resource );
2024-12-09 14:38:21 +00:00
if ( ! $foundEnv ) {
2024-08-23 18:54:38 +00:00
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $generatedValue ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
2025-11-25 09:57:07 +00:00
'comment' => $envComments [ $originalKey ] ? ? null ,
2024-08-23 18:54:38 +00:00
]);
}
}
}
} else {
if ( $value -> contains ( ':-' )) {
$key = $value -> before ( ':' );
$defaultValue = $value -> after ( ':-' );
} elseif ( $value -> contains ( '-' )) {
$key = $value -> before ( '-' );
$defaultValue = $value -> after ( '-' );
} elseif ( $value -> contains ( ':?' )) {
$key = $value -> before ( ':' );
$defaultValue = $value -> after ( ':?' );
} elseif ( $value -> contains ( '?' )) {
$key = $value -> before ( '?' );
$defaultValue = $value -> after ( '?' );
} else {
$key = $value ;
$defaultValue = null ;
}
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
if ( $foundEnv ) {
$defaultValue = data_get ( $foundEnv , 'value' );
}
EnvironmentVariable :: updateOrCreate ([
'key' => $key ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
], [
'value' => $defaultValue ,
2024-12-17 09:38:32 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
2025-11-25 09:57:07 +00:00
'comment' => $envComments [ $originalKey ] ? ? null ,
2024-08-23 18:54:38 +00:00
]);
}
}
}
// Add labels to the service
if ( $savedService -> serviceType ()) {
$fqdns = generateServiceSpecificFqdns ( $savedService );
} else {
$fqdns = collect ( data_get ( $savedService , 'fqdns' )) -> filter ();
}
2024-11-17 21:49:44 +00:00
$defaultLabels = defaultLabels (
id : $resource -> id ,
name : $containerName ,
projectName : $resource -> project () -> name ,
resourceName : $resource -> name ,
type : 'service' ,
2025-01-20 12:58:52 +00:00
subType : $isDatabase ? 'database' : 'application' ,
2024-11-17 21:49:44 +00:00
subId : $savedService -> id ,
subName : $savedService -> name ,
2024-12-02 17:38:21 +00:00
environment : $resource -> environment -> name ,
2024-11-17 21:49:44 +00:00
);
2024-08-23 18:54:38 +00:00
$serviceLabels = $serviceLabels -> merge ( $defaultLabels );
2024-12-09 14:38:21 +00:00
if ( ! $isDatabase && $fqdns -> count () > 0 ) {
2024-08-23 18:54:38 +00:00
if ( $fqdns ) {
$shouldGenerateLabelsExactly = $resource -> server -> settings -> generate_exact_labels ;
if ( $shouldGenerateLabelsExactly ) {
switch ( $resource -> server -> proxyType ()) {
case ProxyTypes :: TRAEFIK -> value :
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge ( fqdnLabelsForTraefik (
2024-08-23 18:54:38 +00:00
uuid : $resource -> uuid ,
domains : $fqdns ,
is_force_https_enabled : true ,
serviceLabels : $serviceLabels ,
is_gzip_enabled : $savedService -> isGzipEnabled (),
is_stripprefix_enabled : $savedService -> isStripprefixEnabled (),
service_name : $serviceName ,
image : data_get ( $service , 'image' )
2025-02-28 19:25:19 +00:00
));
2024-08-23 18:54:38 +00:00
break ;
case ProxyTypes :: CADDY -> value :
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge ( fqdnLabelsForCaddy (
2024-08-23 18:54:38 +00:00
network : $resource -> destination -> network ,
uuid : $resource -> uuid ,
domains : $fqdns ,
is_force_https_enabled : true ,
serviceLabels : $serviceLabels ,
is_gzip_enabled : $savedService -> isGzipEnabled (),
is_stripprefix_enabled : $savedService -> isStripprefixEnabled (),
service_name : $serviceName ,
image : data_get ( $service , 'image' )
2025-02-28 19:25:19 +00:00
));
2024-08-23 18:54:38 +00:00
break ;
}
} else {
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge ( fqdnLabelsForTraefik (
2024-08-23 18:54:38 +00:00
uuid : $resource -> uuid ,
domains : $fqdns ,
is_force_https_enabled : true ,
serviceLabels : $serviceLabels ,
is_gzip_enabled : $savedService -> isGzipEnabled (),
is_stripprefix_enabled : $savedService -> isStripprefixEnabled (),
service_name : $serviceName ,
image : data_get ( $service , 'image' )
2025-02-28 19:25:19 +00:00
));
$serviceLabels = $serviceLabels -> merge ( fqdnLabelsForCaddy (
2024-08-23 18:54:38 +00:00
network : $resource -> destination -> network ,
uuid : $resource -> uuid ,
domains : $fqdns ,
is_force_https_enabled : true ,
serviceLabels : $serviceLabels ,
is_gzip_enabled : $savedService -> isGzipEnabled (),
is_stripprefix_enabled : $savedService -> isStripprefixEnabled (),
service_name : $serviceName ,
image : data_get ( $service , 'image' )
2025-02-28 19:25:19 +00:00
));
2024-08-23 18:54:38 +00:00
}
}
}
if ( $resource -> server -> isLogDrainEnabled () && $savedService -> isLogDrainEnabled ()) {
2024-08-28 09:11:14 +00:00
data_set ( $service , 'logging' , generate_fluentd_configuration ());
2024-08-23 18:54:38 +00:00
}
if ( $serviceLabels -> count () > 0 ) {
if ( $resource -> is_container_label_escape_enabled ) {
$serviceLabels = $serviceLabels -> map ( function ( $value , $key ) {
return escapeDollarSign ( $value );
});
}
}
data_set ( $service , 'labels' , $serviceLabels -> toArray ());
data_forget ( $service , 'is_database' );
2024-12-09 14:38:21 +00:00
if ( ! data_get ( $service , 'restart' )) {
2024-08-23 18:54:38 +00:00
data_set ( $service , 'restart' , RESTART_MODE );
}
if ( data_get ( $service , 'restart' ) === 'no' || data_get ( $service , 'exclude_from_hc' )) {
$savedService -> update ([ 'exclude_from_status' => true ]);
}
data_set ( $service , 'container_name' , $containerName );
data_forget ( $service , 'volumes.*.content' );
data_forget ( $service , 'volumes.*.isDirectory' );
data_forget ( $service , 'volumes.*.is_directory' );
data_forget ( $service , 'exclude_from_hc' );
data_set ( $service , 'environment' , $serviceVariables -> toArray ());
updateCompose ( $savedService );
return $service ;
});
$envs_from_coolify = $resource -> environment_variables () -> get ();
$services = collect ( $services ) -> map ( function ( $service , $serviceName ) use ( $resource , $envs_from_coolify ) {
$serviceVariables = collect ( data_get ( $service , 'environment' , []));
$parsedServiceVariables = collect ([]);
foreach ( $serviceVariables as $key => $value ) {
if ( is_numeric ( $key )) {
$value = str ( $value );
if ( $value -> contains ( '=' )) {
$key = $value -> before ( '=' ) -> value ();
$value = $value -> after ( '=' ) -> value ();
} else {
$key = $value -> value ();
$value = null ;
}
$parsedServiceVariables -> put ( $key , $value );
} else {
$parsedServiceVariables -> put ( $key , $value );
}
}
2025-01-24 11:04:34 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_RESOURCE_UUID' , " { $resource -> uuid } " );
2024-08-23 18:54:38 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_CONTAINER_NAME' , " $serviceName - { $resource -> uuid } " );
2024-08-27 17:36:22 +00:00
// TODO: move this in a shared function
2024-12-09 14:38:21 +00:00
if ( ! $parsedServiceVariables -> has ( 'COOLIFY_APP_NAME' )) {
2024-09-13 06:23:05 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_APP_NAME' , " \" { $resource -> name } \" " );
2024-08-27 17:36:22 +00:00
}
2024-12-09 14:38:21 +00:00
if ( ! $parsedServiceVariables -> has ( 'COOLIFY_SERVER_IP' )) {
2024-09-13 06:23:05 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_SERVER_IP' , " \" { $resource -> destination -> server -> ip } \" " );
2024-08-27 17:36:22 +00:00
}
2024-12-09 14:38:21 +00:00
if ( ! $parsedServiceVariables -> has ( 'COOLIFY_ENVIRONMENT_NAME' )) {
2024-09-13 06:23:05 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_ENVIRONMENT_NAME' , " \" { $resource -> environment -> name } \" " );
2024-08-27 17:36:22 +00:00
}
2024-12-09 14:38:21 +00:00
if ( ! $parsedServiceVariables -> has ( 'COOLIFY_PROJECT_NAME' )) {
2024-09-13 06:23:05 +00:00
$parsedServiceVariables -> put ( 'COOLIFY_PROJECT_NAME' , " \" { $resource -> project () -> name } \" " );
2024-08-27 17:36:22 +00:00
}
2024-08-23 18:54:38 +00:00
$parsedServiceVariables = $parsedServiceVariables -> map ( function ( $value , $key ) use ( $envs_from_coolify ) {
2024-12-09 14:38:21 +00:00
if ( ! str ( $value ) -> startsWith ( '$' )) {
2024-08-23 18:54:38 +00:00
$found_env = $envs_from_coolify -> where ( 'key' , $key ) -> first ();
if ( $found_env ) {
return $found_env -> value ;
}
}
return $value ;
});
data_set ( $service , 'environment' , $parsedServiceVariables -> toArray ());
return $service ;
});
$finalServices = [
'services' => $services -> toArray (),
'volumes' => $topLevelVolumes -> toArray (),
'networks' => $topLevelNetworks -> toArray (),
'configs' => $topLevelConfigs -> toArray (),
'secrets' => $topLevelSecrets -> toArray (),
];
$yaml = data_forget ( $yaml , 'services.*.volumes.*.content' );
$resource -> docker_compose_raw = Yaml :: dump ( $yaml , 10 , 2 );
$resource -> docker_compose = Yaml :: dump ( $finalServices , 10 , 2 );
2024-08-26 08:51:01 +00:00
2024-08-23 18:54:38 +00:00
$resource -> save ();
$resource -> saveComposeConfigs ();
return collect ( $finalServices );
} else {
return collect ([]);
}
2024-10-28 13:56:13 +00:00
} elseif ( $resource -> getMorphClass () === \App\Models\Application :: class ) {
2024-08-23 18:54:38 +00:00
try {
$yaml = Yaml :: parse ( $resource -> docker_compose_raw );
2024-10-31 14:19:37 +00:00
} catch ( \Exception ) {
2024-08-23 18:54:38 +00:00
return ;
}
$server = $resource -> destination -> server ;
$topLevelVolumes = collect ( data_get ( $yaml , 'volumes' , []));
if ( $pull_request_id !== 0 ) {
$topLevelVolumes = collect ([]);
}
if ( $topLevelVolumes -> count () > 0 ) {
$tempTopLevelVolumes = collect ([]);
foreach ( $topLevelVolumes as $volumeName => $volume ) {
if ( is_null ( $volume )) {
continue ;
}
$tempTopLevelVolumes -> put ( $volumeName , $volume );
}
$topLevelVolumes = collect ( $tempTopLevelVolumes );
}
$topLevelNetworks = collect ( data_get ( $yaml , 'networks' , []));
$topLevelConfigs = collect ( data_get ( $yaml , 'configs' , []));
$topLevelSecrets = collect ( data_get ( $yaml , 'secrets' , []));
$services = data_get ( $yaml , 'services' );
$generatedServiceFQDNS = collect ([]);
if ( is_null ( $resource -> destination )) {
$destination = $server -> destinations () -> first ();
if ( $destination ) {
$resource -> destination () -> associate ( $destination );
$resource -> save ();
}
}
$definedNetwork = collect ([ $resource -> uuid ]);
if ( $pull_request_id !== 0 ) {
$definedNetwork = collect ([ " { $resource -> uuid } - $pull_request_id " ]);
}
$services = collect ( $services ) -> map ( function ( $service , $serviceName ) use ( $topLevelVolumes , $topLevelNetworks , $definedNetwork , $isNew , $generatedServiceFQDNS , $resource , $server , $pull_request_id , $preview_id ) {
$serviceVolumes = collect ( data_get ( $service , 'volumes' , []));
$servicePorts = collect ( data_get ( $service , 'ports' , []));
$serviceNetworks = collect ( data_get ( $service , 'networks' , []));
$serviceVariables = collect ( data_get ( $service , 'environment' , []));
$serviceDependencies = collect ( data_get ( $service , 'depends_on' , []));
$serviceLabels = collect ( data_get ( $service , 'labels' , []));
$serviceBuildVariables = collect ( data_get ( $service , 'build.args' , []));
$serviceVariables = $serviceVariables -> merge ( $serviceBuildVariables );
if ( $serviceLabels -> count () > 0 ) {
$removedLabels = collect ([]);
$serviceLabels = $serviceLabels -> filter ( function ( $serviceLabel , $serviceLabelName ) use ( $removedLabels ) {
2025-10-16 06:51:15 +00:00
// Handle array values from YAML (e.g., "traefik.enable: true" becomes an array)
if ( is_array ( $serviceLabel )) {
$removedLabels -> put ( $serviceLabelName , $serviceLabel );
return false ;
}
2024-12-09 14:38:21 +00:00
if ( ! str ( $serviceLabel ) -> contains ( '=' )) {
2024-08-23 18:54:38 +00:00
$removedLabels -> put ( $serviceLabelName , $serviceLabel );
return false ;
}
return $serviceLabel ;
});
foreach ( $removedLabels as $removedLabelName => $removedLabel ) {
2025-10-16 06:51:15 +00:00
// Convert array values to strings
if ( is_array ( $removedLabel )) {
$removedLabel = ( string ) collect ( $removedLabel ) -> first ();
}
2024-08-23 18:54:38 +00:00
$serviceLabels -> push ( " $removedLabelName = $removedLabel " );
}
}
$baseName = generateApplicationContainerName ( $resource , $pull_request_id );
$containerName = " $serviceName - $baseName " ;
if ( $resource -> compose_parsing_version === '1' ) {
if ( count ( $serviceVolumes ) > 0 ) {
$serviceVolumes = $serviceVolumes -> map ( function ( $volume ) use ( $resource , $topLevelVolumes , $pull_request_id ) {
if ( is_string ( $volume )) {
$volume = str ( $volume );
2024-12-09 14:38:21 +00:00
if ( $volume -> contains ( ':' ) && ! $volume -> startsWith ( '/' )) {
2024-08-23 18:54:38 +00:00
$name = $volume -> before ( ':' );
$mount = $volume -> after ( ':' );
if ( $name -> startsWith ( '.' ) || $name -> startsWith ( '~' )) {
2024-12-09 14:38:21 +00:00
$dir = base_configuration_dir () . '/applications/' . $resource -> uuid ;
2024-08-23 18:54:38 +00:00
if ( $name -> startsWith ( '.' )) {
$name = $name -> replaceFirst ( '.' , $dir );
}
if ( $name -> startsWith ( '~' )) {
$name = $name -> replaceFirst ( '~' , $dir );
}
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$name = addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
$volume = str ( " $name : $mount " );
} else {
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$name = addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
$volume = str ( " $name : $mount " );
if ( $topLevelVolumes -> has ( $name )) {
$v = $topLevelVolumes -> get ( $name );
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $v , 'name' , $name );
data_set ( $topLevelVolumes , $name , $v );
}
}
} else {
$topLevelVolumes -> put ( $name , [
'name' => $name ,
]);
}
} else {
if ( $topLevelVolumes -> has ( $name -> value ())) {
$v = $topLevelVolumes -> get ( $name -> value ());
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $topLevelVolumes , $name -> value (), $v );
}
}
} else {
$topLevelVolumes -> put ( $name -> value (), [
'name' => $name -> value (),
]);
}
}
}
} else {
if ( $volume -> startsWith ( '/' )) {
$name = $volume -> before ( ':' );
$mount = $volume -> after ( ':' );
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$name = addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
$volume = str ( " $name : $mount " );
}
}
} elseif ( is_array ( $volume )) {
$source = data_get ( $volume , 'source' );
$target = data_get ( $volume , 'target' );
$read_only = data_get ( $volume , 'read_only' );
if ( $source && $target ) {
if (( str ( $source ) -> startsWith ( '.' ) || str ( $source ) -> startsWith ( '~' ))) {
2024-12-09 14:38:21 +00:00
$dir = base_configuration_dir () . '/applications/' . $resource -> uuid ;
2024-08-23 18:54:38 +00:00
if ( str ( $source , '.' )) {
$source = str ( $source ) -> replaceFirst ( '.' , $dir );
}
if ( str ( $source , '~' )) {
$source = str ( $source ) -> replaceFirst ( '~' , $dir );
}
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$source = addPreviewDeploymentSuffix ( $source , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
if ( $read_only ) {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target . ':ro' );
2024-08-23 18:54:38 +00:00
} else {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target );
2024-08-23 18:54:38 +00:00
}
} else {
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$source = addPreviewDeploymentSuffix ( $source , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
if ( $read_only ) {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target . ':ro' );
2024-08-23 18:54:38 +00:00
} else {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target );
2024-08-23 18:54:38 +00:00
}
2024-12-09 14:38:21 +00:00
if ( ! str ( $source ) -> startsWith ( '/' )) {
2024-08-23 18:54:38 +00:00
if ( $topLevelVolumes -> has ( $source )) {
$v = $topLevelVolumes -> get ( $source );
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $v , 'name' , $source );
data_set ( $topLevelVolumes , $source , $v );
}
}
} else {
$topLevelVolumes -> put ( $source , [
'name' => $source ,
]);
}
}
}
}
}
if ( is_array ( $volume )) {
return data_get ( $volume , 'source' );
}
return $volume -> value ();
});
data_set ( $service , 'volumes' , $serviceVolumes -> toArray ());
}
} elseif ( $resource -> compose_parsing_version === '2' ) {
if ( count ( $serviceVolumes ) > 0 ) {
$serviceVolumes = $serviceVolumes -> map ( function ( $volume ) use ( $resource , $topLevelVolumes , $pull_request_id ) {
if ( is_string ( $volume )) {
$volume = str ( $volume );
2024-12-09 14:38:21 +00:00
if ( $volume -> contains ( ':' ) && ! $volume -> startsWith ( '/' )) {
2024-08-23 18:54:38 +00:00
$name = $volume -> before ( ':' );
$mount = $volume -> after ( ':' );
if ( $name -> startsWith ( '.' ) || $name -> startsWith ( '~' )) {
2024-12-09 14:38:21 +00:00
$dir = base_configuration_dir () . '/applications/' . $resource -> uuid ;
2024-08-23 18:54:38 +00:00
if ( $name -> startsWith ( '.' )) {
$name = $name -> replaceFirst ( '.' , $dir );
}
if ( $name -> startsWith ( '~' )) {
$name = $name -> replaceFirst ( '~' , $dir );
}
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$name = addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
$volume = str ( " $name : $mount " );
} else {
if ( $pull_request_id !== 0 ) {
$uuid = $resource -> uuid ;
2025-09-08 13:15:57 +00:00
$name = $uuid . '-' . addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
$volume = str ( " $name : $mount " );
if ( $topLevelVolumes -> has ( $name )) {
$v = $topLevelVolumes -> get ( $name );
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $v , 'name' , $name );
data_set ( $topLevelVolumes , $name , $v );
}
}
} else {
$topLevelVolumes -> put ( $name , [
'name' => $name ,
]);
}
} else {
$uuid = $resource -> uuid ;
2024-12-09 14:38:21 +00:00
$name = str ( $uuid . " - $name " );
2024-08-23 18:54:38 +00:00
$volume = str ( " $name : $mount " );
if ( $topLevelVolumes -> has ( $name -> value ())) {
$v = $topLevelVolumes -> get ( $name -> value ());
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $topLevelVolumes , $name -> value (), $v );
}
}
} else {
$topLevelVolumes -> put ( $name -> value (), [
'name' => $name -> value (),
]);
}
}
}
} else {
if ( $volume -> startsWith ( '/' )) {
$name = $volume -> before ( ':' );
$mount = $volume -> after ( ':' );
if ( $pull_request_id !== 0 ) {
2025-09-08 13:15:57 +00:00
$name = addPreviewDeploymentSuffix ( $name , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
$volume = str ( " $name : $mount " );
}
}
} elseif ( is_array ( $volume )) {
$source = data_get ( $volume , 'source' );
$target = data_get ( $volume , 'target' );
$read_only = data_get ( $volume , 'read_only' );
if ( $source && $target ) {
$uuid = $resource -> uuid ;
if (( str ( $source ) -> startsWith ( '.' ) || str ( $source ) -> startsWith ( '~' ) || str ( $source ) -> startsWith ( '/' ))) {
2024-12-09 14:38:21 +00:00
$dir = base_configuration_dir () . '/applications/' . $resource -> uuid ;
2024-08-23 18:54:38 +00:00
if ( str ( $source , '.' )) {
$source = str ( $source ) -> replaceFirst ( '.' , $dir );
}
if ( str ( $source , '~' )) {
$source = str ( $source ) -> replaceFirst ( '~' , $dir );
}
if ( $read_only ) {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target . ':ro' );
2024-08-23 18:54:38 +00:00
} else {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target );
2024-08-23 18:54:38 +00:00
}
} else {
if ( $pull_request_id === 0 ) {
2024-12-09 14:38:21 +00:00
$source = $uuid . " - $source " ;
2024-08-23 18:54:38 +00:00
} else {
2025-09-08 13:15:57 +00:00
$source = $uuid . '-' . addPreviewDeploymentSuffix ( $source , $pull_request_id );
2024-08-23 18:54:38 +00:00
}
if ( $read_only ) {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target . ':ro' );
2024-08-23 18:54:38 +00:00
} else {
2024-12-09 14:38:21 +00:00
data_set ( $volume , 'source' , $source . ':' . $target );
2024-08-23 18:54:38 +00:00
}
2024-12-09 14:38:21 +00:00
if ( ! str ( $source ) -> startsWith ( '/' )) {
2024-08-23 18:54:38 +00:00
if ( $topLevelVolumes -> has ( $source )) {
$v = $topLevelVolumes -> get ( $source );
if ( data_get ( $v , 'driver_opts.type' ) === 'cifs' ) {
// Do nothing
} else {
if ( is_null ( data_get ( $v , 'name' ))) {
data_set ( $v , 'name' , $source );
data_set ( $topLevelVolumes , $source , $v );
}
}
} else {
$topLevelVolumes -> put ( $source , [
'name' => $source ,
]);
}
}
}
}
}
if ( is_array ( $volume )) {
return data_get ( $volume , 'source' );
}
dispatch ( new ServerFilesFromServerJob ( $resource ));
return $volume -> value ();
});
data_set ( $service , 'volumes' , $serviceVolumes -> toArray ());
}
}
if ( $pull_request_id !== 0 && count ( $serviceDependencies ) > 0 ) {
$serviceDependencies = $serviceDependencies -> map ( function ( $dependency ) use ( $pull_request_id ) {
2025-09-08 13:15:57 +00:00
return addPreviewDeploymentSuffix ( $dependency , $pull_request_id );
2024-08-23 18:54:38 +00:00
});
data_set ( $service , 'depends_on' , $serviceDependencies -> toArray ());
}
// Decide if the service is a database
2025-06-04 09:44:37 +00:00
$image = data_get_str ( $service , 'image' );
$isDatabase = isDatabaseImage ( $image , $service );
2024-08-23 18:54:38 +00:00
data_set ( $service , 'is_database' , $isDatabase );
// Collect/create/update networks
if ( $serviceNetworks -> count () > 0 ) {
foreach ( $serviceNetworks as $networkName => $networkDetails ) {
if ( $networkName === 'default' ) {
continue ;
}
// ignore alias
if ( $networkDetails [ 'aliases' ] ? ? false ) {
continue ;
}
$networkExists = $topLevelNetworks -> contains ( function ( $value , $key ) use ( $networkName ) {
return $value == $networkName || $key == $networkName ;
});
2024-12-09 14:38:21 +00:00
if ( ! $networkExists ) {
2024-10-03 13:04:40 +00:00
if ( is_string ( $networkDetails ) || is_int ( $networkDetails )) {
$topLevelNetworks -> put ( $networkDetails , null );
}
2024-08-23 18:54:38 +00:00
}
}
}
// Collect/create/update ports
$collectedPorts = collect ([]);
if ( $servicePorts -> count () > 0 ) {
foreach ( $servicePorts as $sport ) {
if ( is_string ( $sport ) || is_numeric ( $sport )) {
$collectedPorts -> push ( $sport );
}
if ( is_array ( $sport )) {
$target = data_get ( $sport , 'target' );
$published = data_get ( $sport , 'published' );
$protocol = data_get ( $sport , 'protocol' );
$collectedPorts -> push ( " $target : $published / $protocol " );
}
}
}
$definedNetworkExists = $topLevelNetworks -> contains ( function ( $value , $_ ) use ( $definedNetwork ) {
return $value == $definedNetwork ;
});
2024-12-09 14:38:21 +00:00
if ( ! $definedNetworkExists ) {
2024-08-23 18:54:38 +00:00
foreach ( $definedNetwork as $network ) {
if ( $pull_request_id !== 0 ) {
$topLevelNetworks -> put ( $network , [
'name' => $network ,
'external' => true ,
]);
} else {
$topLevelNetworks -> put ( $network , [
'name' => $network ,
'external' => true ,
]);
}
}
}
$networks = collect ();
foreach ( $serviceNetworks as $key => $serviceNetwork ) {
if ( gettype ( $serviceNetwork ) === 'string' ) {
// networks:
// - appwrite
$networks -> put ( $serviceNetwork , null );
} elseif ( gettype ( $serviceNetwork ) === 'array' ) {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
$networks -> put ( $key , $serviceNetwork );
}
}
foreach ( $definedNetwork as $key => $network ) {
$networks -> put ( $network , null );
}
if ( data_get ( $resource , 'settings.connect_to_docker_network' )) {
$network = $resource -> destination -> network ;
$networks -> put ( $network , null );
$topLevelNetworks -> put ( $network , [
'name' => $network ,
'external' => true ,
]);
}
data_set ( $service , 'networks' , $networks -> toArray ());
// Get variables from the service
foreach ( $serviceVariables as $variableName => $variable ) {
if ( is_numeric ( $variableName )) {
if ( is_array ( $variable )) {
// - SESSION_SECRET: 123
// - SESSION_SECRET:
$key = str ( collect ( $variable ) -> keys () -> first ());
$value = str ( collect ( $variable ) -> values () -> first ());
} else {
$variable = str ( $variable );
if ( $variable -> contains ( '=' )) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable -> before ( '=' );
$value = $variable -> after ( '=' );
} else {
// - SESSION_SECRET
$key = $variable ;
$value = null ;
}
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
$key = str ( $variableName );
$value = str ( $variable );
}
if ( $key -> startsWith ( 'SERVICE_FQDN' )) {
if ( $isNew ) {
$name = $key -> after ( 'SERVICE_FQDN_' ) -> beforeLast ( '_' ) -> lower ();
$fqdn = generateFqdn ( $server , " { $name -> value () } - { $resource -> uuid } " );
if ( substr_count ( $key -> value (), '_' ) === 3 ) {
// SERVICE_FQDN_UMAMI_1000
$port = $key -> afterLast ( '_' );
} else {
// SERVICE_FQDN_UMAMI
$port = null ;
}
if ( $port ) {
$fqdn = " $fqdn : $port " ;
}
if ( substr_count ( $key -> value (), '_' ) >= 2 ) {
if ( $value ) {
$path = $value -> value ();
} else {
$path = null ;
}
if ( $generatedServiceFQDNS -> count () > 0 ) {
$alreadyGenerated = $generatedServiceFQDNS -> has ( $key -> value ());
if ( $alreadyGenerated ) {
$fqdn = $generatedServiceFQDNS -> get ( $key -> value ());
} else {
$generatedServiceFQDNS -> put ( $key -> value (), $fqdn );
}
} else {
$generatedServiceFQDNS -> put ( $key -> value (), $fqdn );
}
$fqdn = " $fqdn $path " ;
}
}
continue ;
}
if ( $value ? -> startsWith ( '$' )) {
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
]) -> first ();
2024-09-09 13:04:51 +00:00
$value = replaceVariables ( $value );
2024-08-23 18:54:38 +00:00
$key = $value ;
if ( $value -> startsWith ( 'SERVICE_' )) {
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
]) -> first ();
[ 'command' => $command , 'forService' => $forService , 'generatedValue' => $generatedValue , 'port' => $port ] = parseEnvVariable ( $value );
2024-12-09 14:38:21 +00:00
if ( ! is_null ( $command )) {
2024-08-23 18:54:38 +00:00
if ( $command ? -> value () === 'FQDN' || $command ? -> value () === 'URL' ) {
if ( Str :: lower ( $forService ) === $serviceName ) {
$fqdn = generateFqdn ( $server , $containerName );
} else {
2024-12-09 14:38:21 +00:00
$fqdn = generateFqdn ( $server , Str :: lower ( $forService ) . '-' . $resource -> uuid );
2024-08-23 18:54:38 +00:00
}
if ( $port ) {
$fqdn = " $fqdn : $port " ;
}
if ( $foundEnv ) {
$fqdn = data_get ( $foundEnv , 'value' );
} else {
if ( $command ? -> value () === 'URL' ) {
$fqdn = str ( $fqdn ) -> after ( '://' ) -> value ();
}
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $fqdn ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
]);
}
} else {
$generatedValue = generateEnvValue ( $command );
2024-12-09 14:38:21 +00:00
if ( ! $foundEnv ) {
2024-08-23 18:54:38 +00:00
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $generatedValue ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
]);
}
}
}
} else {
if ( $value -> contains ( ':-' )) {
$key = $value -> before ( ':' );
$defaultValue = $value -> after ( ':-' );
} elseif ( $value -> contains ( '-' )) {
$key = $value -> before ( '-' );
$defaultValue = $value -> after ( '-' );
} elseif ( $value -> contains ( ':?' )) {
$key = $value -> before ( ':' );
$defaultValue = $value -> after ( ':?' );
} elseif ( $value -> contains ( '?' )) {
$key = $value -> before ( '?' );
$defaultValue = $value -> after ( '?' );
} else {
$key = $value ;
$defaultValue = null ;
}
$foundEnv = EnvironmentVariable :: where ([
'key' => $key ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
]) -> first ();
if ( $foundEnv ) {
$defaultValue = data_get ( $foundEnv , 'value' );
}
if ( $foundEnv ) {
$foundEnv -> update ([
'key' => $key ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'value' => $defaultValue ,
]);
} else {
EnvironmentVariable :: create ([
'key' => $key ,
'value' => $defaultValue ,
2025-01-22 09:21:41 +00:00
'resourceable_type' => get_class ( $resource ),
'resourceable_id' => $resource -> id ,
2024-08-23 18:54:38 +00:00
'is_preview' => false ,
]);
}
}
}
}
// Add labels to the service
if ( $resource -> serviceType ()) {
$fqdns = generateServiceSpecificFqdns ( $resource );
} else {
$domains = collect ( json_decode ( $resource -> docker_compose_domains )) ? ? [];
if ( $domains ) {
$fqdns = data_get ( $domains , " $serviceName .domain " );
if ( $fqdns ) {
$fqdns = str ( $fqdns ) -> explode ( ',' );
if ( $pull_request_id !== 0 ) {
$preview = $resource -> previews () -> find ( $preview_id );
$docker_compose_domains = collect ( json_decode ( data_get ( $preview , 'docker_compose_domains' )));
if ( $docker_compose_domains -> count () > 0 ) {
$found_fqdn = data_get ( $docker_compose_domains , " $serviceName .domain " );
if ( $found_fqdn ) {
$fqdns = collect ( $found_fqdn );
} else {
$fqdns = collect ([]);
}
} else {
$fqdns = $fqdns -> map ( function ( $fqdn ) use ( $pull_request_id , $resource ) {
$preview = ApplicationPreview :: findPreviewByApplicationAndPullId ( $resource -> id , $pull_request_id );
$url = Url :: fromString ( $fqdn );
$template = $resource -> preview_url_template ;
$host = $url -> getHost ();
$schema = $url -> getScheme ();
$random = new Cuid2 ;
$preview_fqdn = str_replace ( '{{random}}' , $random , $template );
$preview_fqdn = str_replace ( '{{domain}}' , $host , $preview_fqdn );
$preview_fqdn = str_replace ( '{{pr_id}}' , $pull_request_id , $preview_fqdn );
$preview_fqdn = " $schema :// $preview_fqdn " ;
$preview -> fqdn = $preview_fqdn ;
$preview -> save ();
return $preview_fqdn ;
});
}
}
$shouldGenerateLabelsExactly = $server -> settings -> generate_exact_labels ;
if ( $shouldGenerateLabelsExactly ) {
switch ( $server -> proxyType ()) {
case ProxyTypes :: TRAEFIK -> value :
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge (
fqdnLabelsForTraefik (
uuid : $resource -> uuid ,
domains : $fqdns ,
serviceLabels : $serviceLabels ,
generate_unique_uuid : $resource -> build_pack === 'dockercompose' ,
image : data_get ( $service , 'image' ),
is_force_https_enabled : $resource -> isForceHttpsEnabled (),
is_gzip_enabled : $resource -> isGzipEnabled (),
is_stripprefix_enabled : $resource -> isStripprefixEnabled (),
)
2024-08-23 18:54:38 +00:00
);
break ;
case ProxyTypes :: CADDY -> value :
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge (
fqdnLabelsForCaddy (
network : $resource -> destination -> network ,
uuid : $resource -> uuid ,
domains : $fqdns ,
serviceLabels : $serviceLabels ,
image : data_get ( $service , 'image' ),
is_force_https_enabled : $resource -> isForceHttpsEnabled (),
is_gzip_enabled : $resource -> isGzipEnabled (),
is_stripprefix_enabled : $resource -> isStripprefixEnabled (),
)
2024-08-23 18:54:38 +00:00
);
break ;
}
} else {
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge (
fqdnLabelsForTraefik (
uuid : $resource -> uuid ,
domains : $fqdns ,
serviceLabels : $serviceLabels ,
generate_unique_uuid : $resource -> build_pack === 'dockercompose' ,
image : data_get ( $service , 'image' ),
is_force_https_enabled : $resource -> isForceHttpsEnabled (),
is_gzip_enabled : $resource -> isGzipEnabled (),
is_stripprefix_enabled : $resource -> isStripprefixEnabled (),
)
2024-08-23 18:54:38 +00:00
);
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge (
2024-08-23 18:54:38 +00:00
fqdnLabelsForCaddy (
network : $resource -> destination -> network ,
uuid : $resource -> uuid ,
domains : $fqdns ,
serviceLabels : $serviceLabels ,
image : data_get ( $service , 'image' ),
is_force_https_enabled : $resource -> isForceHttpsEnabled (),
is_gzip_enabled : $resource -> isGzipEnabled (),
is_stripprefix_enabled : $resource -> isStripprefixEnabled (),
2025-02-28 19:25:19 +00:00
)
);
2024-08-23 18:54:38 +00:00
}
}
}
}
2024-11-17 21:49:44 +00:00
$defaultLabels = defaultLabels (
id : $resource -> id ,
name : $containerName ,
projectName : $resource -> project () -> name ,
resourceName : $resource -> name ,
2024-12-02 17:38:21 +00:00
environment : $resource -> environment -> name ,
2024-11-17 21:49:44 +00:00
pull_request_id : $pull_request_id ,
type : 'application'
);
2025-02-28 19:25:19 +00:00
$serviceLabels = $serviceLabels -> merge ( $defaultLabels );
2024-08-23 18:54:38 +00:00
2024-09-02 08:57:03 +00:00
if ( $server -> isLogDrainEnabled ()) {
if ( $resource instanceof Application && $resource -> isLogDrainEnabled ()) {
data_set ( $service , 'logging' , generate_fluentd_configuration ());
}
2024-08-23 18:54:38 +00:00
}
if ( $serviceLabels -> count () > 0 ) {
if ( $resource -> settings -> is_container_label_escape_enabled ) {
$serviceLabels = $serviceLabels -> map ( function ( $value , $key ) {
return escapeDollarSign ( $value );
});
}
}
data_set ( $service , 'labels' , $serviceLabels -> toArray ());
data_forget ( $service , 'is_database' );
2024-12-09 14:38:21 +00:00
if ( ! data_get ( $service , 'restart' )) {
2024-08-23 18:54:38 +00:00
data_set ( $service , 'restart' , RESTART_MODE );
}
data_set ( $service , 'container_name' , $containerName );
data_forget ( $service , 'volumes.*.content' );
data_forget ( $service , 'volumes.*.isDirectory' );
2024-12-17 09:38:32 +00:00
data_forget ( $service , 'volumes.*.is_directory' );
data_forget ( $service , 'exclude_from_hc' );
data_set ( $service , 'environment' , $serviceVariables -> toArray ());
2024-08-23 18:54:38 +00:00
return $service ;
});
if ( $pull_request_id !== 0 ) {
$services -> each ( function ( $service , $serviceName ) use ( $pull_request_id , $services ) {
2025-09-08 13:15:57 +00:00
$services [ addPreviewDeploymentSuffix ( $serviceName , $pull_request_id )] = $service ;
2024-08-23 18:54:38 +00:00
data_forget ( $services , $serviceName );
});
}
$finalServices = [
'services' => $services -> toArray (),
'volumes' => $topLevelVolumes -> toArray (),
'networks' => $topLevelNetworks -> toArray (),
'configs' => $topLevelConfigs -> toArray (),
'secrets' => $topLevelSecrets -> toArray (),
];
2024-08-26 08:51:01 +00:00
$resource -> docker_compose_raw = Yaml :: dump ( $yaml , 10 , 2 );
$resource -> docker_compose = Yaml :: dump ( $finalServices , 10 , 2 );
data_forget ( $resource , 'environment_variables' );
data_forget ( $resource , 'environment_variables_preview' );
2024-08-23 18:54:38 +00:00
$resource -> save ();
return collect ( $finalServices );
}
}
2024-09-07 18:56:26 +00:00
2024-08-29 13:11:54 +00:00
function generate_fluentd_configuration () : array
{
2024-08-28 09:11:14 +00:00
return [
'driver' => 'fluentd' ,
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224' ,
'fluentd-async' => 'true' ,
'fluentd-sub-second-precision' => 'true' ,
// env vars are used in the LogDrain configurations
'env' => 'COOLIFY_APP_NAME,COOLIFY_PROJECT_NAME,COOLIFY_SERVER_IP,COOLIFY_ENVIRONMENT_NAME' ,
2024-08-29 13:11:54 +00:00
],
2024-08-28 09:11:14 +00:00
];
}
2024-08-28 11:30:59 +00:00
2024-09-03 15:04:56 +00:00
function isAssociativeArray ( $array )
{
if ( $array instanceof Collection ) {
$array = $array -> toArray ();
}
2024-12-09 14:38:21 +00:00
if ( ! is_array ( $array )) {
2024-09-03 15:04:56 +00:00
throw new \InvalidArgumentException ( 'Input must be an array or a Collection.' );
}
if ( $array === []) {
return false ;
}
return array_keys ( $array ) !== range ( 0 , count ( $array ) - 1 );
}
2024-08-28 11:30:59 +00:00
/**
2024-08-29 13:11:54 +00:00
* This method adds the default environment variables to the resource .
* - COOLIFY_APP_NAME
* - COOLIFY_PROJECT_NAME
* - COOLIFY_SERVER_IP
* - COOLIFY_ENVIRONMENT_NAME
*
* Theses variables are added in place to the $where_to_add array .
*/
function add_coolify_default_environment_variables ( StandaloneRedis | StandalonePostgresql | StandaloneMongodb | StandaloneMysql | StandaloneMariadb | StandaloneKeydb | StandaloneDragonfly | StandaloneClickhouse | Application | Service $resource , Collection & $where_to_add , ? Collection $where_to_check = null )
{
2024-10-11 09:04:44 +00:00
// Currently disabled
return ;
2024-09-03 15:04:56 +00:00
if ( $resource instanceof Service ) {
$ip = $resource -> server -> ip ;
} else {
$ip = $resource -> destination -> server -> ip ;
}
if ( isAssociativeArray ( $where_to_add )) {
$isAssociativeArray = true ;
} else {
$isAssociativeArray = false ;
}
2024-08-28 11:30:59 +00:00
if ( $where_to_check != null && $where_to_check -> where ( 'key' , 'COOLIFY_APP_NAME' ) -> isEmpty ()) {
2024-09-03 15:04:56 +00:00
if ( $isAssociativeArray ) {
2024-09-13 06:23:05 +00:00
$where_to_add -> put ( 'COOLIFY_APP_NAME' , " \" { $resource -> name } \" " );
2024-08-29 13:11:54 +00:00
} else {
2024-09-13 06:23:05 +00:00
$where_to_add -> push ( " COOLIFY_APP_NAME= \" { $resource -> name } \" " );
2024-08-29 13:11:54 +00:00
}
2024-08-28 11:30:59 +00:00
}
if ( $where_to_check != null && $where_to_check -> where ( 'key' , 'COOLIFY_SERVER_IP' ) -> isEmpty ()) {
2024-09-03 15:04:56 +00:00
if ( $isAssociativeArray ) {
2024-09-13 06:23:05 +00:00
$where_to_add -> put ( 'COOLIFY_SERVER_IP' , " \" { $ip } \" " );
2024-08-29 13:11:54 +00:00
} else {
2024-09-13 06:23:05 +00:00
$where_to_add -> push ( " COOLIFY_SERVER_IP= \" { $ip } \" " );
2024-08-29 13:11:54 +00:00
}
2024-08-28 11:30:59 +00:00
}
if ( $where_to_check != null && $where_to_check -> where ( 'key' , 'COOLIFY_ENVIRONMENT_NAME' ) -> isEmpty ()) {
2024-09-03 15:04:56 +00:00
if ( $isAssociativeArray ) {
2024-09-13 06:23:05 +00:00
$where_to_add -> put ( 'COOLIFY_ENVIRONMENT_NAME' , " \" { $resource -> environment -> name } \" " );
2024-08-29 13:11:54 +00:00
} else {
2024-09-13 06:23:05 +00:00
$where_to_add -> push ( " COOLIFY_ENVIRONMENT_NAME= \" { $resource -> environment -> name } \" " );
2024-08-29 13:11:54 +00:00
}
2024-08-28 11:30:59 +00:00
}
if ( $where_to_check != null && $where_to_check -> where ( 'key' , 'COOLIFY_PROJECT_NAME' ) -> isEmpty ()) {
2024-09-03 15:04:56 +00:00
if ( $isAssociativeArray ) {
2024-09-13 06:23:05 +00:00
$where_to_add -> put ( 'COOLIFY_PROJECT_NAME' , " \" { $resource -> project () -> name } \" " );
2024-08-29 13:11:54 +00:00
} else {
2024-09-13 06:23:05 +00:00
$where_to_add -> push ( " COOLIFY_PROJECT_NAME= \" { $resource -> project () -> name } \" " );
2024-08-29 13:11:54 +00:00
}
2024-08-28 11:30:59 +00:00
}
2024-08-29 13:11:54 +00:00
}
2024-08-29 12:35:04 +00:00
2025-02-27 10:29:04 +00:00
function convertToKeyValueCollection ( $environment )
2024-08-28 13:45:11 +00:00
{
$convertedServiceVariables = collect ([]);
2024-09-04 11:37:15 +00:00
if ( isAssociativeArray ( $environment )) {
2024-10-02 16:26:40 +00:00
// Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux'];
2024-09-30 09:15:23 +00:00
if ( $environment instanceof Collection ) {
$changedEnvironment = collect ([]);
$environment -> each ( function ( $value , $key ) use ( $changedEnvironment ) {
2024-10-02 16:26:40 +00:00
if ( is_numeric ( $key )) {
$parts = explode ( '=' , $value , 2 );
if ( count ( $parts ) === 2 ) {
$key = $parts [ 0 ];
$realValue = $parts [ 1 ] ? ? '' ;
$changedEnvironment -> put ( $key , $realValue );
} else {
$changedEnvironment -> put ( $key , $value );
}
2024-09-30 09:15:23 +00:00
} else {
$changedEnvironment -> put ( $key , $value );
}
});
return $changedEnvironment ;
}
2024-09-04 11:37:15 +00:00
$convertedServiceVariables = $environment ;
} else {
2024-10-02 16:26:40 +00:00
// Example: $environment = ['FOO=bar', 'BAZ=qux'];
2024-09-04 11:37:15 +00:00
foreach ( $environment as $value ) {
2024-10-03 13:04:40 +00:00
if ( is_string ( $value )) {
$parts = explode ( '=' , $value , 2 );
$key = $parts [ 0 ];
$realValue = $parts [ 1 ] ? ? '' ;
if ( $key ) {
$convertedServiceVariables -> put ( $key , $realValue );
}
2024-08-28 13:45:11 +00:00
}
}
}
return $convertedServiceVariables ;
2024-08-28 11:30:59 +00:00
}
2024-10-01 08:33:56 +00:00
function instanceSettings ()
{
return InstanceSettings :: get ();
}
2024-10-08 13:11:19 +00:00
2025-12-17 11:09:13 +00:00
function wireNavigate () : string
{
try {
$settings = instanceSettings ();
// Return wire:navigate.hover for SPA navigation with prefetching, or empty string if disabled
return ( $settings -> is_wire_navigate_enabled ? ? true ) ? 'wire:navigate.hover' : '' ;
} catch ( \Exception $e ) {
return 'wire:navigate.hover' ;
}
}
2025-12-26 12:22:18 +00:00
/**
* Redirect to a named route with SPA navigation support .
* Automatically uses wire : navigate when is_wire_navigate_enabled is true .
*/
function redirectRoute ( Livewire\Component $component , string $name , array $parameters = []) : mixed
{
$navigate = true ;
try {
$navigate = instanceSettings () -> is_wire_navigate_enabled ? ? true ;
} catch ( \Exception $e ) {
$navigate = true ;
}
return $component -> redirectRoute ( $name , $parameters , navigate : $navigate );
}
2025-11-03 07:38:43 +00:00
function getHelperVersion () : string
{
$settings = instanceSettings ();
// In development mode, use the dev_helper_version if set, otherwise fallback to config
if ( isDev () && ! empty ( $settings -> dev_helper_version )) {
return $settings -> dev_helper_version ;
}
2025-11-03 07:57:52 +00:00
return config ( 'constants.coolify.helper_version' );
2025-11-03 07:38:43 +00:00
}
2024-10-17 19:48:47 +00:00
function loadConfigFromGit ( string $repository , string $branch , string $base_directory , int $server_id , int $team_id )
{
2024-10-08 13:11:19 +00:00
$server = Server :: find ( $server_id ) -> where ( 'team_id' , $team_id ) -> first ();
2024-12-09 14:38:21 +00:00
if ( ! $server ) {
2024-10-08 13:11:19 +00:00
return ;
}
2024-10-17 19:48:47 +00:00
$uuid = new Cuid2 ;
2024-10-08 13:11:19 +00:00
$cloneCommand = " git clone --no-checkout -b $branch $repository . " ;
$workdir = rtrim ( $base_directory , '/' );
$fileList = collect ([ " . $workdir /coolify.json " ]);
$commands = collect ([
" rm -rf /tmp/ { $uuid } " ,
" mkdir -p /tmp/ { $uuid } " ,
" cd /tmp/ { $uuid } " ,
$cloneCommand ,
'git sparse-checkout init --cone' ,
" git sparse-checkout set { $fileList -> implode ( ' ' ) } " ,
'git read-tree -mu HEAD' ,
" cat . $workdir /coolify.json " ,
'rm -rf /tmp/{$uuid}' ,
]);
try {
return instant_remote_process ( $commands , $server );
2024-10-31 14:19:37 +00:00
} catch ( \Exception ) {
2024-10-17 19:48:47 +00:00
// continue
2024-10-08 13:11:19 +00:00
}
}
2024-10-17 08:04:49 +00:00
2024-10-25 15:49:16 +00:00
function loggy ( $message = null , array $context = [])
2024-10-17 08:04:49 +00:00
{
2024-12-09 14:38:21 +00:00
if ( ! isDev ()) {
2024-10-17 08:04:49 +00:00
return ;
}
if ( function_exists ( 'ray' ) && config ( 'app.debug' )) {
ray ( $message , $context );
}
if ( is_null ( $message )) {
return app ( 'log' );
}
return app ( 'log' ) -> debug ( $message , $context );
}
2024-10-20 20:15:31 +00:00
function sslipDomainWarning ( string $domains )
{
$domains = str ( $domains ) -> trim () -> explode ( ',' );
$showSslipHttpsWarning = false ;
$domains -> each ( function ( $domain ) use ( & $showSslipHttpsWarning ) {
if ( str ( $domain ) -> contains ( 'https' ) && str ( $domain ) -> contains ( 'sslip' )) {
$showSslipHttpsWarning = true ;
}
});
return $showSslipHttpsWarning ;
}
2024-10-25 13:13:23 +00:00
function isEmailRateLimited ( string $limiterKey , int $decaySeconds = 3600 , ? callable $callbackOnSuccess = null ) : bool
{
if ( isDev ()) {
$decaySeconds = 120 ;
}
$rateLimited = false ;
$executed = RateLimiter :: attempt (
$limiterKey ,
$maxAttempts = 0 ,
function () use ( & $rateLimited , & $limiterKey , $callbackOnSuccess ) {
2024-12-09 14:38:21 +00:00
isDev () && loggy ( 'Rate limit not reached for ' . $limiterKey );
2024-10-25 13:13:23 +00:00
$rateLimited = false ;
if ( $callbackOnSuccess ) {
$callbackOnSuccess ();
}
},
$decaySeconds ,
);
2024-12-09 14:38:21 +00:00
if ( ! $executed ) {
isDev () && loggy ( 'Rate limit reached for ' . $limiterKey . '. Rate limiter will be disabled for ' . $decaySeconds . ' seconds.' );
2024-10-25 13:13:23 +00:00
$rateLimited = true ;
}
return $rateLimited ;
}
2024-11-11 13:37:19 +00:00
2025-03-31 13:10:50 +00:00
function defaultNginxConfiguration ( string $type = 'static' ) : string
2024-11-11 13:37:19 +00:00
{
2025-03-31 13:10:50 +00:00
if ( $type === 'spa' ) {
return <<< 'NGINX'
server {
location / {
root / usr / share / nginx / html ;
index index . html ;
try_files $uri $uri / / index . html ;
}
# Handle 404 errors
error_page 404 / 404. html ;
location = / 404. html {
root / usr / share / nginx / html ;
internal ;
}
# Handle server errors (50x)
error_page 500 502 503 504 / 50 x . html ;
location = / 50 x . html {
root / usr / share / nginx / html ;
internal ;
}
}
NGINX ;
} else {
return <<< 'NGINX'
server {
2024-11-11 13:37:19 +00:00
location / {
2025-03-13 19:30:22 +00:00
root / usr / share / nginx / html ;
index index . html index . htm ;
try_files $uri $uri . html $uri / index . html $uri / index . htm $uri / = 404 ;
2024-11-11 13:37:19 +00:00
}
2025-03-13 19:30:22 +00:00
# Handle 404 errors
error_page 404 / 404. html ;
location = / 404. html {
2024-11-11 13:45:34 +00:00
root / usr / share / nginx / html ;
internal ;
2024-11-11 13:37:19 +00:00
}
2025-03-13 19:30:22 +00:00
# Handle server errors (50x)
error_page 500 502 503 504 / 50 x . html ;
location = / 50 x . html {
2024-11-11 13:37:19 +00:00
root / usr / share / nginx / html ;
internal ;
}
2025-03-31 13:10:50 +00:00
}
NGINX ;
}
2024-11-11 13:37:19 +00:00
}
2024-11-12 10:32:18 +00:00
2026-03-11 14:30:46 +00:00
function convertGitUrl ( string $gitRepository , string $deploymentType , GithubApp | GitlabApp | null $source = null ) : array
2024-11-12 10:32:18 +00:00
{
$repository = $gitRepository ;
$providerInfo = [
'host' => null ,
'user' => 'git' ,
'port' => 22 ,
'repository' => $gitRepository ,
];
$sshMatches = [];
$matches = [];
// Let's try and parse the string to detect if it's a valid SSH string or not
preg_match ( '/((.*?)\:\/\/)?(.*@.*:.*)/' , $gitRepository , $sshMatches );
if ( $deploymentType === 'deploy_key' && empty ( $sshMatches ) && $source ) {
// If this happens, the user may have provided an HTTP URL when they needed an SSH one
// Let's try and fix that for known Git providers
switch ( $source -> getMorphClass ()) {
case \App\Models\GithubApp :: class :
2026-03-11 14:30:46 +00:00
case \App\Models\GitlabApp :: class :
2024-11-12 10:32:18 +00:00
$providerInfo [ 'host' ] = Url :: fromString ( $source -> html_url ) -> getHost ();
$providerInfo [ 'port' ] = $source -> custom_port ;
$providerInfo [ 'user' ] = $source -> custom_user ;
break ;
}
if ( ! empty ( $providerInfo [ 'host' ])) {
// Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22
if ( $providerInfo [ 'port' ] === 22 ) {
$repository = " { $providerInfo [ 'user' ] } @ { $providerInfo [ 'host' ] } : { $providerInfo [ 'repository' ] } " ;
} else {
$repository = " ssh:// { $providerInfo [ 'user' ] } @ { $providerInfo [ 'host' ] } : { $providerInfo [ 'port' ] } / { $providerInfo [ 'repository' ] } " ;
}
}
}
preg_match ( '/(?<=:)\d+(?=\/)/' , $gitRepository , $matches );
if ( count ( $matches ) === 1 ) {
$providerInfo [ 'port' ] = $matches [ 0 ];
$gitHost = str ( $gitRepository ) -> before ( ':' );
$gitRepo = str ( $gitRepository ) -> after ( '/' );
$repository = " $gitHost : $gitRepo " ;
}
return [
'repository' => $repository ,
'port' => $providerInfo [ 'port' ],
];
}
2025-01-10 17:27:48 +00:00
2025-01-10 18:53:13 +00:00
function getJobStatus ( ? string $jobId = null )
2025-01-10 17:27:48 +00:00
{
2025-01-10 18:53:13 +00:00
if ( blank ( $jobId )) {
return 'unknown' ;
}
2025-01-10 17:27:48 +00:00
$jobFound = app ( JobRepository :: class ) -> getJobs ([ $jobId ]);
if ( $jobFound -> isEmpty ()) {
return 'unknown' ;
}
return $jobFound -> first () -> status ;
}
2025-03-24 10:43:10 +00:00
function parseDockerfileInterval ( string $something )
{
$value = preg_replace ( '/[^0-9]/' , '' , $something );
$unit = preg_replace ( '/[0-9]/' , '' , $something );
// Default to seconds if no unit specified
$unit = $unit ? : 's' ;
// Convert to seconds based on unit
$seconds = ( int ) $value ;
switch ( $unit ) {
case 'ns' :
$seconds = ( int ) ( $value / 1000000000 );
break ;
case 'us' :
case 'µs' :
$seconds = ( int ) ( $value / 1000000 );
break ;
case 'ms' :
$seconds = ( int ) ( $value / 1000 );
break ;
case 'm' :
$seconds = ( int ) ( $value * 60 );
break ;
case 'h' :
$seconds = ( int ) ( $value * 3600 );
break ;
}
return $seconds ;
}
2025-09-08 13:15:57 +00:00
function addPreviewDeploymentSuffix ( string $name , int $pull_request_id = 0 ) : string
{
2025-09-11 11:59:02 +00:00
return ( $pull_request_id === 0 ) ? $name : $name . '-pr-' . $pull_request_id ;
2025-09-08 13:15:57 +00:00
}
2025-09-11 11:59:02 +00:00
function generateDockerComposeServiceName ( mixed $services , int $pullRequestId = 0 ) : Collection
2025-09-08 13:15:57 +00:00
{
$collection = collect ([]);
foreach ( $services as $serviceName => $_ ) {
2025-09-11 11:59:02 +00:00
$collection -> put ( 'SERVICE_NAME_' . str ( $serviceName ) -> replace ( '-' , '_' ) -> replace ( '.' , '_' ) -> upper (), addPreviewDeploymentSuffix ( $serviceName , $pullRequestId ));
2025-09-08 13:15:57 +00:00
}
2025-09-11 11:59:02 +00:00
2025-09-08 13:15:57 +00:00
return $collection ;
}
2025-11-02 14:19:13 +00:00
2025-11-17 09:05:18 +00:00
function formatBytes ( ? int $bytes , int $precision = 2 ) : string
2025-11-02 14:19:13 +00:00
{
2025-11-17 09:05:18 +00:00
if ( $bytes === null || $bytes === 0 ) {
return '0 B' ;
}
// Handle negative numbers
if ( $bytes < 0 ) {
2025-11-02 14:19:13 +00:00
return '0 B' ;
}
$units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' , 'PB' ];
2025-11-02 17:06:04 +00:00
$base = 1024 ;
$exponent = floor ( log ( $bytes ) / log ( $base ));
$exponent = min ( $exponent , count ( $units ) - 1 );
2025-11-02 14:19:13 +00:00
2025-11-02 17:06:04 +00:00
$value = $bytes / pow ( $base , $exponent );
2025-11-02 14:19:13 +00:00
2025-11-02 17:06:04 +00:00
return round ( $value , $precision ) . ' ' . $units [ $exponent ];
2025-11-02 14:19:13 +00:00
}
2025-11-17 09:05:18 +00:00
/**
* Validates that a file path is safely within the / tmp / directory .
* Protects against path traversal attacks by resolving the real path
* and verifying it stays within / tmp /.
*
* Note : On macOS , / tmp is often a symlink to / private / tmp , which is handled .
*/
function isSafeTmpPath ( ? string $path ) : bool
{
if ( blank ( $path )) {
return false ;
}
// URL decode to catch encoded traversal attempts
$decodedPath = urldecode ( $path );
// Minimum length check - /tmp/x is 6 chars
if ( strlen ( $decodedPath ) < 6 ) {
return false ;
}
// Must start with /tmp/
if ( ! str ( $decodedPath ) -> startsWith ( '/tmp/' )) {
return false ;
}
// Quick check for obvious traversal attempts
if ( str ( $decodedPath ) -> contains ( '..' )) {
return false ;
}
// Check for null bytes (directory traversal technique)
if ( str ( $decodedPath ) -> contains ( " \0 " )) {
return false ;
}
// Remove any trailing slashes for consistent validation
$normalizedPath = rtrim ( $decodedPath , '/' );
// Normalize the path by removing redundant separators and resolving . and ..
// We'll do this manually since realpath() requires the path to exist
$parts = explode ( '/' , $normalizedPath );
$resolvedParts = [];
foreach ( $parts as $part ) {
if ( $part === '' || $part === '.' ) {
// Skip empty parts (from //) and current directory references
continue ;
} elseif ( $part === '..' ) {
// Parent directory - this should have been caught earlier but double-check
return false ;
} else {
$resolvedParts [] = $part ;
}
}
$resolvedPath = '/' . implode ( '/' , $resolvedParts );
// Final check: resolved path must start with /tmp/
// And must have at least one component after /tmp/
if ( ! str ( $resolvedPath ) -> startsWith ( '/tmp/' ) || $resolvedPath === '/tmp' ) {
return false ;
}
// Resolve the canonical /tmp path (handles symlinks like /tmp -> /private/tmp on macOS)
$canonicalTmpPath = realpath ( '/tmp' );
if ( $canonicalTmpPath === false ) {
// If /tmp doesn't exist, something is very wrong, but allow non-existing paths
$canonicalTmpPath = '/tmp' ;
}
2025-11-17 13:13:10 +00:00
// Calculate dirname once to avoid redundant calls
$dirPath = dirname ( $resolvedPath );
2025-11-17 09:05:18 +00:00
// If the directory exists, resolve it via realpath to catch symlink attacks
2025-11-25 09:18:30 +00:00
if ( is_dir ( $dirPath )) {
2025-11-17 09:05:18 +00:00
// For existing paths, resolve to absolute path to catch symlinks
2025-11-25 09:18:30 +00:00
$realDir = realpath ( $dirPath );
if ( $realDir === false ) {
return false ;
}
2025-11-17 09:05:18 +00:00
2025-11-25 09:18:30 +00:00
// Check if the real directory is within /tmp (or its canonical path)
if ( ! str ( $realDir ) -> startsWith ( '/tmp' ) && ! str ( $realDir ) -> startsWith ( $canonicalTmpPath )) {
return false ;
2025-11-17 09:05:18 +00:00
}
}
return true ;
}
2025-11-25 08:35:37 +00:00
2025-11-21 08:12:56 +00:00
/**
* Transform colon - delimited status format to human - readable parentheses format .
*
* Handles Docker container status formats with optional health check status and exclusion modifiers .
*
* Examples :
* - running : healthy → Running ( healthy )
* - running : unhealthy : excluded → Running ( unhealthy , excluded )
* - exited : excluded → Exited ( excluded )
* - Proxy : running → Proxy : running ( preserved as - is for headline formatting )
* - running → Running
*
* @ param string $status The status string to format
* @ return string The formatted status string
*/
function formatContainerStatus ( string $status ) : string
{
// Preserve Proxy statuses as-is (they follow different format)
if ( str ( $status ) -> startsWith ( 'Proxy' )) {
return str ( $status ) -> headline () -> value ();
}
// Check for :excluded suffix
$isExcluded = str ( $status ) -> endsWith ( ':excluded' );
$parts = explode ( ':' , $status );
if ( $isExcluded ) {
if ( count ( $parts ) === 3 ) {
// Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
return str ( $parts [ 0 ]) -> headline () . ' (' . $parts [ 1 ] . ', excluded)' ;
} else {
// No health status: exited:excluded → Exited (excluded)
return str ( $parts [ 0 ]) -> headline () . ' (excluded)' ;
}
} elseif ( count ( $parts ) >= 2 ) {
// Regular colon format: running:healthy → Running (healthy)
return str ( $parts [ 0 ]) -> headline () . ' (' . $parts [ 1 ] . ')' ;
} else {
// Simple status: running → Running
return str ( $status ) -> headline () -> value ();
}
}
2025-12-12 13:12:02 +00:00
/**
* Check if password confirmation should be skipped .
* Returns true if :
* - Two - step confirmation is globally disabled
* - User has no password ( OAuth users )
*
* Used by modal - confirmation . blade . php to determine if password step should be shown .
*
* @ return bool True if password confirmation should be skipped
*/
function shouldSkipPasswordConfirmation () : bool
{
// Skip if two-step confirmation is globally disabled
if ( data_get ( InstanceSettings :: get (), 'disable_two_step_confirmation' )) {
return true ;
}
// Skip if user has no password (OAuth users)
if ( ! Auth :: user () ? -> hasPassword ()) {
return true ;
}
return false ;
}
/**
* Verify password for two - step confirmation .
* Skips verification if :
* - Two - step confirmation is globally disabled
* - User has no password ( OAuth users )
*
* @ param mixed $password The password to verify ( may be array if skipped by frontend )
* @ param \Livewire\Component | null $component Optional Livewire component to add errors to
* @ return bool True if verification passed ( or skipped ), false if password is incorrect
*/
function verifyPasswordConfirmation ( mixed $password , ? Livewire\Component $component = null ) : bool
{
// Skip if password confirmation should be skipped
if ( shouldSkipPasswordConfirmation ()) {
return true ;
}
// Verify the password
if ( ! Hash :: check ( $password , Auth :: user () -> password )) {
if ( $component ) {
$component -> addError ( 'password' , 'The provided password is incorrect.' );
}
return false ;
}
return true ;
}
2025-11-25 14:22:38 +00:00
/**
* Extract hard - coded environment variables from docker - compose YAML .
*
* @ param string $dockerComposeRaw Raw YAML content
* @ return \Illuminate\Support\Collection Collection of arrays with : key , value , comment , service_name
*/
function extractHardcodedEnvironmentVariables ( string $dockerComposeRaw ) : \Illuminate\Support\Collection
{
if ( blank ( $dockerComposeRaw )) {
return collect ([]);
}
try {
$yaml = \Symfony\Component\Yaml\Yaml :: parse ( $dockerComposeRaw );
} catch ( \Exception $e ) {
// Malformed YAML - return empty collection
return collect ([]);
}
$services = data_get ( $yaml , 'services' , []);
if ( empty ( $services )) {
return collect ([]);
}
// Extract inline comments from raw YAML
$envComments = extractYamlEnvironmentComments ( $dockerComposeRaw );
$hardcodedVars = collect ([]);
foreach ( $services as $serviceName => $service ) {
$environment = collect ( data_get ( $service , 'environment' , []));
if ( $environment -> isEmpty ()) {
continue ;
}
// Convert environment variables to key-value format
$environment = convertToKeyValueCollection ( $environment );
foreach ( $environment as $key => $value ) {
$hardcodedVars -> push ([
'key' => $key ,
'value' => $value ,
'comment' => $envComments [ $key ] ? ? null ,
'service_name' => $serviceName ,
]);
}
}
return $hardcodedVars ;
}
2026-02-27 23:09:54 +00:00
2026-01-02 11:36:17 +00:00
/**
* Downsample metrics using the Largest - Triangle - Three - Buckets ( LTTB ) algorithm .
* This preserves the visual shape of the data better than simple averaging .
*
* @ param array $data Array of [ timestamp , value ] pairs
* @ param int $threshold Target number of points
* @ return array Downsampled data
*/
function downsampleLTTB ( array $data , int $threshold ) : array
{
$dataLength = count ( $data );
// Return unchanged if threshold >= data length, or if threshold <= 2
// (threshold <= 2 would cause division by zero in bucket calculation)
if ( $threshold >= $dataLength || $threshold <= 2 ) {
return $data ;
}
$sampled = [];
$sampled [] = $data [ 0 ]; // Always keep first point
$bucketSize = ( $dataLength - 2 ) / ( $threshold - 2 );
$a = 0 ; // Index of previous selected point
for ( $i = 0 ; $i < $threshold - 2 ; $i ++ ) {
// Calculate bucket range
$bucketStart = ( int ) floor (( $i + 1 ) * $bucketSize ) + 1 ;
$bucketEnd = ( int ) floor (( $i + 2 ) * $bucketSize ) + 1 ;
$bucketEnd = min ( $bucketEnd , $dataLength - 1 );
// Calculate average point for next bucket (used as reference)
$nextBucketStart = ( int ) floor (( $i + 2 ) * $bucketSize ) + 1 ;
$nextBucketEnd = ( int ) floor (( $i + 3 ) * $bucketSize ) + 1 ;
$nextBucketEnd = min ( $nextBucketEnd , $dataLength - 1 );
$avgX = 0 ;
$avgY = 0 ;
$nextBucketCount = $nextBucketEnd - $nextBucketStart + 1 ;
if ( $nextBucketCount > 0 ) {
for ( $j = $nextBucketStart ; $j <= $nextBucketEnd ; $j ++ ) {
$avgX += $data [ $j ][ 0 ];
$avgY += $data [ $j ][ 1 ];
}
$avgX /= $nextBucketCount ;
$avgY /= $nextBucketCount ;
}
// Find point in current bucket with largest triangle area
$maxArea = - 1 ;
$maxAreaIndex = $bucketStart ;
$pointAX = $data [ $a ][ 0 ];
$pointAY = $data [ $a ][ 1 ];
for ( $j = $bucketStart ; $j <= $bucketEnd ; $j ++ ) {
// Triangle area calculation
$area = abs (
( $pointAX - $avgX ) * ( $data [ $j ][ 1 ] - $pointAY ) -
( $pointAX - $data [ $j ][ 0 ]) * ( $avgY - $pointAY )
) * 0.5 ;
if ( $area > $maxArea ) {
$maxArea = $area ;
$maxAreaIndex = $j ;
}
}
$sampled [] = $data [ $maxAreaIndex ];
$a = $maxAreaIndex ;
}
$sampled [] = $data [ $dataLength - 1 ]; // Always keep last point
return $sampled ;
}
2026-03-12 12:23:13 +00:00
/**
* Resolve shared environment variable patterns like {{ environment . VAR }}, {{ project . VAR }}, {{ team . VAR }} .
*
* This is the canonical implementation used by both EnvironmentVariable :: realValue and the compose parsers
* to ensure shared variable references are replaced with their actual values .
*/
function resolveSharedEnvironmentVariables ( ? string $value , $resource ) : ? string
{
if ( is_null ( $value ) || $value === '' || is_null ( $resource )) {
return $value ;
}
$value = trim ( $value );
$sharedEnvsFound = str ( $value ) -> matchAll ( '/{{(.*?)}}/' );
if ( $sharedEnvsFound -> isEmpty ()) {
return $value ;
}
foreach ( $sharedEnvsFound as $sharedEnv ) {
$type = str ( $sharedEnv ) -> trim () -> match ( '/(.*?)\./' );
if ( ! collect ( SHARED_VARIABLE_TYPES ) -> contains ( $type )) {
continue ;
}
$variable = str ( $sharedEnv ) -> trim () -> match ( '/\.(.*)/' );
$id = null ;
if ( $type -> value () === 'environment' ) {
$id = $resource -> environment -> id ;
} elseif ( $type -> value () === 'project' ) {
$id = $resource -> environment -> project -> id ;
} elseif ( $type -> value () === 'team' ) {
$id = $resource -> team () -> id ;
}
if ( is_null ( $id )) {
continue ;
}
$found = \App\Models\SharedEnvironmentVariable :: where ( 'type' , $type )
-> where ( 'key' , $variable )
-> where ( 'team_id' , $resource -> team () -> id )
-> where ( " { $type } _id " , $id )
-> first ();
if ( $found ) {
$value = str ( $value ) -> replace ( " { { { $sharedEnv } }} " , $found -> value );
}
}
return str ( $value ) -> value ();
}