mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
SYNC: Merge pull request #15 from dbgate/feature/redis-cluster
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
import { getConnectionLabel } from 'dbgate-tools';
|
import { getConnectionLabel } from 'dbgate-tools';
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
import FormFileInputField from '../forms/FormFileInputField.svelte';
|
import FormFileInputField from '../forms/FormFileInputField.svelte';
|
||||||
|
import FormClusterNodesField from '../forms/FormClusterNodesField.svelte';
|
||||||
|
|
||||||
export let getDatabaseList;
|
export let getDatabaseList;
|
||||||
export let currentConnection;
|
export let currentConnection;
|
||||||
@@ -122,6 +123,24 @@
|
|||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if driver?.showConnectionField('clusterNodes', $values, showConnectionFieldArgs)}
|
||||||
|
<FormClusterNodesField
|
||||||
|
label="Cluster nodes"
|
||||||
|
name="clusterNodes"
|
||||||
|
disabled={isConnected || disabledFields.includes('clusterNodes')}
|
||||||
|
data-testid="ConnectionDriverFields_clusterNodes"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if driver?.showConnectionField('autoDetectNatMap', $values, showConnectionFieldArgs)}
|
||||||
|
<FormCheckboxField
|
||||||
|
label="Auto detect NAT map (use for Redis Cluster in Docker network)"
|
||||||
|
name="autoDetectNatMap"
|
||||||
|
disabled={isConnected}
|
||||||
|
data-testid="ConnectionDriverFields_autoDetectNatMap"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if driver?.showConnectionField('databaseFile', $values, showConnectionFieldArgs)}
|
{#if driver?.showConnectionField('databaseFile', $values, showConnectionFieldArgs)}
|
||||||
{#if electron && !driver?.dialect?.useServerDatabaseFile}
|
{#if electron && !driver?.dialect?.useServerDatabaseFile}
|
||||||
<FormElectronFileSelector
|
<FormElectronFileSelector
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const Redis = require('ioredis');
|
|||||||
const RedisDump = require('node-redis-dump2');
|
const RedisDump = require('node-redis-dump2');
|
||||||
const { filterName } = global.DBGATE_PACKAGES['dbgate-tools'];
|
const { filterName } = global.DBGATE_PACKAGES['dbgate-tools'];
|
||||||
|
|
||||||
|
let isProApp;
|
||||||
|
|
||||||
function splitCommandLine(str) {
|
function splitCommandLine(str) {
|
||||||
let results = [];
|
let results = [];
|
||||||
let word = '';
|
let word = '';
|
||||||
@@ -77,6 +79,47 @@ function splitCommandLine(str) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildNatMapFromSeeds(seeds, redisOptions) {
|
||||||
|
const natMap = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
seeds.map(async (seed) => {
|
||||||
|
const single = new Redis({
|
||||||
|
host: typeof seed === 'string' ? new URL(seed).hostname : seed.host,
|
||||||
|
port: typeof seed === 'string' ? Number(new URL(seed).port || 6379) : seed.port,
|
||||||
|
...redisOptions,
|
||||||
|
// Make these connections quick and disposable
|
||||||
|
lazyConnect: false,
|
||||||
|
enableReadyCheck: false,
|
||||||
|
maxRetriesPerRequest: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodes = await single.cluster('nodes'); // text blob
|
||||||
|
const line = nodes.split(/\r?\n/).find((l) => /\bmyself\b/.test(l));
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
// Example addr token: "172.18.0.3:6379@16379" or "redis-node-0:6379@16379"
|
||||||
|
const addrToken = line.split(' ')[1];
|
||||||
|
const hostPort = addrToken.split('@')[0]; // strip bus port
|
||||||
|
const [advHost, advPortStr] = hostPort.split(':');
|
||||||
|
const advKey = `${advHost}:${Number(advPortStr)}`;
|
||||||
|
|
||||||
|
const extHost = typeof seed === 'string' ? new URL(seed).hostname : seed.host;
|
||||||
|
const extPort = typeof seed === 'string' ? Number(new URL(seed).port || 6379) : seed.port;
|
||||||
|
|
||||||
|
natMap[advKey] = { host: extHost, port: extPort };
|
||||||
|
} catch {
|
||||||
|
// ignore this seed if it fails; others may still succeed
|
||||||
|
} finally {
|
||||||
|
single.disconnect();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return natMap;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('dbgate-types').EngineDriver} */
|
/** @type {import('dbgate-types').EngineDriver} */
|
||||||
const driver = {
|
const driver = {
|
||||||
...driverBase,
|
...driverBase,
|
||||||
@@ -92,6 +135,9 @@ const driver = {
|
|||||||
treeKeySeparator,
|
treeKeySeparator,
|
||||||
ssl,
|
ssl,
|
||||||
skipSetName,
|
skipSetName,
|
||||||
|
authType,
|
||||||
|
clusterNodes,
|
||||||
|
autoDetectNatMap,
|
||||||
}) {
|
}) {
|
||||||
let db = 0;
|
let db = 0;
|
||||||
let client;
|
let client;
|
||||||
@@ -100,6 +146,26 @@ const driver = {
|
|||||||
if (!skipSetName) {
|
if (!skipSetName) {
|
||||||
await client.client('SETNAME', 'dbgate');
|
await client.client('SETNAME', 'dbgate');
|
||||||
}
|
}
|
||||||
|
} else if (authType === 'cluster' && isProApp && isProApp()) {
|
||||||
|
const redisOptions = {
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
let seeds = [];
|
||||||
|
try {
|
||||||
|
seeds = JSON.parse(clusterNodes);
|
||||||
|
} catch {}
|
||||||
|
if (!Array.isArray(seeds) || seeds.length === 0) {
|
||||||
|
throw new Error('Cluster nodes must be a non-empty array of host:port or objects with host and port');
|
||||||
|
}
|
||||||
|
|
||||||
|
const natMap = autoDetectNatMap ? await buildNatMapFromSeeds(seeds, redisOptions) : undefined;
|
||||||
|
|
||||||
|
client = new Redis.Cluster(seeds, {
|
||||||
|
redisOptions,
|
||||||
|
natMap,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2));
|
if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2));
|
||||||
if (_.isNumber(database)) db = database;
|
if (_.isNumber(database)) db = database;
|
||||||
@@ -487,6 +553,20 @@ const driver = {
|
|||||||
async close(dbhan) {
|
async close(dbhan) {
|
||||||
return dbhan.client.quit();
|
return dbhan.client.quit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAuthTypes() {
|
||||||
|
if (isProApp && isProApp()) {
|
||||||
|
return [
|
||||||
|
{ name: 'node', title: 'Single Redis node' },
|
||||||
|
{ name: 'cluster', title: 'Redis Cluster' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
driver.initialize = (dbgateEnv) => {
|
||||||
|
isProApp = dbgateEnv.isProApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = driver;
|
module.exports = driver;
|
||||||
|
|||||||
@@ -3,4 +3,7 @@ const driver = require('./driver');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
packageName: 'dbgate-plugin-redis',
|
packageName: 'dbgate-plugin-redis',
|
||||||
drivers: [driver],
|
drivers: [driver],
|
||||||
|
initialize(dbgateEnv) {
|
||||||
|
driver.initialize(dbgateEnv);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ const driver = {
|
|||||||
title: 'Redis',
|
title: 'Redis',
|
||||||
defaultPort: 6379,
|
defaultPort: 6379,
|
||||||
editorMode: 'text',
|
editorMode: 'text',
|
||||||
|
authTypeFirst: true,
|
||||||
databaseEngineTypes: ['keyvalue'],
|
databaseEngineTypes: ['keyvalue'],
|
||||||
supportedCreateDatabase: false,
|
supportedCreateDatabase: false,
|
||||||
getQuerySplitterOptions: () => redisSplitterOptions,
|
getQuerySplitterOptions: () => redisSplitterOptions,
|
||||||
databaseUrlPlaceholder: 'e.g. redis://:authpassword@127.0.0.1:6380/4',
|
databaseUrlPlaceholder: 'e.g. redis://:authpassword@127.0.0.1:6380/4',
|
||||||
|
authTypeLabel: 'Connection mode',
|
||||||
|
defaultAuthTypeName: 'node',
|
||||||
supportedKeyTypes: [
|
supportedKeyTypes: [
|
||||||
{
|
{
|
||||||
name: 'string',
|
name: 'string',
|
||||||
@@ -76,10 +79,14 @@ const driver = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
showConnectionField: (field, values) => {
|
showConnectionField: (field, values) => {
|
||||||
if (field == 'useDatabaseUrl') return true;
|
if (field == 'useDatabaseUrl') return values.authType != 'cluster';
|
||||||
|
if (field == 'authType') return !values.useDatabaseUrl;
|
||||||
if (values.useDatabaseUrl) {
|
if (values.useDatabaseUrl) {
|
||||||
return ['databaseUrl', 'isReadOnly', 'treeKeySeparator'].includes(field);
|
return ['databaseUrl', 'isReadOnly', 'treeKeySeparator'].includes(field);
|
||||||
}
|
}
|
||||||
|
if (values.authType == 'cluster') {
|
||||||
|
return ['user', 'password', 'isReadOnly', 'treeKeySeparator', 'clusterNodes', 'autoDetectNatMap'].includes(field);
|
||||||
|
}
|
||||||
return ['server', 'port', 'user', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field);
|
return ['server', 'port', 'user', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user