2026-06-16 13:45:16 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models\V5;
|
|
|
|
|
|
2026-07-06 15:40:37 +00:00
|
|
|
use App\Enums\V5\IngressStatus;
|
2026-06-20 07:23:16 +00:00
|
|
|
use App\Events\V5CanvasResourceUpdated;
|
|
|
|
|
use App\Events\V5ClusterUpdated;
|
2026-06-16 13:45:16 +00:00
|
|
|
use App\Models\PrivateKey;
|
|
|
|
|
use App\Models\Team;
|
|
|
|
|
use App\Models\User;
|
2026-07-06 15:40:37 +00:00
|
|
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
2026-06-16 13:45:16 +00:00
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
2026-07-06 15:40:37 +00:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-06-16 13:45:16 +00:00
|
|
|
|
|
|
|
|
class Server extends V5Model
|
|
|
|
|
{
|
|
|
|
|
protected $table = 'v5_servers';
|
|
|
|
|
|
|
|
|
|
protected $fillable = [
|
2026-06-19 09:44:42 +00:00
|
|
|
'uuid',
|
2026-06-16 13:45:16 +00:00
|
|
|
'team_id',
|
|
|
|
|
'cluster_id',
|
|
|
|
|
'created_by_user_id',
|
|
|
|
|
'private_key_id',
|
|
|
|
|
'name',
|
|
|
|
|
'host',
|
|
|
|
|
'ssh_user',
|
|
|
|
|
'ssh_port',
|
|
|
|
|
'status',
|
2026-07-06 15:40:37 +00:00
|
|
|
'status_observed_at',
|
2026-06-20 21:10:48 +00:00
|
|
|
'ingress_type',
|
|
|
|
|
'ingress_status',
|
2026-06-16 13:45:16 +00:00
|
|
|
'capabilities',
|
2026-07-06 15:40:37 +00:00
|
|
|
'has_coold',
|
|
|
|
|
'is_ingress',
|
2026-06-16 13:45:16 +00:00
|
|
|
'builder_enabled',
|
|
|
|
|
'builder_capacity',
|
2026-06-19 05:23:45 +00:00
|
|
|
'builder_cpu_quota',
|
|
|
|
|
'node_address',
|
|
|
|
|
'wireguard_listen_port_override',
|
|
|
|
|
'wireguard_endpoint_override',
|
|
|
|
|
'wireguard_management_ip',
|
|
|
|
|
'wireguard_public_key',
|
2026-07-06 15:40:37 +00:00
|
|
|
'coold_version',
|
|
|
|
|
'agent_token_jti',
|
|
|
|
|
'agent_token_expires_at',
|
2026-06-19 05:23:45 +00:00
|
|
|
'container_subnets',
|
2026-06-20 07:23:16 +00:00
|
|
|
'canvas_x',
|
|
|
|
|
'canvas_y',
|
2026-06-16 13:45:16 +00:00
|
|
|
'last_bootstrapped_at',
|
2026-06-19 09:44:42 +00:00
|
|
|
'last_bootstrap_action',
|
|
|
|
|
'last_bootstrap_status',
|
|
|
|
|
'last_bootstrap_output',
|
|
|
|
|
'last_bootstrap_ran_at',
|
2026-06-19 05:23:45 +00:00
|
|
|
'last_status_check',
|
|
|
|
|
'last_status_output',
|
|
|
|
|
'last_status_checked_at',
|
2026-06-16 13:45:16 +00:00
|
|
|
];
|
|
|
|
|
|
2026-06-20 07:23:16 +00:00
|
|
|
protected static function booted(): void
|
|
|
|
|
{
|
|
|
|
|
static::updated(function (self $server): void {
|
2026-06-20 21:10:48 +00:00
|
|
|
if (
|
|
|
|
|
! $server->wasChanged('status')
|
|
|
|
|
&& ! $server->wasChanged('ingress_type')
|
|
|
|
|
&& ! $server->wasChanged('ingress_status')
|
|
|
|
|
) {
|
2026-06-20 07:23:16 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($server->wasChanged('status') && $server->cluster_id !== null) {
|
2026-07-06 15:40:37 +00:00
|
|
|
DB::afterCommit(fn () => V5ClusterUpdated::dispatch($server->team_id, $server->cluster_id));
|
2026-06-20 07:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-22 09:56:26 +00:00
|
|
|
if ($server->wasChanged('status')) {
|
2026-07-06 15:40:37 +00:00
|
|
|
DB::afterCommit(fn () => V5CanvasResourceUpdated::dispatch(
|
2026-06-22 09:56:26 +00:00
|
|
|
$server->team_id,
|
|
|
|
|
null,
|
|
|
|
|
$server->isIngress() ? $server->id : null,
|
|
|
|
|
$server->id,
|
2026-07-06 15:40:37 +00:00
|
|
|
));
|
2026-06-22 09:56:26 +00:00
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 07:23:16 +00:00
|
|
|
if ($server->isIngress()) {
|
2026-07-06 15:40:37 +00:00
|
|
|
DB::afterCommit(fn () => V5CanvasResourceUpdated::dispatch($server->team_id, null, $server->id));
|
2026-06-20 07:23:16 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 13:45:16 +00:00
|
|
|
protected function casts(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
2026-07-06 15:40:37 +00:00
|
|
|
'has_coold' => 'boolean',
|
|
|
|
|
'is_ingress' => 'boolean',
|
2026-06-16 13:45:16 +00:00
|
|
|
'builder_enabled' => 'boolean',
|
2026-06-19 05:23:45 +00:00
|
|
|
'container_subnets' => 'array',
|
2026-06-20 07:23:16 +00:00
|
|
|
'canvas_x' => 'integer',
|
|
|
|
|
'canvas_y' => 'integer',
|
2026-07-06 15:40:37 +00:00
|
|
|
'status_observed_at' => 'datetime',
|
|
|
|
|
'agent_token_expires_at' => 'datetime',
|
2026-06-16 13:45:16 +00:00
|
|
|
'last_bootstrapped_at' => 'datetime',
|
2026-06-19 09:44:42 +00:00
|
|
|
'last_bootstrap_ran_at' => 'datetime',
|
2026-06-19 05:23:45 +00:00
|
|
|
'last_status_checked_at' => 'datetime',
|
2026-06-16 13:45:16 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 15:40:37 +00:00
|
|
|
public function fluxHostId(): string
|
|
|
|
|
{
|
|
|
|
|
return (string) $this->uuid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Virtual attribute kept for wire-format compatibility: capabilities are
|
|
|
|
|
* stored as the indexed has_coold / is_ingress booleans, but reads and
|
|
|
|
|
* writes of `capabilities` keep working with the historical string array.
|
|
|
|
|
* Unknown capability names are dropped on write.
|
|
|
|
|
*
|
|
|
|
|
* The dropped `capabilities` column intentionally stays in `$fillable`:
|
|
|
|
|
* call sites still mass-assign it, and this mutator maps those writes
|
|
|
|
|
* onto the boolean columns.
|
|
|
|
|
*/
|
|
|
|
|
protected function capabilities(): Attribute
|
|
|
|
|
{
|
|
|
|
|
return Attribute::make(
|
|
|
|
|
get: fn () => array_values(array_filter([
|
|
|
|
|
$this->has_coold ? 'coold' : null,
|
|
|
|
|
$this->is_ingress ? 'ingress' : null,
|
|
|
|
|
])),
|
|
|
|
|
set: fn (?array $capabilities) => [
|
|
|
|
|
'has_coold' => in_array('coold', $capabilities ?? [], true),
|
|
|
|
|
'is_ingress' => in_array('ingress', $capabilities ?? [], true),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 07:23:16 +00:00
|
|
|
public function hasCapability(string $capability): bool
|
|
|
|
|
{
|
2026-07-06 15:40:37 +00:00
|
|
|
return match ($capability) {
|
|
|
|
|
'coold' => (bool) $this->has_coold,
|
|
|
|
|
'ingress' => (bool) $this->is_ingress,
|
|
|
|
|
default => false,
|
|
|
|
|
};
|
2026-06-20 07:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<int, string>
|
|
|
|
|
*/
|
|
|
|
|
public function withCapability(string $capability): array
|
|
|
|
|
{
|
2026-07-06 15:40:37 +00:00
|
|
|
return collect($this->capabilities)
|
2026-06-20 07:23:16 +00:00
|
|
|
->push($capability)
|
|
|
|
|
->unique()
|
|
|
|
|
->values()
|
|
|
|
|
->all();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<int, string>
|
|
|
|
|
*/
|
|
|
|
|
public function withoutCapability(string $capability): array
|
|
|
|
|
{
|
2026-07-06 15:40:37 +00:00
|
|
|
return collect($this->capabilities)
|
2026-06-20 07:23:16 +00:00
|
|
|
->reject(fn (string $existingCapability) => $existingCapability === $capability)
|
|
|
|
|
->values()
|
|
|
|
|
->all();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isIngress(): bool
|
|
|
|
|
{
|
2026-07-06 15:40:37 +00:00
|
|
|
return (bool) $this->is_ingress;
|
2026-06-20 07:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-20 21:10:48 +00:00
|
|
|
public function ingressStatus(): string
|
2026-06-20 07:23:16 +00:00
|
|
|
{
|
2026-07-06 15:40:37 +00:00
|
|
|
return $this->ingress_status ?? IngressStatus::Unknown->value;
|
2026-06-20 07:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-20 21:10:48 +00:00
|
|
|
public function ingressType(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->ingress_type ?? 'caddy';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 13:45:16 +00:00
|
|
|
public function cluster(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Cluster::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function team(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Team::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function creator(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class, 'created_by_user_id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function privateKey(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(PrivateKey::class);
|
|
|
|
|
}
|
|
|
|
|
}
|