mirror of
https://github.com/tiennm99/coolify.git
synced 2026-09-04 00:17:15 +00:00
Merge remote-tracking branch 'origin/next' into v5-parallel-inertia-react
This commit is contained in:
@@ -111,6 +111,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
new OA\Property(property: 'settings', ref: '#/components/schemas/ApplicationSetting'),
|
||||
]
|
||||
)]
|
||||
|
||||
|
||||
@@ -4,7 +4,49 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[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'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean'],
|
||||
'custom_internal_name' => ['type' => 'string', 'nullable' => true],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean'],
|
||||
'is_env_sorting_enabled' => ['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'],
|
||||
'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],
|
||||
]
|
||||
)]
|
||||
class ApplicationSetting extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
@@ -27,6 +69,17 @@ class ApplicationSetting extends Model
|
||||
'is_git_shallow_clone_enabled' => 'boolean',
|
||||
'docker_images_to_keep' => 'integer',
|
||||
'stop_grace_period' => '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',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'is_env_sorting_enabled' => 'boolean',
|
||||
'disable_build_cache' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
+120
-24
@@ -18,6 +18,7 @@ use App\Notifications\Server\Reachable;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use App\Services\DigitalOceanService;
|
||||
use App\Services\HetznerService;
|
||||
use App\Services\VultrService;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
@@ -112,6 +113,15 @@ class Server extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, SchemalessAttributesTrait, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Sentinel IP for servers that do not have a real address yet
|
||||
* (cloud provisioning in progress or parked as unreachable).
|
||||
* Scheduled jobs skip these servers via skipServer().
|
||||
*/
|
||||
public const PLACEHOLDER_IP = '1.2.3.4';
|
||||
|
||||
public const PLACEHOLDER_IPS = [self::PLACEHOLDER_IP, '0.0.0.0', '::'];
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
/**
|
||||
@@ -307,6 +317,85 @@ class Server extends BaseModel
|
||||
return 'server';
|
||||
}
|
||||
|
||||
public function hasPlaceholderIp(): bool
|
||||
{
|
||||
// Cast: the saving hook stores the ip as a Stringable in memory.
|
||||
return self::isPlaceholderIp((string) $this->ip);
|
||||
}
|
||||
|
||||
public static function isPlaceholderIp(?string $ip): bool
|
||||
{
|
||||
return blank($ip) || in_array($ip, self::PLACEHOLDER_IPS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a placeholder IP with the real address once the cloud
|
||||
* provider reports one. Returns true when the IP was updated.
|
||||
*/
|
||||
public function backfillPlaceholderIp(?string $ip): bool
|
||||
{
|
||||
if (self::isPlaceholderIp($ip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updated = static::query()
|
||||
->whereKey($this->getKey())
|
||||
->where(function (Builder $query): void {
|
||||
$query->whereNull('ip')
|
||||
->orWhere('ip', '')
|
||||
->orWhereIn('ip', self::PLACEHOLDER_IPS);
|
||||
})
|
||||
->update(['ip' => $ip]);
|
||||
|
||||
if ($updated === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->forceFill(['ip' => $ip]);
|
||||
$this->syncOriginalAttribute('ip');
|
||||
static::flushIdentityMap();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist provider status without saving a stale in-memory IP value.
|
||||
*
|
||||
* @param array<string, mixed> $updates
|
||||
*/
|
||||
private function persistProviderState(array $updates): void
|
||||
{
|
||||
if (empty($updates)) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::query()->whereKey($this->getKey())->update($updates);
|
||||
$this->forceFill($updates);
|
||||
$this->syncOriginalAttributes(array_keys($updates));
|
||||
static::flushIdentityMap();
|
||||
}
|
||||
|
||||
public function refreshHetznerState(): ?string
|
||||
{
|
||||
if (! $this->hetzner_server_id || ! $this->cloudProviderToken || $this->cloudProviderToken->provider !== 'hetzner') {
|
||||
return $this->hetzner_server_status;
|
||||
}
|
||||
|
||||
$hetznerService = new HetznerService($this->cloudProviderToken->token);
|
||||
$server = $hetznerService->getServer($this->hetzner_server_id);
|
||||
$status = $server['status'] ?? null;
|
||||
$assignedIp = data_get($server, 'public_net.ipv4.ip') ?? data_get($server, 'public_net.ipv6.ip');
|
||||
|
||||
$updates = [];
|
||||
if ($this->hetzner_server_status !== $status) {
|
||||
$updates['hetzner_server_status'] = $status;
|
||||
}
|
||||
$this->persistProviderState($updates);
|
||||
$this->backfillPlaceholderIp($assignedIp);
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function refreshVultrState(): ?string
|
||||
{
|
||||
if (! $this->vultr_instance_id || ! $this->cloudProviderToken) {
|
||||
@@ -322,8 +411,7 @@ class Server extends BaseModel
|
||||
}
|
||||
|
||||
if ($this->vultr_instance_status !== 'deleted') {
|
||||
$this->update(['vultr_instance_status' => 'deleted']);
|
||||
$this->forceFill(['vultr_instance_status' => 'deleted']);
|
||||
$this->persistProviderState(['vultr_instance_status' => 'deleted']);
|
||||
}
|
||||
|
||||
return 'deleted';
|
||||
@@ -338,16 +426,8 @@ class Server extends BaseModel
|
||||
if ($this->vultr_instance_status !== $status) {
|
||||
$updates['vultr_instance_status'] = $status;
|
||||
}
|
||||
|
||||
$hasPlaceholderIp = blank($this->ip) || in_array($this->ip, ['0.0.0.0', '::'], true);
|
||||
if ($hasPlaceholderIp && $publicIp) {
|
||||
$updates['ip'] = $publicIp;
|
||||
}
|
||||
|
||||
if (! empty($updates)) {
|
||||
$this->update($updates);
|
||||
$this->forceFill($updates);
|
||||
}
|
||||
$this->persistProviderState($updates);
|
||||
$this->backfillPlaceholderIp($publicIp);
|
||||
|
||||
return $status;
|
||||
}
|
||||
@@ -364,7 +444,7 @@ class Server extends BaseModel
|
||||
$droplet = $digitalOceanService->getDroplet((int) $this->digitalocean_droplet_id);
|
||||
} catch (RequestException $e) {
|
||||
if ($e->response?->status() === 404) {
|
||||
$this->update(['digitalocean_droplet_status' => 'deleted']);
|
||||
$this->persistProviderState(['digitalocean_droplet_status' => 'deleted']);
|
||||
|
||||
return 'deleted';
|
||||
}
|
||||
@@ -372,7 +452,7 @@ class Server extends BaseModel
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
if ((int) $e->getCode() === 404) {
|
||||
$this->update(['digitalocean_droplet_status' => 'deleted']);
|
||||
$this->persistProviderState(['digitalocean_droplet_status' => 'deleted']);
|
||||
|
||||
return 'deleted';
|
||||
}
|
||||
@@ -387,12 +467,8 @@ class Server extends BaseModel
|
||||
$status = $droplet['status'] ?? null;
|
||||
$ip = $digitalOceanService->getPublicIpAddress($droplet);
|
||||
|
||||
$updates = ['digitalocean_droplet_status' => $status];
|
||||
if ($ip && $ip !== $this->ip) {
|
||||
$updates['ip'] = $ip;
|
||||
}
|
||||
|
||||
$this->update($updates);
|
||||
$this->persistProviderState(['digitalocean_droplet_status' => $status]);
|
||||
$this->backfillPlaceholderIp($ip);
|
||||
|
||||
return $status;
|
||||
}
|
||||
@@ -433,9 +509,29 @@ class Server extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function isUsable()
|
||||
public static function isUsable(): Builder
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
|
||||
return self::usableByBuildServerStatus(false);
|
||||
}
|
||||
|
||||
public static function isUsableBuildServer(): Builder
|
||||
{
|
||||
return self::usableByBuildServerStatus(true);
|
||||
}
|
||||
|
||||
private static function usableByBuildServerStatus(bool $isBuildServer): Builder
|
||||
{
|
||||
return Server::ownedByCurrentTeam()
|
||||
->whereRelation('settings', 'is_reachable', true)
|
||||
->whereRelation('settings', 'is_usable', true)
|
||||
->whereRelation('settings', 'is_swarm_worker', false)
|
||||
->whereRelation('settings', 'is_build_server', $isBuildServer)
|
||||
->whereRelation('settings', 'force_disabled', false);
|
||||
}
|
||||
|
||||
public function canHostResources(): bool
|
||||
{
|
||||
return ! $this->isBuildServer();
|
||||
}
|
||||
|
||||
public function settings()
|
||||
@@ -1176,7 +1272,7 @@ $schema://$host {
|
||||
|
||||
public function skipServer()
|
||||
{
|
||||
if ($this->ip === '1.2.3.4') {
|
||||
if ($this->hasPlaceholderIp()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->settings->force_disabled === true) {
|
||||
@@ -1188,7 +1284,7 @@ $schema://$host {
|
||||
|
||||
public function isFunctional()
|
||||
{
|
||||
$isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4';
|
||||
$isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && ! $this->hasPlaceholderIp();
|
||||
|
||||
if ($isFunctional === false) {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
|
||||
@@ -109,6 +109,7 @@ class ServerSetting extends Model
|
||||
'sentinel_token' => 'encrypted',
|
||||
'is_reachable' => 'boolean',
|
||||
'is_usable' => 'boolean',
|
||||
'is_build_server' => 'boolean',
|
||||
'is_terminal_enabled' => 'boolean',
|
||||
'disable_application_image_retention' => 'boolean',
|
||||
'connection_timeout' => 'integer',
|
||||
|
||||
@@ -33,6 +33,13 @@ class ServiceDatabase extends BaseModel
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'exclude_from_status' => 'boolean',
|
||||
'is_public' => 'boolean',
|
||||
'is_log_drain_enabled' => 'boolean',
|
||||
'is_include_timestamps' => 'boolean',
|
||||
'is_gzip_enabled' => 'boolean',
|
||||
'is_stripprefix_enabled' => 'boolean',
|
||||
'public_port' => 'integer',
|
||||
'public_port_timeout' => 'integer',
|
||||
];
|
||||
|
||||
|
||||
@@ -370,6 +370,6 @@ class StandaloneClickhouse extends BaseModel
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,22 @@ use App\Support\ValidationPatterns;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
schema: 'Destination',
|
||||
description: 'A Docker network destination attached to a server.',
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'network', type: 'string'),
|
||||
new OA\Property(property: 'type', type: 'string', enum: ['standalone', 'swarm']),
|
||||
new OA\Property(property: 'server_uuid', type: 'string'),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
|
||||
],
|
||||
)]
|
||||
class StandaloneDocker extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
@@ -23,6 +38,10 @@ class StandaloneDocker extends BaseModel
|
||||
{
|
||||
parent::boot();
|
||||
static::created(function ($newStandaloneDocker) {
|
||||
if (app()->runningUnitTests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $newStandaloneDocker->server;
|
||||
$safeNetwork = escapeshellarg($newStandaloneDocker->network);
|
||||
instant_remote_process([
|
||||
|
||||
+5
-3
@@ -231,13 +231,15 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
$this->getNotificationSettings('webhook')?->isEnabled();
|
||||
}
|
||||
|
||||
public function subscriptionEnded()
|
||||
public function subscriptionEnded(?Subscription $subscription = null): void
|
||||
{
|
||||
if (! $this->subscription) {
|
||||
$subscription ??= $this->subscription;
|
||||
|
||||
if (! $subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->subscription->update([
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => null,
|
||||
'stripe_cancel_at_period_end' => false,
|
||||
'stripe_invoice_paid' => false,
|
||||
|
||||
Reference in New Issue
Block a user