coolify/app/Models/ServiceApplication.php

257 lines
7.9 KiB
PHP
Raw Normal View History

2023-09-20 13:42:41 +00:00
<?php
namespace App\Models;
2023-09-27 13:48:19 +00:00
use Illuminate\Database\Eloquent\Casts\Attribute;
2023-09-20 13:42:41 +00:00
use Illuminate\Database\Eloquent\Factories\HasFactory;
2023-12-13 11:08:12 +00:00
use Illuminate\Database\Eloquent\SoftDeletes;
2023-09-20 13:42:41 +00:00
class ServiceApplication extends BaseModel
{
2023-12-13 11:08:12 +00:00
use HasFactory, SoftDeletes;
2024-06-10 20:43:34 +00:00
2023-09-20 13:42:41 +00:00
protected $guarded = [];
2023-11-06 17:04:18 +00:00
protected static function booted()
{
static::deleting(function ($service) {
$service->update(['fqdn' => null]);
2023-11-06 17:04:18 +00:00
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
}
});
2023-11-06 17:04:18 +00:00
}
2024-06-10 20:43:34 +00:00
2024-03-25 10:33:38 +00:00
public function restart()
{
2024-09-23 17:51:31 +00:00
$container_id = $this->name.'-'.$this->service->uuid;
2024-03-25 10:33:38 +00:00
instant_remote_process(["docker restart {$container_id}"], $this->service->server);
}
2024-06-10 20:43:34 +00:00
2024-07-01 09:39:10 +00:00
public static function ownedByCurrentTeamAPI(int $teamId)
{
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
public static function ownedByCurrentTeam()
{
return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
2024-09-16 13:35:44 +00:00
public function isRunning()
{
return str($this->status)->contains('running');
}
public function isExited()
{
return str($this->status)->contains('exited');
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
2024-06-10 20:43:34 +00:00
public function isStripprefixEnabled()
{
return data_get($this, 'is_stripprefix_enabled', true);
}
2024-06-10 20:43:34 +00:00
public function isGzipEnabled()
{
return data_get($this, 'is_gzip_enabled', true);
}
2024-06-10 20:43:34 +00:00
2023-09-22 09:23:49 +00:00
public function type()
{
return 'service';
}
2024-06-10 20:43:34 +00:00
2024-05-28 12:49:03 +00:00
public function team()
{
return data_get($this, 'environment.project.team');
}
2024-06-10 20:43:34 +00:00
public function workdir()
{
2024-09-23 17:51:31 +00:00
return service_configuration_dir()."/{$this->service->uuid}";
2024-05-24 15:26:05 +00:00
}
2024-06-10 20:43:34 +00:00
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
2024-06-10 20:43:34 +00:00
return null;
}
2024-06-10 20:43:34 +00:00
2023-09-22 10:08:51 +00:00
public function service()
{
return $this->belongsTo(Service::class);
}
2024-06-10 20:43:34 +00:00
2023-09-22 09:23:49 +00:00
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
2024-06-10 20:43:34 +00:00
2023-09-22 19:31:47 +00:00
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
2024-06-10 20:43:34 +00:00
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
2023-09-27 13:48:19 +00:00
public function fqdns(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
}
2024-06-10 20:43:34 +00:00
/**
* Extract port number from a given FQDN URL.
* Returns null if no port is specified.
*/
public static function extractPortFromUrl(string $url): ?int
{
try {
// Ensure URL has a scheme for proper parsing
if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) {
$url = 'http://'.$url;
}
$parsed = parse_url($url);
$port = $parsed['port'] ?? null;
return $port ? (int) $port : null;
} catch (\Throwable) {
return null;
}
}
/**
* Check if all FQDNs have a port specified.
*/
public function allFqdnsHavePort(): bool
{
if (is_null($this->fqdn) || $this->fqdn === '') {
return false;
}
$fqdns = explode(',', $this->fqdn);
foreach ($fqdns as $fqdn) {
$fqdn = trim($fqdn);
if (empty($fqdn)) {
continue;
}
$port = self::extractPortFromUrl($fqdn);
if ($port === null) {
return false;
}
}
return true;
}
2023-10-02 16:02:32 +00:00
public function getFilesFromServer(bool $isInit = false)
{
2023-10-02 16:02:32 +00:00
getFilesystemVolumesFromServer($this, $isInit);
}
2024-10-03 18:47:02 +00:00
public function isBackupSolutionAvailable()
{
return false;
}
/**
* Get the required port for this service application.
* Extracts port from SERVICE_URL_* or SERVICE_FQDN_* environment variables
* stored at the Service level, filtering by normalized container name.
* Falls back to service-level port if no port-specific variable is found.
*/
public function getRequiredPort(): ?int
{
try {
// Parse the Docker Compose to find SERVICE_URL/SERVICE_FQDN variables DIRECTLY DECLARED
// for this specific service container (not just referenced from other containers)
$dockerComposeRaw = data_get($this->service, 'docker_compose_raw');
if (! $dockerComposeRaw) {
// Fall back to service-level port if no compose file
return $this->service->getRequiredPort();
}
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
$serviceConfig = data_get($dockerCompose, "services.{$this->name}");
if (! $serviceConfig) {
return $this->service->getRequiredPort();
}
$environment = data_get($serviceConfig, 'environment', []);
// Extract SERVICE_URL and SERVICE_FQDN variables DIRECTLY DECLARED in this service's environment
// (not variables that are merely referenced with ${VAR} syntax)
$portFound = null;
foreach ($environment as $key => $value) {
if (is_int($key) && is_string($value)) {
// List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
// Extract variable name (before '=' if present)
$envVarName = str($value)->before('=')->trim();
// Only process direct declarations
if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
// Parse to check if it has a port suffix
$parsed = parseServiceEnvironmentVariable($envVarName->value());
if ($parsed['has_port'] && $parsed['port']) {
// Found a port-specific variable for this service
$portFound = (int) $parsed['port'];
break;
}
}
} elseif (is_string($key)) {
// Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
$envVarName = str($key);
// Only process direct declarations
if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
// Parse to check if it has a port suffix
$parsed = parseServiceEnvironmentVariable($envVarName->value());
if ($parsed['has_port'] && $parsed['port']) {
// Found a port-specific variable for this service
$portFound = (int) $parsed['port'];
break;
}
}
}
}
// If a port was found in the template, return it
if ($portFound !== null) {
return $portFound;
}
// No port-specific variables found for this service, return null
// (DO NOT fall back to service-level port, as that applies to all services)
return null;
} catch (\Throwable $e) {
return null;
}
}
2023-09-20 13:42:41 +00:00
}