@sysnee/pgs 0.1.3 → 0.1.4-rc1
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 +88 -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,48 @@
|
|
|
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';
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.sysnee-config');
|
|
11
|
+
|
|
12
|
+
const COMPOSE_FILE_PATH = path.join(CONFIG_DIR, 'docker-compose.yml');
|
|
13
|
+
const INIT_DIR_PATH = path.join(CONFIG_DIR, 'init');
|
|
14
|
+
const HAPROXY_CFG_PATH = path.join(CONFIG_DIR, 'haproxy.cfg');
|
|
15
|
+
const TENANT_ACCESS_FILE_PATH = path.join(CONFIG_DIR, 'tenant-access.json');
|
|
16
|
+
const SETUP_STATUS_PATH = path.join(CONFIG_DIR, 'status.txt');
|
|
13
17
|
|
|
14
18
|
function loadCompose() {
|
|
15
|
-
if (!fs.existsSync(
|
|
16
|
-
|
|
19
|
+
if (!fs.existsSync(COMPOSE_FILE_PATH)) {
|
|
20
|
+
const dockerComposeInitialContent = readFileSync(path.join(path.dirname(''), 'docker-compose.yml'), 'utf8')
|
|
21
|
+
writeFileSync(COMPOSE_FILE_PATH, dockerComposeInitialContent)
|
|
17
22
|
}
|
|
18
|
-
const content = fs.readFileSync(
|
|
23
|
+
const content = fs.readFileSync(COMPOSE_FILE_PATH, 'utf8');
|
|
24
|
+
console.debug(`docker-compose file readed from ${COMPOSE_FILE_PATH}`)
|
|
19
25
|
return yaml.load(content);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
function saveCompose(doc) {
|
|
23
29
|
const content = yaml.dump(doc, { indent: 2, lineWidth: -1 });
|
|
24
|
-
fs.writeFileSync(
|
|
30
|
+
fs.writeFileSync(COMPOSE_FILE_PATH, content);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
// ==================== Tenant Access Management ====================
|
|
28
34
|
|
|
29
35
|
function loadTenantAccess() {
|
|
30
|
-
if (!fs.existsSync(
|
|
31
|
-
fs.writeFileSync(
|
|
36
|
+
if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
|
|
37
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
|
|
32
38
|
}
|
|
33
|
-
const content = fs.readFileSync(
|
|
39
|
+
const content = fs.readFileSync(TENANT_ACCESS_FILE_PATH, 'utf8');
|
|
34
40
|
return JSON.parse(content) || {};
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function saveTenantAccess(access) {
|
|
38
44
|
const content = JSON.stringify(access, null, 2);
|
|
39
|
-
fs.writeFileSync(
|
|
45
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, content);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function setTenantAccess(tenantId, enabled) {
|
|
@@ -54,49 +60,20 @@ function removeTenantAccess(tenantId) {
|
|
|
54
60
|
// ==================== HAProxy Configuration ====================
|
|
55
61
|
|
|
56
62
|
function generateHAProxyConfig() {
|
|
63
|
+
console.debug('In generateHAProxy function')
|
|
57
64
|
const doc = loadCompose();
|
|
58
65
|
|
|
59
66
|
// Get all tenant services
|
|
60
67
|
const tenants = Object.keys(doc.services || {})
|
|
61
68
|
.filter(name => name.startsWith('pgs_') && name !== 'haproxy');
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
let config = `global
|
|
65
|
-
log stdout format raw local0 info
|
|
66
|
-
maxconn 4096
|
|
67
|
-
lua-load /etc/haproxy/lua/pg-route.lua
|
|
70
|
+
console.debug(`tenants.lenght: ${tenants.length}`)
|
|
68
71
|
|
|
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
|
-
`;
|
|
72
|
+
// Get initial HAProxy config
|
|
73
|
+
const templateFilePath = path.join(path.dirname(''), 'haproxy.cfg')
|
|
74
|
+
console.debug(`haproxy template file path: ${templateFilePath}`)
|
|
75
|
+
let config = readFileSync(templateFilePath, 'utf8');
|
|
76
|
+
console.debug(`haproxy template file loaded: ${config}`)
|
|
100
77
|
|
|
101
78
|
// Add all tenant backends to SSL pool for SSL negotiation
|
|
102
79
|
// PostgreSQL will respond 'N' (no SSL) during negotiation, then client retries without SSL
|
|
@@ -121,7 +98,9 @@ backend pg_ssl_pool
|
|
|
121
98
|
`;
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
|
|
101
|
+
console.log(`Preparing to save haproxy file in ${HAPROXY_CFG_PATH}`)
|
|
102
|
+
writeFileSync(HAPROXY_CFG_PATH, config);
|
|
103
|
+
console.log(`Succesfully saved haproxy file in ${HAPROXY_CFG_PATH}`)
|
|
125
104
|
}
|
|
126
105
|
|
|
127
106
|
function updateHAProxyDependsOn() {
|
|
@@ -157,21 +136,23 @@ function updateHAProxyDependsOn() {
|
|
|
157
136
|
saveCompose(doc);
|
|
158
137
|
|
|
159
138
|
// Restart HAProxy
|
|
160
|
-
executeCommand('
|
|
139
|
+
executeCommand('docker restart postgres_proxy');
|
|
161
140
|
}
|
|
162
141
|
|
|
163
142
|
function createInitScript({ tenantId, password, databaseName }) {
|
|
164
|
-
if (!fs.existsSync(
|
|
165
|
-
fs.mkdirSync(
|
|
143
|
+
if (!fs.existsSync(INIT_DIR_PATH)) {
|
|
144
|
+
fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
const initFile = path.join(
|
|
147
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
169
148
|
const sql = `CREATE ROLE ${tenantId} WITH LOGIN PASSWORD '${password}' SUPERUSER CREATEDB CREATEROLE;
|
|
170
149
|
CREATE DATABASE ${databaseName} OWNER ${tenantId};
|
|
171
150
|
GRANT ALL PRIVILEGES ON DATABASE ${databaseName} TO ${tenantId};
|
|
172
151
|
`;
|
|
173
152
|
|
|
174
153
|
fs.writeFileSync(initFile, sql);
|
|
154
|
+
|
|
155
|
+
console.debug('Init script created!')
|
|
175
156
|
return initFile;
|
|
176
157
|
}
|
|
177
158
|
|
|
@@ -217,6 +198,30 @@ function ensureNetwork(doc) {
|
|
|
217
198
|
}
|
|
218
199
|
}
|
|
219
200
|
|
|
201
|
+
function ensureDockerPrivilegies() {
|
|
202
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
203
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
execSync('sudo usermod -aG docker $USER && newgrp docker')
|
|
206
|
+
writeFileSync(SETUP_STATUS_PATH, 'container:ok')
|
|
207
|
+
console.log('All ready!')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function checkSetupStatus() {
|
|
211
|
+
try {
|
|
212
|
+
const content = readFileSync(SETUP_STATUS_PATH, 'utf8')
|
|
213
|
+
if (content === 'container:ok') {
|
|
214
|
+
return true
|
|
215
|
+
} else {
|
|
216
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
217
|
+
process.exit(0)
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
221
|
+
process.exit(0)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
function createTenant(tenantId, options = {}) {
|
|
221
226
|
const { version = '18', password, limits = DEFAULT_LIMITS } = options;
|
|
222
227
|
const doc = loadCompose();
|
|
@@ -255,7 +260,7 @@ function createTenant(tenantId, options = {}) {
|
|
|
255
260
|
updateHAProxyDependsOn();
|
|
256
261
|
|
|
257
262
|
// start the tenant
|
|
258
|
-
executeCommand(`
|
|
263
|
+
executeCommand(`docker compose up -d ${serviceName}`.trim());
|
|
259
264
|
|
|
260
265
|
console.log(`✓ Created tenant: ${tenantId}`);
|
|
261
266
|
console.log(` Service: ${serviceName}`);
|
|
@@ -315,7 +320,7 @@ function enableTenantAccess(tenantId) {
|
|
|
315
320
|
setTenantAccess(tenantId, true);
|
|
316
321
|
|
|
317
322
|
// Restart HAProxy
|
|
318
|
-
executeCommand('
|
|
323
|
+
executeCommand('docker restart postgres_proxy');
|
|
319
324
|
|
|
320
325
|
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
321
326
|
}
|
|
@@ -332,7 +337,7 @@ function disableTenantAccess(tenantId) {
|
|
|
332
337
|
setTenantAccess(tenantId, false);
|
|
333
338
|
|
|
334
339
|
// Restart HAProxy
|
|
335
|
-
executeCommand('
|
|
340
|
+
executeCommand('docker restart postgres_proxy');
|
|
336
341
|
|
|
337
342
|
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
338
343
|
}
|
|
@@ -352,7 +357,7 @@ function removeTenant(tenantId) {
|
|
|
352
357
|
delete doc.volumes[volumeName];
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
const initFile = path.join(
|
|
360
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
356
361
|
if (fs.existsSync(initFile)) {
|
|
357
362
|
fs.unlinkSync(initFile);
|
|
358
363
|
}
|
|
@@ -371,7 +376,7 @@ function removeTenant(tenantId) {
|
|
|
371
376
|
|
|
372
377
|
function executeCommand(command) {
|
|
373
378
|
try {
|
|
374
|
-
execSync(command, { stdio: 'inherit', cwd:
|
|
379
|
+
execSync(command, { stdio: 'inherit', cwd: CONFIG_DIR });
|
|
375
380
|
} catch (error) {
|
|
376
381
|
console.error(`Error executing: ${command}`);
|
|
377
382
|
process.exit(1);
|
|
@@ -387,7 +392,14 @@ const program = new Command();
|
|
|
387
392
|
program
|
|
388
393
|
.name('postgres-manager')
|
|
389
394
|
.description('Manage PostgreSQL tenant instances')
|
|
390
|
-
.version('1.
|
|
395
|
+
.version('0.1.4-rc1');
|
|
396
|
+
|
|
397
|
+
program
|
|
398
|
+
.command('setup')
|
|
399
|
+
.description('Initial required setup')
|
|
400
|
+
.action(() => {
|
|
401
|
+
ensureDockerPrivilegies()
|
|
402
|
+
})
|
|
391
403
|
|
|
392
404
|
program
|
|
393
405
|
.command('create')
|
|
@@ -399,7 +411,9 @@ program
|
|
|
399
411
|
.option('--cpu <cpu>', 'CPU limit (cores)', '0.5')
|
|
400
412
|
.option('--memory <memory>', 'Memory limit (MB)', '256')
|
|
401
413
|
.action((...args) => {
|
|
414
|
+
checkSetupStatus()
|
|
402
415
|
try {
|
|
416
|
+
|
|
403
417
|
const providedTenantId = args[0];
|
|
404
418
|
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
405
419
|
const tenantId = `${providedTenantId}_${randomSuffix}`;
|
|
@@ -411,17 +425,18 @@ program
|
|
|
411
425
|
version: null,
|
|
412
426
|
limits: null
|
|
413
427
|
}
|
|
414
|
-
|
|
428
|
+
|
|
415
429
|
if (opts.file) {
|
|
416
430
|
const manifestFilePath = path.resolve(opts.file)
|
|
417
431
|
const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
|
|
418
|
-
|
|
432
|
+
|
|
419
433
|
if (optsFromManifest.type !== 'postgres') {
|
|
420
434
|
throw new Error(`The type "${optsFromManifest.type}" is not valid for this agent.`)
|
|
421
435
|
}
|
|
422
436
|
console.log('optsFromManifest', optsFromManifest)
|
|
423
437
|
|
|
424
438
|
tenantOptions.limits = optsFromManifest.shared_limits
|
|
439
|
+
tenantOptions.version = optsFromManifest.version
|
|
425
440
|
} else {
|
|
426
441
|
tenantOptions.version = opts.version;
|
|
427
442
|
tenantOptions.limits = {
|
|
@@ -429,9 +444,9 @@ program
|
|
|
429
444
|
memory: parseInt(opts.memory, 10),
|
|
430
445
|
};
|
|
431
446
|
}
|
|
432
|
-
|
|
447
|
+
|
|
433
448
|
tenantOptions.password = opts.password || generateRandomPassword();
|
|
434
|
-
|
|
449
|
+
|
|
435
450
|
createTenant(tenantId, tenantOptions);
|
|
436
451
|
} catch (error) {
|
|
437
452
|
console.error(`Error: ${error}`);
|
|
@@ -443,6 +458,7 @@ program
|
|
|
443
458
|
.command('list')
|
|
444
459
|
.description('List all tenant instances')
|
|
445
460
|
.action(() => {
|
|
461
|
+
checkSetupStatus()
|
|
446
462
|
try {
|
|
447
463
|
listTenants();
|
|
448
464
|
} catch (error) {
|
|
@@ -456,6 +472,7 @@ program
|
|
|
456
472
|
.description('Remove a tenant instance')
|
|
457
473
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
458
474
|
.action((tenantId) => {
|
|
475
|
+
checkSetupStatus()
|
|
459
476
|
try {
|
|
460
477
|
removeTenant(tenantId);
|
|
461
478
|
} catch (error) {
|
|
@@ -469,8 +486,9 @@ program
|
|
|
469
486
|
.description('Start all PostgreSQL services')
|
|
470
487
|
.argument('[tenant-id]', 'Tenant identifier (optional, starts all if omitted)')
|
|
471
488
|
.action((tenantId) => {
|
|
489
|
+
checkSetupStatus()
|
|
472
490
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
473
|
-
executeCommand(`
|
|
491
|
+
executeCommand(`docker compose up -d ${service}`.trim());
|
|
474
492
|
});
|
|
475
493
|
|
|
476
494
|
program
|
|
@@ -478,9 +496,10 @@ program
|
|
|
478
496
|
.description('Stop all PostgreSQL services')
|
|
479
497
|
.argument('[tenant-id]', 'Tenant identifier (optional, stops all if omitted)')
|
|
480
498
|
.action((tenantId) => {
|
|
499
|
+
checkSetupStatus()
|
|
481
500
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
482
|
-
executeCommand(`
|
|
483
|
-
executeCommand('
|
|
501
|
+
executeCommand(`docker compose stop ${service}`.trim());
|
|
502
|
+
executeCommand('docker restart postgres_proxy');
|
|
484
503
|
});
|
|
485
504
|
|
|
486
505
|
program
|
|
@@ -488,6 +507,7 @@ program
|
|
|
488
507
|
.description('Enable external access for a tenant')
|
|
489
508
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
490
509
|
.action((tenantId) => {
|
|
510
|
+
checkSetupStatus()
|
|
491
511
|
try {
|
|
492
512
|
enableTenantAccess(tenantId);
|
|
493
513
|
} catch (error) {
|
|
@@ -501,6 +521,7 @@ program
|
|
|
501
521
|
.description('Disable external access for a tenant')
|
|
502
522
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
503
523
|
.action((tenantId) => {
|
|
524
|
+
checkSetupStatus()
|
|
504
525
|
try {
|
|
505
526
|
disableTenantAccess(tenantId);
|
|
506
527
|
} 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
|
-
}
|