生产经营分析副本库隧道端点 #8
@ -37,6 +37,19 @@ return [
|
||||
'enabled' => filter_var(env('UPSTREAM_STORED_CONFIG_ENABLED', false), FILTER_VALIDATE_BOOL),
|
||||
],
|
||||
|
||||
'tunnel_endpoints' => [
|
||||
// 仅生产环境使用的私有隧道端点。它只替换连接主机和端口,
|
||||
// 数据库名、账号和密码仍从共享系统数据库读取,不能写入此处。
|
||||
'jiansun' => [
|
||||
'host' => env('UPSTREAM_TUNNEL_JIANSUN_HOST'),
|
||||
'port' => env('UPSTREAM_TUNNEL_JIANSUN_PORT'),
|
||||
],
|
||||
'operations_replica' => [
|
||||
'host' => env('UPSTREAM_TUNNEL_OPERATIONS_REPLICA_HOST'),
|
||||
'port' => env('UPSTREAM_TUNNEL_OPERATIONS_REPLICA_PORT'),
|
||||
],
|
||||
],
|
||||
|
||||
'local_configuration' => [
|
||||
// 开发机显式开启后完全使用本机私有来源配置,不读取共享密码。
|
||||
'enabled' => filter_var(env('UPSTREAM_LOCAL_CONFIG_ENABLED', false), FILTER_VALIDATE_BOOL),
|
||||
|
||||
@ -59,10 +59,14 @@ class UpstreamSourceRegistry
|
||||
return $this->connection($definition['key'], (array) $configured);
|
||||
}
|
||||
|
||||
// Hosts and credentials come from the shared record, while the ODBC
|
||||
// driver must match the operating system that runs this application.
|
||||
if ($suffix === 'connection.odbc_driver' && trim((string) $configured) !== '') {
|
||||
return $configured;
|
||||
if (str_starts_with($suffix, 'connection.')) {
|
||||
$field = substr($suffix, strlen('connection.'));
|
||||
$connection = $this->connection(
|
||||
$definition['key'],
|
||||
(array) config($definition['config'] . '.connection', []),
|
||||
);
|
||||
|
||||
return $connection[$field] ?? $default;
|
||||
}
|
||||
|
||||
return $this->storedConfiguration->value($definition['key'], $suffix, $configured);
|
||||
@ -94,13 +98,63 @@ class UpstreamSourceRegistry
|
||||
$resolved = $configured;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$resolved[$field] = $this->value(
|
||||
// ODBC 驱动由运行应用的操作系统决定,不能因共享记录中的
|
||||
// Windows 驱动名而覆盖 Linux 容器中的可用驱动。
|
||||
if ($field === 'odbc_driver' && trim((string) ($configured[$field] ?? '')) !== '') {
|
||||
$resolved[$field] = $configured[$field];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolved[$field] = $this->storedConfiguration->value(
|
||||
$source,
|
||||
'connection.' . $field,
|
||||
$configured[$field] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
return $this->withProductionTunnelEndpoint($source, $resolved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产服务器无法直连上游时,只覆盖到受限 SSH 隧道的私有端点。
|
||||
* 共享系统数据库仍是账号、密码和业务库信息的唯一来源,开发机则
|
||||
* 保持原始上游地址直连,避免把生产 Docker 网络地址传播给开发环境。
|
||||
*/
|
||||
private function withProductionTunnelEndpoint(string $source, array $connection): array
|
||||
{
|
||||
if (
|
||||
! app()->environment('production')
|
||||
|| $this->usesLocalConfiguration()
|
||||
|| ! $this->storedConfiguration->enabled()
|
||||
) {
|
||||
return $connection;
|
||||
}
|
||||
|
||||
$endpoint = config('upstreams.tunnel_endpoints.' . $source, []);
|
||||
|
||||
if (! is_array($endpoint)) {
|
||||
return $connection;
|
||||
}
|
||||
|
||||
$host = trim((string) ($endpoint['host'] ?? ''));
|
||||
$port = $endpoint['port'] ?? null;
|
||||
|
||||
if ($host === '' && ($port === null || $port === '')) {
|
||||
return $connection;
|
||||
}
|
||||
|
||||
if (
|
||||
$host === ''
|
||||
|| preg_match('/^[A-Za-z0-9._:-]+$/', $host) !== 1
|
||||
|| filter_var($port, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 65535]]) === false
|
||||
) {
|
||||
throw new \RuntimeException('生产上游隧道端点配置不完整。');
|
||||
}
|
||||
|
||||
return array_replace($connection, [
|
||||
'host' => $host,
|
||||
'port' => (int) $port,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +227,40 @@ class UpstreamDatabasesTest extends FeatureTestCase
|
||||
$this->assertSame('ReadOnly', $resolved['application_intent']);
|
||||
}
|
||||
|
||||
public function test_production_tunnel_replaces_only_the_shared_connection_endpoint(): void
|
||||
{
|
||||
$connection = $this->connection('shared-secret');
|
||||
$connection->update([
|
||||
'source_key' => 'operations_replica',
|
||||
'host' => 'shared-db.test',
|
||||
'port' => 14330,
|
||||
'database_name' => 'shared_database',
|
||||
'username' => 'shared_reader',
|
||||
]);
|
||||
$this->app->detectEnvironment(fn () => 'production');
|
||||
|
||||
try {
|
||||
config([
|
||||
'upstreams.local_configuration.enabled' => false,
|
||||
'upstreams.stored_configuration.enabled' => true,
|
||||
'upstreams.tunnel_endpoints.operations_replica' => [
|
||||
'host' => 'private-tunnel.test',
|
||||
'port' => 15433,
|
||||
],
|
||||
]);
|
||||
$sources = new UpstreamSourceRegistry(new UpstreamStoredConfiguration());
|
||||
$resolved = $sources->value('operations_replica', 'connection', []);
|
||||
|
||||
$this->assertSame('private-tunnel.test', $resolved['host']);
|
||||
$this->assertSame(15433, $resolved['port']);
|
||||
$this->assertSame('shared_database', $resolved['database']);
|
||||
$this->assertSame('shared_reader', $resolved['username']);
|
||||
$this->assertSame('shared-secret', $resolved['password']);
|
||||
} finally {
|
||||
$this->app->detectEnvironment(fn () => 'testing');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_global_wildcard_scope_is_rejected(): void
|
||||
{
|
||||
$this->withExceptionHandling();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user