feat(tunnels): add Service type with dynamic env.php endpoint discovery
CI / php (push) Successful in 2m53s
CI / php (push) Successful in 2m53s
- Add EnvServicePortResolver (DeploymentConfig) for MySQL, Redis frontends, session Redis, AMQP, http_cache_hosts, optional search - Extend tunnel registry with tunnel_type, service_key, service metadata; provisionServiceTunnel without store/NGINX map; teardown guards - Admin: Type (Website|Service), service dropdown, list Type column; JS toggle - TunnelManager/start-server: forward to resolved host:port - Tests: EnvServicePortResolver + service teardown; test stubs for Magento/PSR - docs: admin-ui Type/Service and security notes
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Model;
|
||||
|
||||
use Magento\Framework\App\DeploymentConfig;
|
||||
|
||||
/**
|
||||
* Discovers TCP endpoints from app/etc/env.php (via DeploymentConfig) for service-type Holesail tunnels.
|
||||
*/
|
||||
class EnvServicePortResolver
|
||||
{
|
||||
private const MYSQL_DEFAULT_KEY = 'mysql_default';
|
||||
|
||||
private const SESSION_REDIS_KEY = 'session_redis';
|
||||
|
||||
private const QUEUE_AMQP_KEY = 'queue_amqp';
|
||||
|
||||
public function __construct(
|
||||
private readonly DeploymentConfig $deploymentConfig
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
public function getDiscoveredServices(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($this->discoverMysql() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
foreach ($this->discoverCacheFrontends() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
foreach ($this->discoverSessionRedis() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
foreach ($this->discoverAmqp() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
foreach ($this->discoverHttpCacheHosts() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
foreach ($this->discoverCatalogSearch() as $row) {
|
||||
$out[] = $row;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{host: string, port: int}|null
|
||||
*/
|
||||
public function resolveService(string $serviceKey): ?array
|
||||
{
|
||||
foreach ($this->getDiscoveredServices() as $row) {
|
||||
if ($row['service_key'] === $serviceKey) {
|
||||
return ['host' => $row['host'], 'port' => $row['port']];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverMysql(): array
|
||||
{
|
||||
$conn = $this->deploymentConfig->get('db/connection/default');
|
||||
if (!\is_array($conn)) {
|
||||
return [];
|
||||
}
|
||||
$host = trim((string) ($conn['host'] ?? ''));
|
||||
if ($host === '') {
|
||||
return [];
|
||||
}
|
||||
$port = $this->parsePort($conn['port'] ?? null) ?? 3306;
|
||||
|
||||
return [[
|
||||
'service_key' => self::MYSQL_DEFAULT_KEY,
|
||||
'label' => sprintf('MySQL (db/connection/default) — %s:%d', $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverCacheFrontends(): array
|
||||
{
|
||||
$cache = $this->deploymentConfig->get('cache');
|
||||
if (!\is_array($cache)) {
|
||||
return [];
|
||||
}
|
||||
$frontends = $cache['frontend'] ?? null;
|
||||
if (!\is_array($frontends)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($frontends as $name => $frontend) {
|
||||
if (!\is_array($frontend)) {
|
||||
continue;
|
||||
}
|
||||
$backend = (string) ($frontend['backend'] ?? '');
|
||||
if ($backend === '' || stripos($backend, 'Redis') === false) {
|
||||
continue;
|
||||
}
|
||||
$opts = $frontend['backend_options'] ?? null;
|
||||
if (!\is_array($opts)) {
|
||||
continue;
|
||||
}
|
||||
$host = trim((string) ($opts['server'] ?? ''));
|
||||
$port = $this->parsePort($opts['port'] ?? null);
|
||||
if ($host === '' || $port === null) {
|
||||
continue;
|
||||
}
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', (string) $name) ?: 'frontend';
|
||||
$key = 'cache_frontend_' . $safeName;
|
||||
$out[] = [
|
||||
'service_key' => $key,
|
||||
'label' => sprintf('Redis cache (%s) — %s:%d', (string) $name, $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverSessionRedis(): array
|
||||
{
|
||||
$session = $this->deploymentConfig->get('session');
|
||||
if (!\is_array($session)) {
|
||||
return [];
|
||||
}
|
||||
if (strtolower((string) ($session['save'] ?? '')) !== 'redis') {
|
||||
return [];
|
||||
}
|
||||
$redis = $session['redis'] ?? null;
|
||||
if (!\is_array($redis)) {
|
||||
return [];
|
||||
}
|
||||
$host = trim((string) ($redis['host'] ?? ''));
|
||||
$port = $this->parsePort($redis['port'] ?? null);
|
||||
if ($host === '' || $port === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'service_key' => self::SESSION_REDIS_KEY,
|
||||
'label' => sprintf('Redis (sessions) — %s:%d', $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverAmqp(): array
|
||||
{
|
||||
$queue = $this->deploymentConfig->get('queue');
|
||||
if (!\is_array($queue)) {
|
||||
return [];
|
||||
}
|
||||
$amqp = $queue['amqp'] ?? null;
|
||||
if (!\is_array($amqp)) {
|
||||
return [];
|
||||
}
|
||||
$host = trim((string) ($amqp['host'] ?? ''));
|
||||
$port = $this->parsePort($amqp['port'] ?? null);
|
||||
if ($host === '' || $port === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'service_key' => self::QUEUE_AMQP_KEY,
|
||||
'label' => sprintf('RabbitMQ (AMQP) — %s:%d', $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverHttpCacheHosts(): array
|
||||
{
|
||||
$hosts = $this->deploymentConfig->get('http_cache_hosts');
|
||||
if (!\is_array($hosts)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($hosts as $i => $entry) {
|
||||
if (!\is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
$host = trim((string) ($entry['host'] ?? ''));
|
||||
$port = $this->parsePort($entry['port'] ?? null);
|
||||
if ($host === '' || $port === null) {
|
||||
continue;
|
||||
}
|
||||
$key = 'http_cache_' . $i;
|
||||
$out[] = [
|
||||
'service_key' => $key,
|
||||
'label' => sprintf('HTTP cache host — %s:%d', $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch / OpenSearch from catalog/search in env when present.
|
||||
*
|
||||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||||
*/
|
||||
private function discoverCatalogSearch(): array
|
||||
{
|
||||
$catalog = $this->deploymentConfig->get('catalog');
|
||||
if (!\is_array($catalog)) {
|
||||
return [];
|
||||
}
|
||||
$search = $catalog['search'] ?? null;
|
||||
if (!\is_array($search)) {
|
||||
return [];
|
||||
}
|
||||
$candidates = [
|
||||
[
|
||||
'key' => 'catalog_search_elasticsearch7',
|
||||
'host' => 'elasticsearch7_server_hostname',
|
||||
'port' => 'elasticsearch7_server_port',
|
||||
'label' => 'Elasticsearch 7',
|
||||
],
|
||||
[
|
||||
'key' => 'catalog_search_elasticsearch8',
|
||||
'host' => 'elasticsearch8_server_hostname',
|
||||
'port' => 'elasticsearch8_server_port',
|
||||
'label' => 'Elasticsearch 8',
|
||||
],
|
||||
[
|
||||
'key' => 'catalog_search_opensearch',
|
||||
'host' => 'opensearch_server_hostname',
|
||||
'port' => 'opensearch_server_port',
|
||||
'label' => 'OpenSearch',
|
||||
],
|
||||
];
|
||||
$out = [];
|
||||
foreach ($candidates as $c) {
|
||||
$host = trim((string) ($search[$c['host']] ?? ''));
|
||||
$port = $this->parsePort($search[$c['port']] ?? null);
|
||||
if ($host === '' || $port === null) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'service_key' => $c['key'],
|
||||
'label' => sprintf('%s — %s:%d', $c['label'], $host, $port),
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function parsePort(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
$n = (int) $value;
|
||||
|
||||
return ($n >= 1 && $n <= 65535) ? $n : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user