@sysnee/pgs 0.1.3 → 0.1.4-rc2
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/haproxy.cfg +35 -0
- package/manager.js +93 -67
- package/package.json +1 -1
- package/manifest.json +0 -32
package/haproxy.cfg
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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
|
package/manager.js
CHANGED
|
@@ -1,42 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs, { readFileSync } from 'fs';
|
|
3
|
+
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import { Command } from 'commander';
|
|
7
8
|
import { execSync } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
8
11
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.sysnee-config');
|
|
16
|
+
|
|
17
|
+
const COMPOSE_FILE_PATH = path.join(CONFIG_DIR, 'docker-compose.yml');
|
|
18
|
+
const INIT_DIR_PATH = path.join(CONFIG_DIR, 'init');
|
|
19
|
+
const HAPROXY_CFG_PATH = path.join(CONFIG_DIR, 'haproxy.cfg');
|
|
20
|
+
const TENANT_ACCESS_FILE_PATH = path.join(CONFIG_DIR, 'tenant-access.json');
|
|
21
|
+
const SETUP_STATUS_PATH = path.join(CONFIG_DIR, 'status.txt');
|
|
13
22
|
|
|
14
23
|
function loadCompose() {
|
|
15
|
-
if (!fs.existsSync(
|
|
16
|
-
|
|
24
|
+
if (!fs.existsSync(COMPOSE_FILE_PATH)) {
|
|
25
|
+
const dockerComposeInitialContent = readFileSync(path.join(path.dirname(''), 'docker-compose.yml'), 'utf8')
|
|
26
|
+
writeFileSync(COMPOSE_FILE_PATH, dockerComposeInitialContent)
|
|
17
27
|
}
|
|
18
|
-
const content = fs.readFileSync(
|
|
28
|
+
const content = fs.readFileSync(COMPOSE_FILE_PATH, 'utf8');
|
|
29
|
+
console.debug(`docker-compose file readed from ${COMPOSE_FILE_PATH}`)
|
|
19
30
|
return yaml.load(content);
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
function saveCompose(doc) {
|
|
23
34
|
const content = yaml.dump(doc, { indent: 2, lineWidth: -1 });
|
|
24
|
-
fs.writeFileSync(
|
|
35
|
+
fs.writeFileSync(COMPOSE_FILE_PATH, content);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
// ==================== Tenant Access Management ====================
|
|
28
39
|
|
|
29
40
|
function loadTenantAccess() {
|
|
30
|
-
if (!fs.existsSync(
|
|
31
|
-
fs.writeFileSync(
|
|
41
|
+
if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
|
|
42
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
|
|
32
43
|
}
|
|
33
|
-
const content = fs.readFileSync(
|
|
44
|
+
const content = fs.readFileSync(TENANT_ACCESS_FILE_PATH, 'utf8');
|
|
34
45
|
return JSON.parse(content) || {};
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
function saveTenantAccess(access) {
|
|
38
49
|
const content = JSON.stringify(access, null, 2);
|
|
39
|
-
fs.writeFileSync(
|
|
50
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, content);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
function setTenantAccess(tenantId, enabled) {
|
|
@@ -54,49 +65,20 @@ function removeTenantAccess(tenantId) {
|
|
|
54
65
|
// ==================== HAProxy Configuration ====================
|
|
55
66
|
|
|
56
67
|
function generateHAProxyConfig() {
|
|
68
|
+
console.debug('In generateHAProxy function')
|
|
57
69
|
const doc = loadCompose();
|
|
58
70
|
|
|
59
71
|
// Get all tenant services
|
|
60
72
|
const tenants = Object.keys(doc.services || {})
|
|
61
73
|
.filter(name => name.startsWith('pgs_') && name !== 'haproxy');
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
let config = `global
|
|
65
|
-
log stdout format raw local0 info
|
|
66
|
-
maxconn 4096
|
|
67
|
-
lua-load /etc/haproxy/lua/pg-route.lua
|
|
75
|
+
console.debug(`tenants.lenght: ${tenants.length}`)
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
timeout client 30s
|
|
75
|
-
timeout server 30s
|
|
76
|
-
retries 3
|
|
77
|
-
|
|
78
|
-
frontend postgres_frontend
|
|
79
|
-
bind *:5432
|
|
80
|
-
mode tcp
|
|
81
|
-
|
|
82
|
-
# Use Lua script to parse PostgreSQL startup packet and route by username
|
|
83
|
-
tcp-request inspect-delay 5s
|
|
84
|
-
tcp-request content lua.pg_route
|
|
85
|
-
tcp-request content reject if { var(txn.pg_blocked) -m bool }
|
|
86
|
-
|
|
87
|
-
# Route to backend based on username extracted by Lua
|
|
88
|
-
use_backend %[var(txn.pg_backend)] if { var(txn.pg_backend) -m found }
|
|
89
|
-
|
|
90
|
-
# Default backend for unknown connections
|
|
91
|
-
default_backend pg_reject
|
|
92
|
-
|
|
93
|
-
backend pg_reject
|
|
94
|
-
mode tcp
|
|
95
|
-
timeout server 1s
|
|
96
|
-
|
|
97
|
-
backend pg_ssl_pool
|
|
98
|
-
mode tcp
|
|
99
|
-
`;
|
|
77
|
+
// Get initial HAProxy config
|
|
78
|
+
const templateFilePath = path.join(__dirname, 'haproxy.cfg')
|
|
79
|
+
console.debug(`haproxy template file path: ${templateFilePath}`)
|
|
80
|
+
let config = readFileSync(templateFilePath, 'utf8');
|
|
81
|
+
console.debug(`haproxy template file loaded`)
|
|
100
82
|
|
|
101
83
|
// Add all tenant backends to SSL pool for SSL negotiation
|
|
102
84
|
// PostgreSQL will respond 'N' (no SSL) during negotiation, then client retries without SSL
|
|
@@ -121,7 +103,9 @@ backend pg_ssl_pool
|
|
|
121
103
|
`;
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
|
|
106
|
+
console.log(`Preparing to save haproxy file in ${HAPROXY_CFG_PATH}`)
|
|
107
|
+
writeFileSync(HAPROXY_CFG_PATH, config);
|
|
108
|
+
console.log(`Succesfully saved haproxy file in ${HAPROXY_CFG_PATH}`)
|
|
125
109
|
}
|
|
126
110
|
|
|
127
111
|
function updateHAProxyDependsOn() {
|
|
@@ -157,21 +141,23 @@ function updateHAProxyDependsOn() {
|
|
|
157
141
|
saveCompose(doc);
|
|
158
142
|
|
|
159
143
|
// Restart HAProxy
|
|
160
|
-
executeCommand('
|
|
144
|
+
executeCommand('docker restart postgres_proxy');
|
|
161
145
|
}
|
|
162
146
|
|
|
163
147
|
function createInitScript({ tenantId, password, databaseName }) {
|
|
164
|
-
if (!fs.existsSync(
|
|
165
|
-
fs.mkdirSync(
|
|
148
|
+
if (!fs.existsSync(INIT_DIR_PATH)) {
|
|
149
|
+
fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
const initFile = path.join(
|
|
152
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
169
153
|
const sql = `CREATE ROLE ${tenantId} WITH LOGIN PASSWORD '${password}' SUPERUSER CREATEDB CREATEROLE;
|
|
170
154
|
CREATE DATABASE ${databaseName} OWNER ${tenantId};
|
|
171
155
|
GRANT ALL PRIVILEGES ON DATABASE ${databaseName} TO ${tenantId};
|
|
172
156
|
`;
|
|
173
157
|
|
|
174
158
|
fs.writeFileSync(initFile, sql);
|
|
159
|
+
|
|
160
|
+
console.debug('Init script created!')
|
|
175
161
|
return initFile;
|
|
176
162
|
}
|
|
177
163
|
|
|
@@ -217,6 +203,30 @@ function ensureNetwork(doc) {
|
|
|
217
203
|
}
|
|
218
204
|
}
|
|
219
205
|
|
|
206
|
+
function ensureDockerPrivilegies() {
|
|
207
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
208
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
execSync('sudo usermod -aG docker $USER && newgrp docker')
|
|
211
|
+
writeFileSync(SETUP_STATUS_PATH, 'container:ok')
|
|
212
|
+
console.log('All ready!')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function checkSetupStatus() {
|
|
216
|
+
try {
|
|
217
|
+
const content = readFileSync(SETUP_STATUS_PATH, 'utf8')
|
|
218
|
+
if (content === 'container:ok') {
|
|
219
|
+
return true
|
|
220
|
+
} else {
|
|
221
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
222
|
+
process.exit(0)
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
226
|
+
process.exit(0)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
220
230
|
function createTenant(tenantId, options = {}) {
|
|
221
231
|
const { version = '18', password, limits = DEFAULT_LIMITS } = options;
|
|
222
232
|
const doc = loadCompose();
|
|
@@ -255,7 +265,7 @@ function createTenant(tenantId, options = {}) {
|
|
|
255
265
|
updateHAProxyDependsOn();
|
|
256
266
|
|
|
257
267
|
// start the tenant
|
|
258
|
-
executeCommand(`
|
|
268
|
+
executeCommand(`docker compose up -d ${serviceName}`.trim());
|
|
259
269
|
|
|
260
270
|
console.log(`✓ Created tenant: ${tenantId}`);
|
|
261
271
|
console.log(` Service: ${serviceName}`);
|
|
@@ -315,7 +325,7 @@ function enableTenantAccess(tenantId) {
|
|
|
315
325
|
setTenantAccess(tenantId, true);
|
|
316
326
|
|
|
317
327
|
// Restart HAProxy
|
|
318
|
-
executeCommand('
|
|
328
|
+
executeCommand('docker restart postgres_proxy');
|
|
319
329
|
|
|
320
330
|
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
321
331
|
}
|
|
@@ -332,7 +342,7 @@ function disableTenantAccess(tenantId) {
|
|
|
332
342
|
setTenantAccess(tenantId, false);
|
|
333
343
|
|
|
334
344
|
// Restart HAProxy
|
|
335
|
-
executeCommand('
|
|
345
|
+
executeCommand('docker restart postgres_proxy');
|
|
336
346
|
|
|
337
347
|
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
338
348
|
}
|
|
@@ -352,7 +362,7 @@ function removeTenant(tenantId) {
|
|
|
352
362
|
delete doc.volumes[volumeName];
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
const initFile = path.join(
|
|
365
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
356
366
|
if (fs.existsSync(initFile)) {
|
|
357
367
|
fs.unlinkSync(initFile);
|
|
358
368
|
}
|
|
@@ -371,7 +381,7 @@ function removeTenant(tenantId) {
|
|
|
371
381
|
|
|
372
382
|
function executeCommand(command) {
|
|
373
383
|
try {
|
|
374
|
-
execSync(command, { stdio: 'inherit', cwd:
|
|
384
|
+
execSync(command, { stdio: 'inherit', cwd: CONFIG_DIR });
|
|
375
385
|
} catch (error) {
|
|
376
386
|
console.error(`Error executing: ${command}`);
|
|
377
387
|
process.exit(1);
|
|
@@ -387,7 +397,14 @@ const program = new Command();
|
|
|
387
397
|
program
|
|
388
398
|
.name('postgres-manager')
|
|
389
399
|
.description('Manage PostgreSQL tenant instances')
|
|
390
|
-
.version('
|
|
400
|
+
.version(JSON.parse(readFileSync('package.json', 'utf8')).version);
|
|
401
|
+
|
|
402
|
+
program
|
|
403
|
+
.command('setup')
|
|
404
|
+
.description('Initial required setup')
|
|
405
|
+
.action(() => {
|
|
406
|
+
ensureDockerPrivilegies()
|
|
407
|
+
})
|
|
391
408
|
|
|
392
409
|
program
|
|
393
410
|
.command('create')
|
|
@@ -399,7 +416,9 @@ program
|
|
|
399
416
|
.option('--cpu <cpu>', 'CPU limit (cores)', '0.5')
|
|
400
417
|
.option('--memory <memory>', 'Memory limit (MB)', '256')
|
|
401
418
|
.action((...args) => {
|
|
419
|
+
checkSetupStatus()
|
|
402
420
|
try {
|
|
421
|
+
|
|
403
422
|
const providedTenantId = args[0];
|
|
404
423
|
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
405
424
|
const tenantId = `${providedTenantId}_${randomSuffix}`;
|
|
@@ -411,17 +430,18 @@ program
|
|
|
411
430
|
version: null,
|
|
412
431
|
limits: null
|
|
413
432
|
}
|
|
414
|
-
|
|
433
|
+
|
|
415
434
|
if (opts.file) {
|
|
416
435
|
const manifestFilePath = path.resolve(opts.file)
|
|
417
436
|
const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
|
|
418
|
-
|
|
437
|
+
|
|
419
438
|
if (optsFromManifest.type !== 'postgres') {
|
|
420
439
|
throw new Error(`The type "${optsFromManifest.type}" is not valid for this agent.`)
|
|
421
440
|
}
|
|
422
441
|
console.log('optsFromManifest', optsFromManifest)
|
|
423
442
|
|
|
424
443
|
tenantOptions.limits = optsFromManifest.shared_limits
|
|
444
|
+
tenantOptions.version = optsFromManifest.version
|
|
425
445
|
} else {
|
|
426
446
|
tenantOptions.version = opts.version;
|
|
427
447
|
tenantOptions.limits = {
|
|
@@ -429,9 +449,9 @@ program
|
|
|
429
449
|
memory: parseInt(opts.memory, 10),
|
|
430
450
|
};
|
|
431
451
|
}
|
|
432
|
-
|
|
452
|
+
|
|
433
453
|
tenantOptions.password = opts.password || generateRandomPassword();
|
|
434
|
-
|
|
454
|
+
|
|
435
455
|
createTenant(tenantId, tenantOptions);
|
|
436
456
|
} catch (error) {
|
|
437
457
|
console.error(`Error: ${error}`);
|
|
@@ -443,6 +463,7 @@ program
|
|
|
443
463
|
.command('list')
|
|
444
464
|
.description('List all tenant instances')
|
|
445
465
|
.action(() => {
|
|
466
|
+
checkSetupStatus()
|
|
446
467
|
try {
|
|
447
468
|
listTenants();
|
|
448
469
|
} catch (error) {
|
|
@@ -456,6 +477,7 @@ program
|
|
|
456
477
|
.description('Remove a tenant instance')
|
|
457
478
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
458
479
|
.action((tenantId) => {
|
|
480
|
+
checkSetupStatus()
|
|
459
481
|
try {
|
|
460
482
|
removeTenant(tenantId);
|
|
461
483
|
} catch (error) {
|
|
@@ -469,8 +491,9 @@ program
|
|
|
469
491
|
.description('Start all PostgreSQL services')
|
|
470
492
|
.argument('[tenant-id]', 'Tenant identifier (optional, starts all if omitted)')
|
|
471
493
|
.action((tenantId) => {
|
|
494
|
+
checkSetupStatus()
|
|
472
495
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
473
|
-
executeCommand(`
|
|
496
|
+
executeCommand(`docker compose up -d ${service}`.trim());
|
|
474
497
|
});
|
|
475
498
|
|
|
476
499
|
program
|
|
@@ -478,9 +501,10 @@ program
|
|
|
478
501
|
.description('Stop all PostgreSQL services')
|
|
479
502
|
.argument('[tenant-id]', 'Tenant identifier (optional, stops all if omitted)')
|
|
480
503
|
.action((tenantId) => {
|
|
504
|
+
checkSetupStatus()
|
|
481
505
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
482
|
-
executeCommand(`
|
|
483
|
-
executeCommand('
|
|
506
|
+
executeCommand(`docker compose stop ${service}`.trim());
|
|
507
|
+
executeCommand('docker restart postgres_proxy');
|
|
484
508
|
});
|
|
485
509
|
|
|
486
510
|
program
|
|
@@ -488,6 +512,7 @@ program
|
|
|
488
512
|
.description('Enable external access for a tenant')
|
|
489
513
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
490
514
|
.action((tenantId) => {
|
|
515
|
+
checkSetupStatus()
|
|
491
516
|
try {
|
|
492
517
|
enableTenantAccess(tenantId);
|
|
493
518
|
} catch (error) {
|
|
@@ -501,6 +526,7 @@ program
|
|
|
501
526
|
.description('Disable external access for a tenant')
|
|
502
527
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
503
528
|
.action((tenantId) => {
|
|
529
|
+
checkSetupStatus()
|
|
504
530
|
try {
|
|
505
531
|
disableTenantAccess(tenantId);
|
|
506
532
|
} catch (error) {
|
package/package.json
CHANGED
package/manifest.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"type": "postgres",
|
|
3
|
-
"version": "18",
|
|
4
|
-
"tier": 1,
|
|
5
|
-
"purpose": "development",
|
|
6
|
-
"storage": 2,
|
|
7
|
-
"region": "us-central-1",
|
|
8
|
-
"provider": "gcp",
|
|
9
|
-
"zone": "a",
|
|
10
|
-
"shared": true,
|
|
11
|
-
"shared_limits": {
|
|
12
|
-
"cpu": 0.5,
|
|
13
|
-
"memory": 512
|
|
14
|
-
},
|
|
15
|
-
"postgres": {
|
|
16
|
-
"port": 5432,
|
|
17
|
-
"username": "postgres",
|
|
18
|
-
"password": "postgres",
|
|
19
|
-
"default_database": "postgres"
|
|
20
|
-
},
|
|
21
|
-
"firewall": {
|
|
22
|
-
"rules": [
|
|
23
|
-
{
|
|
24
|
-
"description": "Allow all traffic",
|
|
25
|
-
"type": "allow",
|
|
26
|
-
"protocol": "tcp",
|
|
27
|
-
"port": 5432,
|
|
28
|
-
"source": "0.0.0.0/0"
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
}
|