Files
MageSail/Magesail/Block/Adminhtml/Tunnel.php
T
snxraven 960db9801c
CI / php (push) Successful in 2m53s
feat(tunnels): add Service type with dynamic env.php endpoint discovery
- 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
2026-03-21 04:38:37 -05:00

302 lines
9.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace MageSail\Magesail\Block\Adminhtml;
use Magento\Backend\Block\Template;
use Magento\Backend\Block\Template\Context;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\Model\StoreManagerInterface;
use MageSail\Magesail\Model\EnvServicePortResolver;
use MageSail\Magesail\Model\TunnelRegistry;
use MageSail\Magesail\Model\TunnelStatus;
use MageSail\Magesail\Model\TunnelStoreProvisioner;
class Tunnel extends Template
{
private const LOG_TAIL_BYTES = 8192;
private const LOG_TAIL_LINES = 40;
private const XML_MODE = 'magesail/settings/mode';
public function __construct(
Context $context,
private readonly TunnelStatus $tunnelStatus,
private readonly TunnelStoreProvisioner $tunnelStoreProvisioner,
private readonly StoreManagerInterface $storeManager,
private readonly ScopeConfigInterface $scopeConfig,
private readonly TunnelRegistry $tunnelRegistry,
private readonly EnvServicePortResolver $envServicePortResolver,
private readonly Json $json,
array $data = []
) {
parent::__construct($context, $data);
}
/**
* @return list<array{tunnel_id: string, label: string, store_code: string, local_port: int, running: bool, pid: int|null, key: string|null}>
*/
public function getAllTunnelStatuses(): array
{
return $this->tunnelStatus->getAllTunnelStatuses();
}
/**
* @return list<array<string, mixed>>
*/
public function listTunnelsFromRegistry(): array
{
return $this->tunnelRegistry->listTunnels();
}
public function hasAnyNginxPendingReload(): bool
{
return $this->tunnelRegistry->anyTunnelHasNginxPendingReload();
}
/**
* Preset ports shown first in Admin (443 is typical for Holesail + HTTPS upstream).
*
* @return list<int>
*/
public function getLocalPortPresetValues(): array
{
return [443, 80, 8080];
}
/**
* Initial Add-tunnel UI: preset key '443'|'80'|'8080'|'custom' and optional custom port when not a preset.
*
* @return array{preset: string, custom_port: int}
*/
public function getInitialLocalPortUiState(): array
{
$presets = $this->getLocalPortPresetValues();
$default = 443;
foreach ($presets as $p) {
if ($p === $default) {
return ['preset' => (string) $p, 'custom_port' => $default];
}
}
return ['preset' => 'custom', 'custom_port' => $default];
}
public function getDefaultHolesailSecure(): bool
{
return (bool) $this->scopeConfig->getValue(self::XML_MODE);
}
/**
* TCP endpoints discovered from env.php (for Service tunnel type).
*
* @return list<array{service_key: string, label: string, host: string, port: int}>
*/
public function getDiscoveredServices(): array
{
return $this->envServicePortResolver->getDiscoveredServices();
}
/**
* @return list<array{value: string, label: string}>
*/
public function getWebsiteOptions(): array
{
$options = [];
foreach ($this->storeManager->getWebsites() as $website) {
$options[] = [
'value' => (string) $website->getId(),
'label' => $website->getName() . ' (' . $website->getCode() . ')',
];
}
return $options;
}
/**
* JSON map: websiteId string => list of {id, name}
*/
public function getStoreGroupsByWebsiteJson(): string
{
$map = [];
foreach ($this->storeManager->getWebsites() as $website) {
$wid = (string) $website->getId();
$map[$wid] = [];
foreach ($website->getGroups() as $group) {
$map[$wid][] = [
'id' => (string) $group->getId(),
'name' => $group->getName() . ' (' . $group->getCode() . ')',
];
}
}
return $this->json->serialize($map);
}
/**
* @deprecated
*/
public function getTunnelStoreState(): ?array
{
return $this->tunnelStoreProvisioner->getPersistedState();
}
public function getTunnelStoreCodeConstant(): string
{
return TunnelStoreProvisioner::STORE_CODE;
}
/**
* Strip scheme for display so Admin shows hostname-only; persisted URLs stay full in state.
*/
/**
* @param array<string, mixed> $row
*/
public function getTunnelRowHostDisplay(array $row): string
{
if (($row['tunnel_type'] ?? 'website') === 'service') {
$h = trim((string) ($row['service_target_host'] ?? ''));
$p = (int) ($row['local_port'] ?? 0);
if ($h !== '' && $p > 0) {
return $h . ':' . $p;
}
return (string) ($row['service_label'] ?? '');
}
return $this->getTunnelHostDisplay($row['unsecure_base_url'] ?? null);
}
public function getTunnelHostDisplay(?string $baseUrl): string
{
if ($baseUrl === null || $baseUrl === '') {
return '';
}
$parts = parse_url($baseUrl);
if ($parts === false || empty($parts['host'])) {
return $baseUrl;
}
$out = $parts['host'];
if (!empty($parts['port'])) {
$out .= ':' . $parts['port'];
}
if (!empty($parts['path']) && $parts['path'] !== '/' && $parts['path'] !== '') {
$out .= rtrim($parts['path'], '/');
}
return $out;
}
public function getStatusUrl(): string
{
return $this->getUrl('*/*/status');
}
public function getIndexUrl(): string
{
return $this->getUrl('*/*/index');
}
public function getLogtailUrl(): string
{
return $this->getUrl('*/*/logtail');
}
public function getFormKey(): string
{
return $this->formKey->getFormKey();
}
public function getLogFilePathForTunnel(string $tunnelId): string
{
return $this->tunnelStatus->getLogFilePath($tunnelId);
}
/**
* @return string[]
*/
public function getRecentLogLinesForTunnel(string $tunnelId): array
{
$path = $this->tunnelStatus->getLogFilePath($tunnelId);
if (!\is_readable($path)) {
return [];
}
$size = filesize($path);
if ($size === false || $size === 0) {
return [];
}
$fp = fopen($path, 'rb');
if ($fp === false) {
return [];
}
$start = max(0, $size - self::LOG_TAIL_BYTES);
fseek($fp, $start);
if ($start > 0) {
fgets($fp);
}
$chunk = stream_get_contents($fp) ?: '';
fclose($fp);
$lines = preg_split('/\R/', $chunk) ?: [];
$lines = array_values(array_filter($lines, static fn ($l) => $l !== ''));
if (\count($lines) > self::LOG_TAIL_LINES) {
$lines = \array_slice($lines, -self::LOG_TAIL_LINES);
}
return $lines;
}
/**
* Config for RequireJS magesail-tunnel-ui (URLs, form key, i18n, store groups).
*
* @return array<string, mixed>
*/
public function getTunnelUiInitConfig(): array
{
$groupsJson = $this->getStoreGroupsByWebsiteJson();
$groups = json_decode($groupsJson, true);
return [
'statusUrl' => $this->getStatusUrl(),
'indexUrl' => $this->getIndexUrl(),
'logtailUrl' => $this->getLogtailUrl(),
'formKey' => $this->getFormKey(),
'groupsByWebsite' => \is_array($groups) ? $groups : [],
'defaultTab' => \count($this->listTunnelsFromRegistry()) === 0 ? 'add' : 'list',
'discoveredServices' => $this->envServicePortResolver->getDiscoveredServices(),
'translations' => [
'copied' => (string) __('Copied!'),
'copyFailed' => (string) __('Copy failed'),
'copyStoreCode' => (string) __('Copy store code'),
'copyKey' => (string) __('Copy key'),
'copyCommand' => (string) __('Copy command'),
'copyLog' => (string) __('Copy log'),
'copy' => (string) __('Copy'),
'selectWebsiteFirst' => (string) __('— Select website first —'),
'selectDash' => (string) __('— Select —'),
'stopFailed' => (string) __('Stop failed'),
'removeFailed' => (string) __('Remove failed'),
'removeConfirm' => (string) __(
'Remove this tunnel, delete its store view, and update the NGINX map?'
),
'removeConfirmService' => (string) __(
'Remove this service tunnel? No store view will be deleted.'
),
'typeWebsite' => (string) __('Website'),
'typeService' => (string) __('Service'),
'tunnelType' => (string) __('Type'),
'serviceToExpose' => (string) __('Service to expose'),
'noServicesInEnv' => (string) __(
'No TCP services were found in env.php. Check db, cache, session, queue, and http_cache_hosts.'
),
'selectService' => (string) __('— Select a service —'),
'selectWebsiteGroup' => (string) __('Select website and store group.'),
'enterHostname' => (string) __('Enter the tunnel hostname.'),
'invalidPort' => (string) __('Enter a valid local port (165535).'),
'runningPidTpl' => (string) __('Running (PID %1)', 'X'),
'clientLabel' => (string) __('Client:'),
],
];
}
public function getTunnelUiInitJson(): string
{
return $this->json->serialize($this->getTunnelUiInitConfig());
}
}