@sysnee/pgs 0.1.7-rc.2 → 0.1.7-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-compose.yml +9 -6
- package/manager.js +108 -94
- package/package.json +1 -1
- package/traefik.yml +15 -0
- package/haproxy-lua/pg-route.lua +0 -177
- package/haproxy.cfg +0 -35
package/docker-compose.yml
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
version: "3.8"
|
|
2
|
+
|
|
1
3
|
services:
|
|
2
|
-
|
|
3
|
-
image:
|
|
4
|
+
traefik:
|
|
5
|
+
image: traefik:v3.0
|
|
4
6
|
container_name: postgres_proxy
|
|
5
7
|
ports:
|
|
6
|
-
-
|
|
8
|
+
- "5432:5432"
|
|
7
9
|
volumes:
|
|
8
|
-
- ./
|
|
9
|
-
- ./
|
|
10
|
-
- ./
|
|
10
|
+
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
|
11
|
+
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
12
|
+
- ./certs:/etc/traefik/certs:ro
|
|
11
13
|
networks:
|
|
12
14
|
- postgres_network
|
|
13
15
|
restart: unless-stopped
|
|
16
|
+
|
|
14
17
|
networks:
|
|
15
18
|
postgres_network:
|
|
16
19
|
driver: bridge
|
package/manager.js
CHANGED
|
@@ -19,8 +19,9 @@ const CONFIG_DIR = path.join(os.homedir(), '.sysnee-config');
|
|
|
19
19
|
|
|
20
20
|
const COMPOSE_FILE_PATH = path.join(CONFIG_DIR, 'docker-compose.yml');
|
|
21
21
|
const INIT_DIR_PATH = path.join(CONFIG_DIR, 'init');
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const CERTS_DIR_PATH = path.join(CONFIG_DIR, 'certs');
|
|
23
|
+
const TRAEFIK_STATIC_PATH = path.join(CONFIG_DIR, 'traefik.yml');
|
|
24
|
+
const TRAEFIK_DYNAMIC_PATH = path.join(CONFIG_DIR, 'dynamic.yml');
|
|
24
25
|
const TENANT_ACCESS_FILE_PATH = path.join(CONFIG_DIR, 'tenant-access.json');
|
|
25
26
|
const SETUP_STATUS_PATH = path.join(CONFIG_DIR, 'status.txt');
|
|
26
27
|
|
|
@@ -59,86 +60,93 @@ function removeTenantAccess(tenantId) {
|
|
|
59
60
|
saveTenantAccess(access);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// ====================
|
|
63
|
+
// ==================== Traefik Configuration ====================
|
|
63
64
|
|
|
64
|
-
function
|
|
65
|
-
console.debug('
|
|
65
|
+
function generateTraefikDynamicConfig() {
|
|
66
|
+
console.debug('Generating Traefik dynamic config')
|
|
66
67
|
const doc = loadCompose();
|
|
68
|
+
const access = loadTenantAccess();
|
|
67
69
|
|
|
68
|
-
// Get all tenant services
|
|
69
70
|
const tenants = Object.keys(doc.services || {})
|
|
70
|
-
.filter(name => name.startsWith('pgs_') && name !== '
|
|
71
|
-
|
|
72
|
-
console.debug(`tenants.lenght: ${tenants.length}`)
|
|
71
|
+
.filter(name => name.startsWith('pgs_') && name !== 'traefik');
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
const templateFilePath = path.join(__dirname, 'haproxy.cfg')
|
|
76
|
-
console.debug(`haproxy template file path: ${templateFilePath}`)
|
|
77
|
-
let config = readFileSync(templateFilePath, 'utf8');
|
|
78
|
-
console.debug(`haproxy template file loaded`)
|
|
73
|
+
console.debug(`tenants.length: ${tenants.length}`)
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const dynamicConfig = {
|
|
76
|
+
tcp: {
|
|
77
|
+
routers: {},
|
|
78
|
+
services: {}
|
|
79
|
+
},
|
|
80
|
+
tls: {
|
|
81
|
+
certificates: [
|
|
82
|
+
{
|
|
83
|
+
certFile: '/etc/traefik/certs/fullchain.pem',
|
|
84
|
+
keyFile: '/etc/traefik/certs/privkey.pem'
|
|
85
|
+
}
|
|
86
|
+
]
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
`;
|
|
89
|
-
} else {
|
|
90
|
-
config += ` # No tenants configured yet
|
|
91
|
-
`;
|
|
92
|
-
}
|
|
88
|
+
};
|
|
93
89
|
|
|
94
|
-
// Generate backend for each tenant
|
|
95
90
|
for (const serviceName of tenants) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
const tenantId = serviceName.replace('pgs_', '');
|
|
92
|
+
|
|
93
|
+
if (access[tenantId] !== true) {
|
|
94
|
+
console.debug(`Skipping disabled tenant: ${tenantId}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const routerName = `router_${tenantId}`.replace(/-/g, '_');
|
|
99
|
+
const svcName = `svc_${tenantId}`.replace(/-/g, '_');
|
|
100
|
+
|
|
101
|
+
dynamicConfig.tcp.routers[routerName] = {
|
|
102
|
+
entryPoints: ['postgres'],
|
|
103
|
+
rule: `HostSNI(\`${tenantId}.pgs.us-central-1.sysnee.com\`)`,
|
|
104
|
+
service: svcName,
|
|
105
|
+
tls: {}
|
|
106
|
+
};
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
dynamicConfig.tcp.services[svcName] = {
|
|
109
|
+
loadBalancer: {
|
|
110
|
+
servers: [
|
|
111
|
+
{ address: `${serviceName}:5432` }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
};
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.log(`
|
|
117
|
+
const yamlContent = yaml.dump(dynamicConfig, { indent: 2, lineWidth: -1, quotingType: '"' });
|
|
118
|
+
|
|
119
|
+
console.log(`Saving Traefik dynamic config to ${TRAEFIK_DYNAMIC_PATH}`)
|
|
120
|
+
writeFileSync(TRAEFIK_DYNAMIC_PATH, yamlContent);
|
|
121
|
+
console.log('Traefik dynamic config saved successfully')
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
function
|
|
124
|
+
function updateTraefikService() {
|
|
109
125
|
const doc = loadCompose();
|
|
110
126
|
|
|
111
|
-
// Get all tenant services
|
|
112
127
|
const tenants = Object.keys(doc.services || {})
|
|
113
|
-
.filter(name => name.startsWith('pgs_') && name !== '
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
restart: 'unless-stopped'
|
|
128
|
-
};
|
|
129
|
-
}
|
|
128
|
+
.filter(name => name.startsWith('pgs_') && name !== 'traefik');
|
|
129
|
+
|
|
130
|
+
doc.services.traefik = {
|
|
131
|
+
image: 'traefik:v3.0',
|
|
132
|
+
container_name: 'postgres_proxy',
|
|
133
|
+
ports: ['5432:5432'],
|
|
134
|
+
volumes: [
|
|
135
|
+
`${TRAEFIK_STATIC_PATH}:/etc/traefik/traefik.yml:ro`,
|
|
136
|
+
`${TRAEFIK_DYNAMIC_PATH}:/etc/traefik/dynamic.yml:ro`,
|
|
137
|
+
`${CERTS_DIR_PATH}:/etc/traefik/certs:ro`
|
|
138
|
+
],
|
|
139
|
+
networks: ['postgres_network'],
|
|
140
|
+
restart: 'unless-stopped'
|
|
141
|
+
};
|
|
130
142
|
|
|
131
|
-
// Update depends_on
|
|
132
143
|
if (tenants.length > 0) {
|
|
133
|
-
doc.services.
|
|
134
|
-
} else {
|
|
135
|
-
delete doc.services.haproxy.depends_on;
|
|
144
|
+
doc.services.traefik.depends_on = tenants;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
saveCompose(doc);
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
executeCommand('docker compose up -d haproxy');
|
|
149
|
+
executeCommand('docker compose up -d --force-recreate traefik');
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
function createInitScript({ tenantId, password, databaseName }) {
|
|
@@ -228,21 +236,33 @@ function createInitialFiles() {
|
|
|
228
236
|
writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
|
|
229
237
|
}
|
|
230
238
|
|
|
231
|
-
//
|
|
232
|
-
if (!existsSync(
|
|
233
|
-
const
|
|
234
|
-
writeFileSync(
|
|
239
|
+
// Traefik static config
|
|
240
|
+
if (!existsSync(TRAEFIK_STATIC_PATH)) {
|
|
241
|
+
const traefikTemplate = readFileSync(path.join(__dirname, 'traefik.yml'), 'utf8');
|
|
242
|
+
writeFileSync(TRAEFIK_STATIC_PATH, traefikTemplate);
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
//
|
|
238
|
-
if (!existsSync(
|
|
239
|
-
|
|
245
|
+
// Traefik dynamic config (empty initially)
|
|
246
|
+
if (!existsSync(TRAEFIK_DYNAMIC_PATH)) {
|
|
247
|
+
const emptyDynamic = yaml.dump({
|
|
248
|
+
tcp: { routers: {}, services: {} },
|
|
249
|
+
tls: {
|
|
250
|
+
certificates: [{
|
|
251
|
+
certFile: '/etc/traefik/certs/fullchain.pem',
|
|
252
|
+
keyFile: '/etc/traefik/certs/privkey.pem'
|
|
253
|
+
}]
|
|
254
|
+
}
|
|
255
|
+
}, { indent: 2 });
|
|
256
|
+
writeFileSync(TRAEFIK_DYNAMIC_PATH, emptyDynamic);
|
|
240
257
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!existsSync(
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
|
|
259
|
+
// Certs directory
|
|
260
|
+
if (!existsSync(CERTS_DIR_PATH)) {
|
|
261
|
+
mkdirSync(CERTS_DIR_PATH, { recursive: true });
|
|
262
|
+
console.log(`Created certs directory at ${CERTS_DIR_PATH}`);
|
|
263
|
+
console.log('Place your SSL certificate files there:');
|
|
264
|
+
console.log(' - fullchain.pem (certificate chain)');
|
|
265
|
+
console.log(' - privkey.pem (private key)');
|
|
246
266
|
}
|
|
247
267
|
}
|
|
248
268
|
|
|
@@ -291,21 +311,23 @@ function createTenant(tenantId, options = {}) {
|
|
|
291
311
|
|
|
292
312
|
saveCompose(doc);
|
|
293
313
|
|
|
294
|
-
// Add tenant to access control
|
|
314
|
+
// Add tenant to access control
|
|
295
315
|
setTenantAccess(tenantId, true);
|
|
296
316
|
|
|
297
|
-
// Regenerate
|
|
298
|
-
|
|
299
|
-
|
|
317
|
+
// Regenerate Traefik config and update service
|
|
318
|
+
generateTraefikDynamicConfig();
|
|
319
|
+
updateTraefikService();
|
|
300
320
|
|
|
301
321
|
// start the tenant
|
|
302
322
|
executeCommand(`docker compose up -d ${serviceName}`.trim());
|
|
303
323
|
|
|
304
324
|
console.log(`✓ Created tenant: ${tenantId}`);
|
|
305
325
|
console.log(` Service: ${serviceName}`);
|
|
326
|
+
console.log(` Host: ${tenantId}.pgs.us-central-1.sysnee.com`);
|
|
306
327
|
console.log(` Port: 5432`);
|
|
307
328
|
console.log(` Database: ${tenantId}`);
|
|
308
329
|
console.log(` Password: ${password}`);
|
|
330
|
+
console.log(` SSL: required`);
|
|
309
331
|
console.log(` Access: enabled (use 'disable-access ${tenantId}' to disable)`);
|
|
310
332
|
|
|
311
333
|
return { tenantId, serviceName };
|
|
@@ -321,7 +343,7 @@ function listTenants() {
|
|
|
321
343
|
}
|
|
322
344
|
|
|
323
345
|
const tenants = Object.keys(doc.services)
|
|
324
|
-
.filter(name => name.startsWith('pgs_') && name !== '
|
|
346
|
+
.filter(name => name.startsWith('pgs_') && name !== 'traefik')
|
|
325
347
|
.map(name => {
|
|
326
348
|
const service = doc.services[name];
|
|
327
349
|
const db = service.environment?.POSTGRES_DB || 'N/A';
|
|
@@ -337,14 +359,15 @@ function listTenants() {
|
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
console.log('\nTenants:');
|
|
340
|
-
console.log('─'.repeat(
|
|
341
|
-
console.log(` ${'ID'.padEnd(
|
|
342
|
-
console.log('─'.repeat(
|
|
362
|
+
console.log('─'.repeat(90));
|
|
363
|
+
console.log(` ${'ID'.padEnd(25)} ${'Host'.padEnd(45)} ${'Access'.padEnd(15)}`);
|
|
364
|
+
console.log('─'.repeat(90));
|
|
343
365
|
tenants.forEach(t => {
|
|
344
366
|
const accessStr = t.access ? '✓ enabled' : '✗ disabled';
|
|
345
|
-
|
|
367
|
+
const host = `${t.tenantId}.pgs.us-central-1.sysnee.com`;
|
|
368
|
+
console.log(` ${t.tenantId.padEnd(25)} ${host.padEnd(45)} ${accessStr}`);
|
|
346
369
|
});
|
|
347
|
-
console.log('─'.repeat(
|
|
370
|
+
console.log('─'.repeat(90));
|
|
348
371
|
}
|
|
349
372
|
|
|
350
373
|
function enableTenantAccess(tenantId) {
|
|
@@ -355,11 +378,8 @@ function enableTenantAccess(tenantId) {
|
|
|
355
378
|
throw new Error(`Tenant ${tenantId} not found`);
|
|
356
379
|
}
|
|
357
380
|
|
|
358
|
-
// Update access control
|
|
359
381
|
setTenantAccess(tenantId, true);
|
|
360
|
-
|
|
361
|
-
// Restart HAProxy
|
|
362
|
-
executeCommand('docker restart postgres_proxy');
|
|
382
|
+
generateTraefikDynamicConfig();
|
|
363
383
|
|
|
364
384
|
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
365
385
|
}
|
|
@@ -372,11 +392,8 @@ function disableTenantAccess(tenantId) {
|
|
|
372
392
|
throw new Error(`Tenant ${tenantId} not found`);
|
|
373
393
|
}
|
|
374
394
|
|
|
375
|
-
// Update access control
|
|
376
395
|
setTenantAccess(tenantId, false);
|
|
377
|
-
|
|
378
|
-
// Restart HAProxy
|
|
379
|
-
executeCommand('docker restart postgres_proxy');
|
|
396
|
+
generateTraefikDynamicConfig();
|
|
380
397
|
|
|
381
398
|
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
382
399
|
}
|
|
@@ -403,10 +420,9 @@ function removeTenant(tenantId) {
|
|
|
403
420
|
|
|
404
421
|
saveCompose(doc);
|
|
405
422
|
|
|
406
|
-
// Remove from tenant access and regenerate HAProxy config
|
|
407
423
|
removeTenantAccess(tenantId);
|
|
408
|
-
|
|
409
|
-
|
|
424
|
+
generateTraefikDynamicConfig();
|
|
425
|
+
updateTraefikService();
|
|
410
426
|
|
|
411
427
|
console.log(`✓ Removed tenant: ${tenantId}`);
|
|
412
428
|
console.log(` Run: docker compose down ${serviceName}`);
|
|
@@ -538,7 +554,6 @@ program
|
|
|
538
554
|
checkSetupStatus()
|
|
539
555
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
540
556
|
executeCommand(`docker compose stop ${service}`.trim());
|
|
541
|
-
executeCommand('docker restart postgres_proxy');
|
|
542
557
|
});
|
|
543
558
|
|
|
544
559
|
program
|
|
@@ -570,4 +585,3 @@ program
|
|
|
570
585
|
});
|
|
571
586
|
|
|
572
587
|
program.parse();
|
|
573
|
-
|
package/package.json
CHANGED
package/traefik.yml
ADDED
package/haproxy-lua/pg-route.lua
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
-- PostgreSQL Protocol Parser for HAProxy
|
|
2
|
-
-- Routes connections based on username extracted from startup packet
|
|
3
|
-
-- Checks tenant-access.json for access control
|
|
4
|
-
|
|
5
|
-
-- Simple JSON parser for our limited use case (flat object with string keys and boolean values)
|
|
6
|
-
local function parse_json(str)
|
|
7
|
-
local result = {}
|
|
8
|
-
-- Match patterns like "key": true or "key": false
|
|
9
|
-
for key, value in string.gmatch(str, '"([^"]+)":%s*(%w+)') do
|
|
10
|
-
if value == "true" then
|
|
11
|
-
result[key] = true
|
|
12
|
-
elseif value == "false" then
|
|
13
|
-
result[key] = false
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
return result
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
-- Cache for tenant access configuration
|
|
20
|
-
local tenant_access_cache = {}
|
|
21
|
-
local cache_timestamp = 0
|
|
22
|
-
local CACHE_TTL = 5 -- seconds
|
|
23
|
-
|
|
24
|
-
-- Load tenant access configuration
|
|
25
|
-
local function load_tenant_access()
|
|
26
|
-
local now = core.now().sec
|
|
27
|
-
if now - cache_timestamp < CACHE_TTL then
|
|
28
|
-
return tenant_access_cache
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
local file = io.open("/etc/haproxy/tenant-access.json", "r")
|
|
32
|
-
if not file then
|
|
33
|
-
core.Warning("tenant-access.json not found, denying all connections")
|
|
34
|
-
tenant_access_cache = {}
|
|
35
|
-
cache_timestamp = now
|
|
36
|
-
return tenant_access_cache
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
local content = file:read("*all")
|
|
40
|
-
file:close()
|
|
41
|
-
|
|
42
|
-
local data = parse_json(content)
|
|
43
|
-
tenant_access_cache = data or {}
|
|
44
|
-
cache_timestamp = now
|
|
45
|
-
|
|
46
|
-
return tenant_access_cache
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
-- Parse PostgreSQL startup packet to extract username
|
|
50
|
-
-- PostgreSQL startup packet format:
|
|
51
|
-
-- [4 bytes: length] [4 bytes: protocol version] [key=value pairs\0]
|
|
52
|
-
local function parse_startup_packet(data)
|
|
53
|
-
if #data < 8 then
|
|
54
|
-
return nil
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
-- Read packet length (big-endian)
|
|
58
|
-
local len = (string.byte(data, 1) * 16777216) +
|
|
59
|
-
(string.byte(data, 2) * 65536) +
|
|
60
|
-
(string.byte(data, 3) * 256) +
|
|
61
|
-
string.byte(data, 4)
|
|
62
|
-
|
|
63
|
-
if #data < len then
|
|
64
|
-
return nil
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
-- Read protocol version
|
|
68
|
-
local major = (string.byte(data, 5) * 256) + string.byte(data, 6)
|
|
69
|
-
local minor = (string.byte(data, 7) * 256) + string.byte(data, 8)
|
|
70
|
-
|
|
71
|
-
-- Check for SSL request (protocol 1234.5679)
|
|
72
|
-
if major == 1234 and minor == 5679 then
|
|
73
|
-
return { ssl_request = true }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
-- Check for cancel request (protocol 1234.5678)
|
|
77
|
-
if major == 1234 and minor == 5678 then
|
|
78
|
-
return { cancel_request = true }
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
-- Normal startup message (protocol 3.0)
|
|
82
|
-
if major ~= 3 then
|
|
83
|
-
return nil
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
-- Parse key-value pairs starting at byte 9
|
|
87
|
-
local params = {}
|
|
88
|
-
local pos = 9
|
|
89
|
-
|
|
90
|
-
while pos < len do
|
|
91
|
-
-- Read key
|
|
92
|
-
local key_start = pos
|
|
93
|
-
while pos <= len and string.byte(data, pos) ~= 0 do
|
|
94
|
-
pos = pos + 1
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
if pos > len then break end
|
|
98
|
-
|
|
99
|
-
local key = string.sub(data, key_start, pos - 1)
|
|
100
|
-
pos = pos + 1 -- skip null
|
|
101
|
-
|
|
102
|
-
if key == "" then break end -- end of parameters
|
|
103
|
-
|
|
104
|
-
-- Read value
|
|
105
|
-
local val_start = pos
|
|
106
|
-
while pos <= len and string.byte(data, pos) ~= 0 do
|
|
107
|
-
pos = pos + 1
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
local value = string.sub(data, val_start, pos - 1)
|
|
111
|
-
pos = pos + 1 -- skip null
|
|
112
|
-
|
|
113
|
-
params[key] = value
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
return params
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
-- Main routing function called by HAProxy
|
|
120
|
-
function pg_route(txn)
|
|
121
|
-
-- Get data from the request buffer
|
|
122
|
-
local data = txn.req:data(0)
|
|
123
|
-
|
|
124
|
-
if not data or #data == 0 then
|
|
125
|
-
-- No data yet, need to wait
|
|
126
|
-
return
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
local params = parse_startup_packet(data)
|
|
130
|
-
|
|
131
|
-
if not params then
|
|
132
|
-
core.Warning("Failed to parse PostgreSQL startup packet")
|
|
133
|
-
txn:set_var("txn.pg_blocked", true)
|
|
134
|
-
return
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
-- Handle SSL request - route to default pool for SSL negotiation
|
|
138
|
-
-- PostgreSQL will respond 'N' (no SSL), then client retries without SSL
|
|
139
|
-
if params.ssl_request then
|
|
140
|
-
core.Info("SSL request received, routing to SSL negotiation pool")
|
|
141
|
-
-- Route to SSL pool backend which forwards to any available PostgreSQL
|
|
142
|
-
-- This allows SSL negotiation to complete
|
|
143
|
-
txn:set_var("txn.pg_backend", "pg_ssl_pool")
|
|
144
|
-
return
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
-- Handle cancel request
|
|
148
|
-
if params.cancel_request then
|
|
149
|
-
txn:set_var("txn.pg_blocked", true)
|
|
150
|
-
return
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
local username = params["user"]
|
|
154
|
-
if not username then
|
|
155
|
-
core.Warning("No username in PostgreSQL startup packet")
|
|
156
|
-
txn:set_var("txn.pg_blocked", true)
|
|
157
|
-
return
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
-- Load tenant access configuration
|
|
161
|
-
local access = load_tenant_access()
|
|
162
|
-
|
|
163
|
-
-- Check if tenant has external access enabled
|
|
164
|
-
if not access[username] then
|
|
165
|
-
core.Warning("Access denied: " .. username)
|
|
166
|
-
txn:set_var("txn.pg_blocked", true)
|
|
167
|
-
return
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
-- Route to tenant's backend
|
|
171
|
-
local backend = "pgs_" .. username
|
|
172
|
-
core.Info("Routing user " .. username .. " to backend " .. backend)
|
|
173
|
-
txn:set_var("txn.pg_backend", backend)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
-- Register the action with HAProxy
|
|
177
|
-
core.register_action("pg_route", { "tcp-req" }, pg_route, 0)
|
package/haproxy.cfg
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
global
|
|
2
|
-
log stdout format raw local0 info
|
|
3
|
-
maxconn 4096
|
|
4
|
-
lua-load /etc/haproxy/lua/pg-route.lua
|
|
5
|
-
|
|
6
|
-
defaults
|
|
7
|
-
log global
|
|
8
|
-
mode tcp
|
|
9
|
-
option tcplog
|
|
10
|
-
timeout connect 5s
|
|
11
|
-
timeout client 30s
|
|
12
|
-
timeout server 30s
|
|
13
|
-
retries 3
|
|
14
|
-
|
|
15
|
-
frontend postgres_frontend
|
|
16
|
-
bind *:5432
|
|
17
|
-
mode tcp
|
|
18
|
-
|
|
19
|
-
# Use Lua script to parse PostgreSQL startup packet and route by username
|
|
20
|
-
tcp-request inspect-delay 5s
|
|
21
|
-
tcp-request content lua.pg_route
|
|
22
|
-
tcp-request content reject if { var(txn.pg_blocked) -m bool }
|
|
23
|
-
|
|
24
|
-
# Route to backend based on username extracted by Lua
|
|
25
|
-
use_backend %[var(txn.pg_backend)] if { var(txn.pg_backend) -m found }
|
|
26
|
-
|
|
27
|
-
# Default backend for unknown connections
|
|
28
|
-
default_backend pg_reject
|
|
29
|
-
|
|
30
|
-
backend pg_reject
|
|
31
|
-
mode tcp
|
|
32
|
-
timeout server 1s
|
|
33
|
-
|
|
34
|
-
backend pg_ssl_pool
|
|
35
|
-
mode tcp
|