Files
coolify/app/Models/ApplicationSetting.php
T

288 lines
12 KiB
PHP

<?php
namespace App\Models;
use App\Services\Access\AccessResolver;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
#[OA\Schema(
description: 'Application settings.',
type: 'object',
properties: [
'is_static' => ['type' => 'boolean'],
'is_git_submodules_enabled' => ['type' => 'boolean'],
'is_git_lfs_enabled' => ['type' => 'boolean'],
'is_auto_deploy_enabled' => ['type' => 'boolean'],
'is_force_https_enabled' => ['type' => 'boolean'],
'is_debug_enabled' => ['type' => 'boolean'],
'is_preview_deployments_enabled' => ['type' => 'boolean'],
'is_log_drain_enabled' => ['type' => 'boolean'],
'is_gpu_enabled' => ['type' => 'boolean'],
'gpu_driver' => ['type' => 'string', 'nullable' => true],
'gpu_count' => ['type' => 'string', 'nullable' => true],
'gpu_device_ids' => ['type' => 'string', 'nullable' => true],
'gpu_options' => ['type' => 'string', 'nullable' => true],
'is_include_timestamps' => ['type' => 'boolean'],
'is_swarm_only_worker_nodes' => ['type' => 'boolean'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean'],
'is_build_server_enabled' => ['type' => 'boolean'],
'is_consistent_container_name_enabled' => ['type' => 'boolean'],
'is_gzip_enabled' => ['type' => 'boolean'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.'],
'is_stripprefix_enabled' => ['type' => 'boolean'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent (301/308) instead of temporary (302/307) proxy redirects.'],
'connect_to_docker_network' => ['type' => 'boolean'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true],
'is_container_label_escape_enabled' => ['type' => 'boolean'],
'is_env_sorting_enabled' => ['type' => 'boolean'],
'include_coolify_default_variables' => ['type' => 'boolean'],
'is_container_label_readonly_enabled' => ['type' => 'boolean'],
'is_preserve_repository_enabled' => ['type' => 'boolean'],
'disable_build_cache' => ['type' => 'boolean'],
'is_spa' => ['type' => 'boolean'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean'],
'use_build_secrets' => ['type' => 'boolean'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Hide every environment variable value in logs. When false, only locked values are hidden.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean'],
'include_source_commit_in_build' => ['type' => 'boolean'],
'docker_images_to_keep' => ['type' => 'integer'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true],
'rolling_update_drain_seconds' => ['type' => 'integer', 'description' => 'Seconds to wait after the new container is healthy before old containers stop during a rolling update. 0 turns the wait off.'],
]
)]
class ApplicationSetting extends Model
{
/**
* Keeps generated names (prefix, timestamp and for compose apps the service name) well below the
* 63 character DNS label limit, with room for a longer suffix in the future.
*/
public const MAX_CONTAINER_NAME_PREFIX_LENGTH = 30;
public const GPU_COUNT_PATTERN = '/^(all|\d+)$/';
protected $casts = [
'is_static' => 'boolean',
'is_spa' => 'boolean',
'is_build_server_enabled' => 'boolean',
'is_preserve_repository_enabled' => 'boolean',
'is_container_label_escape_enabled' => 'boolean',
'is_container_label_readonly_enabled' => 'boolean',
'use_build_secrets' => 'boolean',
'inject_build_args_to_dockerfile' => 'boolean',
'include_source_commit_in_build' => 'boolean',
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'is_debug_enabled' => 'boolean',
'is_preview_deployments_enabled' => 'boolean',
'is_pr_deployments_public_enabled' => 'boolean',
'is_git_submodules_enabled' => 'boolean',
'is_git_lfs_enabled' => 'boolean',
'is_git_shallow_clone_enabled' => 'boolean',
'docker_images_to_keep' => 'integer',
'stop_grace_period' => 'integer',
'rolling_update_drain_seconds' => 'integer',
'is_log_drain_enabled' => 'boolean',
'is_gpu_enabled' => 'boolean',
'is_include_timestamps' => 'boolean',
'is_swarm_only_worker_nodes' => 'boolean',
'is_raw_compose_deployment_enabled' => 'boolean',
'is_consistent_container_name_enabled' => 'boolean',
'is_gzip_enabled' => 'boolean',
'is_stripprefix_enabled' => 'boolean',
'is_permanent_redirect_enabled' => 'boolean',
'connect_to_docker_network' => 'boolean',
'is_env_sorting_enabled' => 'boolean',
'include_coolify_default_variables' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'disable_build_cache' => 'boolean',
'is_auto_cancel_queued_deployments_enabled' => 'boolean',
'is_auto_cancel_building_deployments_enabled' => 'boolean',
];
protected $fillable = [
'application_id',
'is_static',
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_auto_deploy_enabled',
'is_force_https_enabled',
'is_debug_enabled',
'is_preview_deployments_enabled',
'is_log_drain_enabled',
'is_gpu_enabled',
'gpu_driver',
'gpu_count',
'gpu_device_ids',
'gpu_options',
'is_include_timestamps',
'is_swarm_only_worker_nodes',
'is_raw_compose_deployment_enabled',
'is_build_server_enabled',
'is_consistent_container_name_enabled',
'is_gzip_enabled',
'traefik_certificate_resolver',
'is_stripprefix_enabled',
'is_permanent_redirect_enabled',
'connect_to_docker_network',
'custom_internal_name',
'custom_container_name_prefix',
'is_container_label_escape_enabled',
'is_env_sorting_enabled',
'include_coolify_default_variables',
'is_container_label_readonly_enabled',
'is_preserve_repository_enabled',
'disable_build_cache',
'is_spa',
'is_git_shallow_clone_enabled',
'is_pr_deployments_public_enabled',
'use_build_secrets',
'redact_all_env_values_in_logs',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'docker_images_to_keep',
'stop_grace_period',
'rolling_update_drain_seconds',
'is_auto_cancel_queued_deployments_enabled',
'is_auto_cancel_building_deployments_enabled',
];
/**
* Like custom container names, a prefix must be unique per server so that uuid, custom container
* name and prefix each identify one container when resolving connections.
*/
public static function isContainerNamePrefixInUse(string $prefix, Server $server, ?int $ignoreApplicationId = null): bool
{
// Hidden applications of a scoped member still count, or two names on one server could collide.
return app(AccessResolver::class)->withoutVisibilityScope(fn () => $server->applications()->contains(function (Application $application) use ($prefix, $ignoreApplicationId) {
return $application->id !== $ignoreApplicationId
&& in_array($prefix, [$application->uuid, $application->settings->custom_container_name_prefix, $application->settings->custom_internal_name], true);
}));
}
public function stopGracePeriodSeconds(): int
{
if (
$this->stop_grace_period >= MIN_STOP_GRACE_PERIOD_SECONDS &&
$this->stop_grace_period <= MAX_STOP_GRACE_PERIOD_SECONDS
) {
return $this->stop_grace_period;
}
return DEFAULT_STOP_GRACE_PERIOD_SECONDS;
}
public function rollingUpdateDrainSeconds(): int
{
return min(max($this->rolling_update_drain_seconds ?? DEFAULT_ROLLING_UPDATE_DRAIN_SECONDS, 0), MAX_ROLLING_UPDATE_DRAIN_SECONDS);
}
public function deploymentStopGracePeriodSeconds(): int
{
if (isDev() && $this->stop_grace_period === null) {
return MIN_STOP_GRACE_PERIOD_SECONDS;
}
return $this->stopGracePeriodSeconds();
}
/**
* The compose device request (deploy.resources.reservations.devices item) for the GPU settings.
* A count wins over device IDs. Options that cannot be parsed are left out.
*
* @return array{capabilities: list<string>, driver?: string, count?: int|string, device_ids?: list<string>, options?: array<string, string>}
*/
public function gpuDeviceReservation(): array
{
$device = ['capabilities' => ['gpu']];
$driver = trim((string) $this->gpu_driver);
if ($driver !== '') {
$device['driver'] = $driver;
}
$count = trim((string) $this->gpu_count);
$deviceIds = str((string) $this->gpu_device_ids)->explode(',')->map(fn (string $id) => trim($id))->filter(fn (string $id) => $id !== '')->values()->all();
if ($count !== '') {
$device['count'] = $count === 'all' ? 'all' : (int) $count;
} elseif ($deviceIds !== []) {
$device['device_ids'] = $deviceIds;
}
$options = self::parseGpuOptions($this->gpu_options);
if ($options) {
$device['options'] = $options;
}
return $device;
}
/**
* Parse GPU driver options written as a YAML/JSON mapping or as key=value pairs, one per line.
*
* @return array<string, string>|null null when the value is not a flat mapping
*/
public static function parseGpuOptions(?string $options): ?array
{
$options = trim((string) $options);
if ($options === '') {
return [];
}
try {
$parsed = Yaml::parse($options);
} catch (ParseException) {
$parsed = null;
}
if (is_array($parsed) && $parsed !== [] && ! array_is_list($parsed)) {
$mapping = [];
foreach ($parsed as $key => $value) {
if (! is_scalar($value)) {
return null;
}
$mapping[(string) $key] = is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
}
return $mapping;
}
$mapping = [];
foreach (preg_split('/\R/', $options) as $pair) {
$pair = trim($pair);
if ($pair === '') {
continue;
}
[$key, $value] = array_pad(explode('=', $pair, 2), 2, null);
if ($value === null || trim($key) === '') {
return null;
}
$mapping[trim($key)] = trim($value);
}
return $mapping;
}
public function isStatic(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value) {
$this->application->ports_exposes = 80;
}
$this->application->save();
return $value;
}
);
}
public function application()
{
return $this->belongsTo(Application::class);
}
}