Skip to content

Commit 3a04bee

Browse files
maxbronnikov10maxbronnikov10
andauthored
feat: Add ability for nat mapping through function (#1948)
Co-authored-by: maxbronnikov10 <[email protected]>
1 parent 2f9843d commit 3a04bee

File tree

6 files changed

+138
-14
lines changed

6 files changed

+138
-14
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,31 @@ const cluster = new Redis.Cluster(
11381138
);
11391139
```
11401140

1141+
Or you can specify this parameter through function:
1142+
```javascript
1143+
const cluster = new Redis.Cluster(
1144+
[
1145+
{
1146+
host: "203.0.113.73",
1147+
port: 30001,
1148+
},
1149+
],
1150+
{
1151+
natMap: (key) => {
1152+
if(key.indexOf('30001')) {
1153+
return { host: "203.0.113.73", port: 30001 };
1154+
}
1155+
1156+
return null;
1157+
},
1158+
}
1159+
);
1160+
```
1161+
11411162
This option is also useful when the cluster is running inside a Docker container.
1163+
Also it works for Clusters in cloud infrastructure where cluster nodes connected through dedicated subnet.
1164+
1165+
Specifying through may be useful if you don't know concrete internal host and know only node port.
11421166

11431167
### Transaction and Pipeline in Cluster Mode
11441168

lib/cluster/ClusterOptions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ export type DNSLookupFunction = (
1919
family?: number
2020
) => void
2121
) => void;
22-
export interface NatMap {
22+
23+
export type NatMapFunction = (key: string) => { host: string; port: number } | null;
24+
export type NatMap = {
2325
[key: string]: { host: string; port: number };
24-
}
26+
} | NatMapFunction
2527

2628
/**
2729
* Options for Cluster constructor

lib/cluster/index.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -791,17 +791,23 @@ class Cluster extends Commander {
791791
}
792792

793793
private natMapper(nodeKey: NodeKey | RedisOptions): RedisOptions {
794-
if (this.options.natMap && typeof this.options.natMap === "object") {
795-
const key =
796-
typeof nodeKey === "string"
797-
? nodeKey
798-
: `${nodeKey.host}:${nodeKey.port}`;
799-
const mapped = this.options.natMap[key];
800-
if (mapped) {
801-
debug("NAT mapping %s -> %O", key, mapped);
802-
return Object.assign({}, mapped);
803-
}
794+
const key =
795+
typeof nodeKey === "string"
796+
? nodeKey
797+
: `${nodeKey.host}:${nodeKey.port}`;
798+
799+
let mapped = null;
800+
if (this.options.natMap && typeof this.options.natMap === "function") {
801+
mapped = this.options.natMap(key);
802+
} else if (this.options.natMap && typeof this.options.natMap === "object") {
803+
mapped = this.options.natMap[key];
804+
}
805+
806+
if (mapped) {
807+
debug("NAT mapping %s -> %O", key, mapped);
808+
return Object.assign({}, mapped);
804809
}
810+
805811
return typeof nodeKey === "string"
806812
? nodeKeyToRedisOptions(nodeKey)
807813
: nodeKey;

lib/connectors/SentinelConnector/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,16 @@ export default class SentinelConnector extends AbstractConnector {
282282
private sentinelNatResolve(item: SentinelAddress | null) {
283283
if (!item || !this.options.natMap) return item;
284284

285-
return this.options.natMap[`${item.host}:${item.port}`] || item;
285+
const key = `${item.host}:${item.port}`;
286+
287+
let result = item;
288+
if(typeof this.options.natMap === "function") {
289+
result = this.options.natMap(key) || item;
290+
} else if (typeof this.options.natMap === "object") {
291+
result = this.options.natMap[key] || item;
292+
}
293+
294+
return result;
286295
}
287296

288297
private connectToSentinel(

test/functional/cluster/nat.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Cluster } from "../../../lib";
55
import * as sinon from "sinon";
66

77
describe("NAT", () => {
8-
it("works for normal case", (done) => {
8+
it("works for normal case with object", (done) => {
99
const slotTable = [
1010
[0, 1, ["192.168.1.1", 30001]],
1111
[2, 16383, ["192.168.1.2", 30001]],
@@ -42,6 +42,48 @@ describe("NAT", () => {
4242
cluster.get("foo");
4343
});
4444

45+
it("works for normal case with function", (done) => {
46+
const slotTable = [
47+
[0, 1, ["192.168.1.1", 30001]],
48+
[2, 16383, ["192.168.1.2", 30001]],
49+
];
50+
51+
let cluster;
52+
new MockServer(30001, null, slotTable);
53+
new MockServer(
54+
30002,
55+
([command, arg]) => {
56+
if (command === "get" && arg === "foo") {
57+
cluster.disconnect();
58+
done();
59+
}
60+
},
61+
slotTable
62+
);
63+
64+
cluster = new Cluster(
65+
[
66+
{
67+
host: "127.0.0.1",
68+
port: 30001,
69+
},
70+
],
71+
{
72+
natMap: (key) => {
73+
if(key === "192.168.1.1:30001") {
74+
return { host: "127.0.0.1", port: 30001 };
75+
}
76+
if(key === "192.168.1.2:30001") {
77+
return { host: "127.0.0.1", port: 30002 };
78+
}
79+
return null;
80+
}
81+
}
82+
);
83+
84+
cluster.get("foo");
85+
});
86+
4587
it("works if natMap does not match all the cases", (done) => {
4688
const slotTable = [
4789
[0, 1, ["192.168.1.1", 30001]],

test/unit/clusters/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,47 @@ describe("cluster", () => {
5656
}).to.throw(/Invalid role/);
5757
});
5858
});
59+
60+
61+
describe("natMapper", () => {
62+
it("returns the original nodeKey if no NAT mapping is provided", () => {
63+
const cluster = new Cluster([]);
64+
const nodeKey = { host: "127.0.0.1", port: 6379 };
65+
const result = cluster["natMapper"](nodeKey);
66+
67+
expect(result).to.eql(nodeKey);
68+
});
69+
70+
it("maps external IP to internal IP using NAT mapping object", () => {
71+
const natMap = { "203.0.113.1:6379": { host: "127.0.0.1", port: 30000 } };
72+
const cluster = new Cluster([], { natMap });
73+
const nodeKey = "203.0.113.1:6379";
74+
const result = cluster["natMapper"](nodeKey);
75+
expect(result).to.eql({ host: "127.0.0.1", port: 30000 });
76+
});
77+
78+
it("maps external IP to internal IP using NAT mapping function", () => {
79+
const natMap = (key) => {
80+
if (key === "203.0.113.1:6379") {
81+
return { host: "127.0.0.1", port: 30000 };
82+
}
83+
return null;
84+
};
85+
const cluster = new Cluster([], { natMap });
86+
const nodeKey = "203.0.113.1:6379";
87+
const result = cluster["natMapper"](nodeKey);
88+
expect(result).to.eql({ host: "127.0.0.1", port: 30000 });
89+
});
90+
91+
it("returns the original nodeKey if NAT mapping is invalid", () => {
92+
const natMap = { "invalid:key": { host: "127.0.0.1", port: 30000 } };
93+
const cluster = new Cluster([], { natMap });
94+
const nodeKey = "203.0.113.1:6379";
95+
const result = cluster["natMapper"](nodeKey);
96+
expect(result).to.eql({ host: "203.0.113.1", port: 6379 });
97+
});
98+
});
99+
59100
});
60101

61102
describe("nodeKeyToRedisOptions()", () => {

0 commit comments

Comments
 (0)