*/ 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 $oldRules * @param Collection $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']; } }