mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 14:17:18 +00:00
chore(v5): archive V5 implementation and remove runtime integration
Move V5 source, migrations, UI, scripts, and tests into documentation, then remove V5 routes, models, jobs, configuration, dependencies, and application hooks.
This commit is contained in:
1 parent
1440d35750
commit
76030a30d4
237 files changed
+288
-7079
No files matched your search
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\Server as V5Server;
|
||||
|
||||
/**
|
||||
* Single source of truth for the canvas resource payloads served by the
|
||||
* dashboard Inertia props and broadcast by V5CanvasResourceUpdated — the two
|
||||
* must stay identical for websocket vs. initial-load parity.
|
||||
*/
|
||||
class CanvasResourceSerializer
|
||||
{
|
||||
public const CARD_WIDTH = 320;
|
||||
|
||||
public const CARD_HEIGHT = 144;
|
||||
|
||||
public const CARD_GAP = 32;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function serializeApplication(V5Application $application): array
|
||||
{
|
||||
$application->loadMissing(['server', 'domains', 'project', 'environment']);
|
||||
$server = $application->server;
|
||||
$isServerReachable = ! $server instanceof V5Server || $this->isServerReachable($server);
|
||||
|
||||
return [
|
||||
'id' => $application->uuid,
|
||||
'name' => $application->name,
|
||||
'image' => $application->image,
|
||||
'containerName' => $application->container_name,
|
||||
'status' => $application->status,
|
||||
'statusMessage' => $application->status_message,
|
||||
'effectiveStatus' => $isServerReachable ? $application->status : 'unknown',
|
||||
'effectiveStatusMessage' => $isServerReachable
|
||||
? $application->status_message
|
||||
: $this->serverStatusMessage($server),
|
||||
'runtimeContainerId' => $application->runtime_container_id,
|
||||
'serverName' => $server?->name,
|
||||
'serverStatus' => $server?->status,
|
||||
'serverStatusMessage' => $server instanceof V5Server ? $this->serverStatusMessage($server) : null,
|
||||
'isServerReachable' => $isServerReachable,
|
||||
'serverIngressEnabled' => (bool) $server?->isIngress(),
|
||||
'meshNamespace' => $application->mesh_namespace,
|
||||
'ingressEnabled' => $application->ingress_enabled,
|
||||
'internalPort' => $application->internal_port,
|
||||
'domains' => $application->domains->pluck('domain')->values()->all(),
|
||||
'meshFqdn' => $application->container_name.'.'.($application->mesh_namespace ?: 'default').'.coolify.internal',
|
||||
'projectUuid' => $application->project?->uuid,
|
||||
'environmentUuid' => $application->environment?->uuid,
|
||||
'canvasX' => $application->canvas_x,
|
||||
'canvasY' => $application->canvas_y,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function serializeCaddyIngress(V5Server $server, int $index = 0): array
|
||||
{
|
||||
$isServerReachable = $this->isServerReachable($server);
|
||||
|
||||
return [
|
||||
'id' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'host' => $server->host,
|
||||
'type' => $server->ingressType(),
|
||||
'status' => $isServerReachable ? $server->ingressStatus() : 'unreachable',
|
||||
'statusMessage' => $isServerReachable ? null : $this->serverStatusMessage($server),
|
||||
'canvasX' => $server->canvas_x ?? -(self::CARD_WIDTH + self::CARD_GAP),
|
||||
'canvasY' => $server->canvas_y ?? $index * (self::CARD_HEIGHT + self::CARD_GAP),
|
||||
];
|
||||
}
|
||||
|
||||
private function isServerReachable(V5Server $server): bool
|
||||
{
|
||||
return $server->status !== 'unreachable';
|
||||
}
|
||||
|
||||
private function serverStatusMessage(?V5Server $server): ?string
|
||||
{
|
||||
return $server?->last_status_output ?: null;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
use App\Models\V5\Cluster as V5Cluster;
|
||||
use App\Models\V5\Server as V5Server;
|
||||
|
||||
/**
|
||||
* Single source of truth for the cluster payload served by the Clusters
|
||||
* Inertia props and broadcast by V5ClusterUpdated — the two must stay
|
||||
* identical for websocket vs. initial-load parity.
|
||||
*/
|
||||
class ClusterSerializer
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function serialize(V5Cluster $cluster): array
|
||||
{
|
||||
return [
|
||||
'id' => $cluster->uuid,
|
||||
'name' => $cluster->name,
|
||||
'description' => $cluster->description,
|
||||
'wireguardInterface' => $cluster->wireguard_interface,
|
||||
'wireguardManagementPool' => $cluster->wireguard_management_pool,
|
||||
'wireguardListenPort' => $cluster->wireguard_listen_port,
|
||||
'containerNetworkPool' => $cluster->container_network_pool,
|
||||
'containerNetworkPrefix' => $cluster->container_network_prefix,
|
||||
'namespaces' => $cluster->namespaces ?? V5Cluster::DEFAULT_NAMESPACES,
|
||||
'defaultDenyContainers' => $cluster->default_deny_containers,
|
||||
'cooldVersion' => $cluster->coold_version,
|
||||
'corrosionVersion' => $cluster->corrosion_version,
|
||||
'corrosionGossipPort' => $cluster->corrosion_gossip_port,
|
||||
'corrosionApiPort' => $cluster->corrosion_api_port,
|
||||
'builderEnabled' => $cluster->builder_enabled,
|
||||
'builderCapacity' => $cluster->builder_capacity,
|
||||
'builderCpuQuota' => $cluster->builder_cpu_quota,
|
||||
'builderMemoryMax' => $cluster->builder_memory_max,
|
||||
'builderTimeoutSecs' => $cluster->builder_timeout_secs,
|
||||
'lastCliAction' => $cluster->last_cli_action,
|
||||
'lastCliStatus' => $cluster->last_cli_status,
|
||||
'lastCliSummary' => $cluster->last_cli_summary,
|
||||
'lastCliRanAt' => $cluster->last_cli_ran_at?->toJSON(),
|
||||
'serversCount' => $cluster->servers_count ?? $cluster->servers->count(),
|
||||
'servers' => $cluster->servers->map(fn (V5Server $server) => [
|
||||
'id' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'host' => $server->host,
|
||||
'status' => $server->status,
|
||||
'capabilities' => $server->capabilities ?? [],
|
||||
'builderEnabled' => $server->builder_enabled,
|
||||
'builderCapacity' => $server->builder_capacity,
|
||||
'builderCpuQuota' => $server->builder_cpu_quota,
|
||||
'ingressEnabled' => $server->isIngress(),
|
||||
'ingressType' => $server->ingress_type,
|
||||
'uuid' => $server->uuid,
|
||||
'nodeAddress' => $server->node_address,
|
||||
'wireguardListenPortOverride' => $server->wireguard_listen_port_override,
|
||||
'wireguardEndpointOverride' => $server->wireguard_endpoint_override,
|
||||
'wireguardManagementIp' => $server->wireguard_management_ip,
|
||||
'wireguardPublicKey' => $server->wireguard_public_key,
|
||||
'containerSubnets' => $server->container_subnets ?? [],
|
||||
'privateKeyName' => $server->privateKey?->name,
|
||||
'lastBootstrappedAt' => $server->last_bootstrapped_at?->toJSON(),
|
||||
'lastBootstrapAction' => $server->last_bootstrap_action,
|
||||
'lastBootstrapStatus' => $server->last_bootstrap_status,
|
||||
'lastBootstrapOutput' => $server->last_bootstrap_output,
|
||||
'lastBootstrapRanAt' => $server->last_bootstrap_ran_at?->toJSON(),
|
||||
'lastStatusOutput' => $server->last_status_output,
|
||||
'lastStatusCheckedAt' => $server->last_status_checked_at?->toJSON(),
|
||||
])->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload servers (with keys) and counts before serializing so the payload
|
||||
* always reflects the latest database state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function serializeFresh(V5Cluster $cluster): array
|
||||
{
|
||||
$cluster->load(['servers' => fn ($query) => $query
|
||||
->with('privateKey')
|
||||
->orderBy('name')]);
|
||||
$cluster->loadCount('servers');
|
||||
|
||||
return $this->serialize($cluster);
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
use App\Exceptions\V5\UnsupportedCooldVerb;
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection;
|
||||
use App\Models\V5\Server as V5Server;
|
||||
use App\Services\Flux\FluxClient;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Single source of truth for deriving node firewall rules from a resource
|
||||
* connection's DB rules and converging them through Flux. Reusable from
|
||||
* controllers, jobs, and events alike; deterministic rule ids keep repeated
|
||||
* syncs and compensating rollbacks idempotent.
|
||||
*/
|
||||
class ConnectionFirewallSync
|
||||
{
|
||||
/**
|
||||
* @return Collection<int, array{id: string, hostId: string, rule: array{id: string, namespace: string, src: string, dst: string, proto: string, port: int}}>
|
||||
*/
|
||||
public function rulesFor(ResourceConnection $connection): Collection
|
||||
{
|
||||
$applicationIds = $connection->rules
|
||||
->flatMap(fn ($rule) => [$rule->source_resource_id, $rule->target_resource_id])
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$applications = V5Application::query()
|
||||
->whereIn('id', $applicationIds)
|
||||
->with('server')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return $connection->rules
|
||||
->flatMap(function ($rule) use ($applications, $connection): Collection {
|
||||
$source = $applications->get($rule->source_resource_id);
|
||||
$target = $applications->get($rule->target_resource_id);
|
||||
|
||||
if (! $source instanceof V5Application || ! $target instanceof V5Application) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$missingHost = collect([$source, $target])
|
||||
->first(function (V5Application $application): bool {
|
||||
$hostId = $application->server?->fluxHostId();
|
||||
|
||||
return ! is_string($hostId) || $hostId === '';
|
||||
});
|
||||
|
||||
if ($missingHost instanceof V5Application) {
|
||||
throw new \RuntimeException("Application {$missingHost->name} has no reachable server host id, so its firewall rules cannot be synced.");
|
||||
}
|
||||
|
||||
$hostIds = collect([$source->server, $target->server])
|
||||
->map(fn (V5Server $server) => $server->fluxHostId())
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$firewallRule = [
|
||||
'id' => $this->ruleId($connection, $rule),
|
||||
'namespace' => $target->mesh_namespace ?: 'default',
|
||||
'src' => $source->container_name,
|
||||
'dst' => $target->container_name,
|
||||
'proto' => $rule->protocol ?: 'tcp',
|
||||
'port' => (int) $rule->port,
|
||||
];
|
||||
|
||||
return $hostIds->map(fn (string $hostId): array => [
|
||||
'id' => $firewallRule['id'],
|
||||
'hostId' => $hostId,
|
||||
'rule' => $firewallRule,
|
||||
]);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, array{id: string, hostId: string, rule: array{id: string, namespace: string, src: string, dst: string, proto: string, port: int}}> $oldRules
|
||||
* @param Collection<int, array{id: string, hostId: string, rule: array{id: string, namespace: string, src: string, dst: string, proto: string, port: int}}> $newRules
|
||||
*/
|
||||
public function sync(FluxClient $fluxClient, Collection $oldRules, Collection $newRules): void
|
||||
{
|
||||
$newRuleKeys = $newRules->map(fn (array $rule): string => $this->syncKey($rule))->all();
|
||||
$oldRuleKeys = $oldRules->map(fn (array $rule): string => $this->syncKey($rule))->all();
|
||||
|
||||
$oldRules
|
||||
->reject(fn (array $oldRule): bool => in_array($this->syncKey($oldRule), $newRuleKeys, true))
|
||||
->each(fn (array $oldRule): ?string => $this->revokeRuleIfPresent($fluxClient, $oldRule['hostId'], $oldRule['id']));
|
||||
|
||||
$newRules
|
||||
->reject(fn (array $newRule): bool => in_array($this->syncKey($newRule), $oldRuleKeys, true))
|
||||
->each(function (array $newRule) use ($fluxClient): void {
|
||||
try {
|
||||
$fluxClient->applyFirewallRule($newRule['hostId'], $newRule['rule']);
|
||||
} catch (UnsupportedCooldVerb $exception) {
|
||||
Log::warning('V5 resource connection firewall rule skipped: coold verb unsupported', [
|
||||
'host_id' => $newRule['hostId'],
|
||||
'rule_id' => $newRule['id'],
|
||||
'verb' => $exception->verb,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function revokeRuleIfPresent(FluxClient $fluxClient, string $hostId, string $ruleId): ?string
|
||||
{
|
||||
try {
|
||||
return $fluxClient->revokeFirewallRule($hostId, $ruleId);
|
||||
} catch (UnsupportedCooldVerb $exception) {
|
||||
Log::warning('V5 resource connection firewall revoke skipped: coold verb unsupported', [
|
||||
'host_id' => $hostId,
|
||||
'rule_id' => $ruleId,
|
||||
'verb' => $exception->verb,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
} catch (\RuntimeException $exception) {
|
||||
if (str_contains(Str::lower($exception->getMessage()), 'not found')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic node-side rule id derived only from the connection id and
|
||||
* the rule's stable attributes — never from the rule row's primary key —
|
||||
* so rewritten or restored DB rows resolve to the same firewall rule ids
|
||||
* and compensating re-syncs stay idempotent.
|
||||
*/
|
||||
public function ruleId(ResourceConnection $connection, mixed $rule): string
|
||||
{
|
||||
return implode(':', [
|
||||
'v5-resource-connection',
|
||||
$connection->id,
|
||||
$rule->source_resource_id,
|
||||
$rule->target_resource_id,
|
||||
$rule->protocol ?: 'tcp',
|
||||
(int) $rule->port,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id: string, hostId: string, rule: array{id: string, namespace: string, src: string, dst: string, proto: string, port: int}} $rule
|
||||
*/
|
||||
private function syncKey(array $rule): string
|
||||
{
|
||||
return $rule['hostId'].'|'.$rule['id'];
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Single source of truth for the resource connection payloads served by the
|
||||
* dashboard Inertia props and the connection endpoints — the wire format is
|
||||
* consumed by resources/js/v5/types.ts and must stay stable.
|
||||
*/
|
||||
class ResourceConnectionSerializer
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function serialize(ResourceConnection $connection): array
|
||||
{
|
||||
$applications = $this->applicationsById($connection);
|
||||
$resourceOneUuid = $applications->get($connection->resource_one_id)?->uuid;
|
||||
$resourceTwoUuid = $applications->get($connection->resource_two_id)?->uuid;
|
||||
$applicationsById = $applications;
|
||||
|
||||
return [
|
||||
'id' => $connection->uuid,
|
||||
'applicationIds' => array_values(array_filter([
|
||||
$resourceOneUuid,
|
||||
$resourceTwoUuid,
|
||||
])),
|
||||
'fromApplicationId' => $resourceOneUuid,
|
||||
'toApplicationId' => $resourceTwoUuid,
|
||||
'portsByDirection' => $connection->rules
|
||||
->groupBy(function ($rule) use ($applicationsById): string {
|
||||
$sourceUuid = $applicationsById->get($rule->source_resource_id)?->uuid;
|
||||
$targetUuid = $applicationsById->get($rule->target_resource_id)?->uuid;
|
||||
|
||||
return "{$sourceUuid}->{$targetUuid}";
|
||||
})
|
||||
->filter(fn (Collection $rules, string $direction): bool => ! str_starts_with($direction, '->') && ! str_ends_with($direction, '->'))
|
||||
->map(fn (Collection $rules) => $rules
|
||||
->sortBy('port')
|
||||
->pluck('port')
|
||||
->map(fn ($port) => (string) $port)
|
||||
->values()
|
||||
->all())
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<string, V5Application>
|
||||
*/
|
||||
public function applicationsByUuid(ResourceConnection $connection): Collection
|
||||
{
|
||||
return $this->applicationsById($connection)->keyBy('uuid');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, V5Application>
|
||||
*/
|
||||
public function applicationsById(ResourceConnection $connection): Collection
|
||||
{
|
||||
return V5Application::query()
|
||||
->whereIn('id', [
|
||||
(int) $connection->resource_one_id,
|
||||
(int) $connection->resource_two_id,
|
||||
])
|
||||
->get()
|
||||
->keyBy('id');
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
use App\Enums\V5\ApplicationStatus;
|
||||
use App\Enums\V5\ContainerState;
|
||||
use App\Enums\V5\IngressStatus;
|
||||
use App\Enums\V5\ServerStatus;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Shared status-observation watermarking and enum normalization used by every
|
||||
* v5 status write path (the flux webhook, the reconcile job, and the manual
|
||||
* refresh endpoint) so out-of-order updates are dropped and raw coold states
|
||||
* are normalized identically everywhere.
|
||||
*/
|
||||
class StatusObservation
|
||||
{
|
||||
/**
|
||||
* A write whose observation timestamp is older than the one already
|
||||
* persisted is stale (delivered or computed out of order) and must not
|
||||
* clobber the newer state.
|
||||
*
|
||||
* @param array<string, mixed> $logContext
|
||||
*/
|
||||
public static function isStale(?CarbonInterface $observedAt, ?CarbonInterface $currentObservedAt, string $context, array $logContext): bool
|
||||
{
|
||||
if ($observedAt === null || $currentObservedAt === null || ! $observedAt->lt($currentObservedAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::debug("Dropping stale flux {$context} update.", [
|
||||
...$logContext,
|
||||
'observed_at' => $observedAt->toIso8601String(),
|
||||
'current_status_observed_at' => $currentObservedAt->toIso8601String(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a raw status string onto the given status enum. Unknown values are
|
||||
* never written to the database: they fall back to the enum's Unknown case
|
||||
* and are logged. Returns null only when no raw value is supplied.
|
||||
*
|
||||
* @param class-string<ApplicationStatus|ContainerState|IngressStatus|ServerStatus> $enumClass
|
||||
*/
|
||||
public static function normalize(?string $raw, string $enumClass): ?string
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status = $enumClass::tryFrom(strtolower($raw));
|
||||
|
||||
if ($status === null) {
|
||||
Log::warning('Received unknown flux resource status; falling back to unknown.', [
|
||||
'raw_status' => $raw,
|
||||
'status_enum' => $enumClass,
|
||||
]);
|
||||
|
||||
return $enumClass::Unknown->value;
|
||||
}
|
||||
|
||||
return $status->value;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\V5;
|
||||
|
||||
class V5Feature
|
||||
{
|
||||
private const DEVELOPMENT_ENVIRONMENTS = ['local', 'development', 'dev', 'testing'];
|
||||
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return (bool) config('v5.enabled');
|
||||
}
|
||||
|
||||
public static function enabledForEnvironment(string $environment): bool
|
||||
{
|
||||
return in_array($environment, self::DEVELOPMENT_ENVIRONMENTS, true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user