@sysnee/pgs 0.1.2 → 0.1.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/haproxy.cfg +35 -0
- package/manager.js +78 -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,47 @@
|
|
|
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');
|
|
19
24
|
return yaml.load(content);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
function saveCompose(doc) {
|
|
23
28
|
const content = yaml.dump(doc, { indent: 2, lineWidth: -1 });
|
|
24
|
-
fs.writeFileSync(
|
|
29
|
+
fs.writeFileSync(COMPOSE_FILE_PATH, content);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
// ==================== Tenant Access Management ====================
|
|
28
33
|
|
|
29
34
|
function loadTenantAccess() {
|
|
30
|
-
if (!fs.existsSync(
|
|
31
|
-
fs.writeFileSync(
|
|
35
|
+
if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
|
|
36
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
|
|
32
37
|
}
|
|
33
|
-
const content = fs.readFileSync(
|
|
38
|
+
const content = fs.readFileSync(TENANT_ACCESS_FILE_PATH, 'utf8');
|
|
34
39
|
return JSON.parse(content) || {};
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function saveTenantAccess(access) {
|
|
38
43
|
const content = JSON.stringify(access, null, 2);
|
|
39
|
-
fs.writeFileSync(
|
|
44
|
+
fs.writeFileSync(TENANT_ACCESS_FILE_PATH, content);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
function setTenantAccess(tenantId, enabled) {
|
|
@@ -60,43 +65,9 @@ function generateHAProxyConfig() {
|
|
|
60
65
|
const tenants = Object.keys(doc.services || {})
|
|
61
66
|
.filter(name => name.startsWith('pgs_') && name !== 'haproxy');
|
|
62
67
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
maxconn 4096
|
|
67
|
-
lua-load /etc/haproxy/lua/pg-route.lua
|
|
68
|
-
|
|
69
|
-
defaults
|
|
70
|
-
log global
|
|
71
|
-
mode tcp
|
|
72
|
-
option tcplog
|
|
73
|
-
timeout connect 5s
|
|
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
|
-
`;
|
|
68
|
+
// Get initial HAProxy config
|
|
69
|
+
const templateFilePath = path.join(path.dirname(''), 'haproxy.cfg')
|
|
70
|
+
let config = readFileSync(templateFilePath, 'utf8');
|
|
100
71
|
|
|
101
72
|
// Add all tenant backends to SSL pool for SSL negotiation
|
|
102
73
|
// PostgreSQL will respond 'N' (no SSL) during negotiation, then client retries without SSL
|
|
@@ -121,7 +92,7 @@ backend pg_ssl_pool
|
|
|
121
92
|
`;
|
|
122
93
|
}
|
|
123
94
|
|
|
124
|
-
fs.writeFileSync(
|
|
95
|
+
fs.writeFileSync(HAPROXY_CFG_PATH, config);
|
|
125
96
|
}
|
|
126
97
|
|
|
127
98
|
function updateHAProxyDependsOn() {
|
|
@@ -157,15 +128,15 @@ function updateHAProxyDependsOn() {
|
|
|
157
128
|
saveCompose(doc);
|
|
158
129
|
|
|
159
130
|
// Restart HAProxy
|
|
160
|
-
executeCommand('
|
|
131
|
+
executeCommand('docker restart postgres_proxy');
|
|
161
132
|
}
|
|
162
133
|
|
|
163
134
|
function createInitScript({ tenantId, password, databaseName }) {
|
|
164
|
-
if (!fs.existsSync(
|
|
165
|
-
fs.mkdirSync(
|
|
135
|
+
if (!fs.existsSync(INIT_DIR_PATH)) {
|
|
136
|
+
fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
|
|
166
137
|
}
|
|
167
138
|
|
|
168
|
-
const initFile = path.join(
|
|
139
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
169
140
|
const sql = `CREATE ROLE ${tenantId} WITH LOGIN PASSWORD '${password}' SUPERUSER CREATEDB CREATEROLE;
|
|
170
141
|
CREATE DATABASE ${databaseName} OWNER ${tenantId};
|
|
171
142
|
GRANT ALL PRIVILEGES ON DATABASE ${databaseName} TO ${tenantId};
|
|
@@ -217,6 +188,30 @@ function ensureNetwork(doc) {
|
|
|
217
188
|
}
|
|
218
189
|
}
|
|
219
190
|
|
|
191
|
+
function ensureDockerPrivilegies() {
|
|
192
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
193
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
execSync('sudo usermod -aG docker $USER && newgrp docker')
|
|
196
|
+
writeFileSync(SETUP_STATUS_PATH, 'container:ok')
|
|
197
|
+
console.log('All ready!')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function checkSetupStatus() {
|
|
201
|
+
try {
|
|
202
|
+
const content = readFileSync(SETUP_STATUS_PATH, 'utf8')
|
|
203
|
+
if (content === 'container:ok') {
|
|
204
|
+
return true
|
|
205
|
+
} else {
|
|
206
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
207
|
+
process.exit(0)
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.warn('Missing setup config. Please run "pgs setup"')
|
|
211
|
+
process.exit(0)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
220
215
|
function createTenant(tenantId, options = {}) {
|
|
221
216
|
const { version = '18', password, limits = DEFAULT_LIMITS } = options;
|
|
222
217
|
const doc = loadCompose();
|
|
@@ -255,7 +250,7 @@ function createTenant(tenantId, options = {}) {
|
|
|
255
250
|
updateHAProxyDependsOn();
|
|
256
251
|
|
|
257
252
|
// start the tenant
|
|
258
|
-
executeCommand(`
|
|
253
|
+
executeCommand(`docker compose up -d ${serviceName}`.trim());
|
|
259
254
|
|
|
260
255
|
console.log(`✓ Created tenant: ${tenantId}`);
|
|
261
256
|
console.log(` Service: ${serviceName}`);
|
|
@@ -315,7 +310,7 @@ function enableTenantAccess(tenantId) {
|
|
|
315
310
|
setTenantAccess(tenantId, true);
|
|
316
311
|
|
|
317
312
|
// Restart HAProxy
|
|
318
|
-
executeCommand('
|
|
313
|
+
executeCommand('docker restart postgres_proxy');
|
|
319
314
|
|
|
320
315
|
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
321
316
|
}
|
|
@@ -332,7 +327,7 @@ function disableTenantAccess(tenantId) {
|
|
|
332
327
|
setTenantAccess(tenantId, false);
|
|
333
328
|
|
|
334
329
|
// Restart HAProxy
|
|
335
|
-
executeCommand('
|
|
330
|
+
executeCommand('docker restart postgres_proxy');
|
|
336
331
|
|
|
337
332
|
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
338
333
|
}
|
|
@@ -352,7 +347,7 @@ function removeTenant(tenantId) {
|
|
|
352
347
|
delete doc.volumes[volumeName];
|
|
353
348
|
}
|
|
354
349
|
|
|
355
|
-
const initFile = path.join(
|
|
350
|
+
const initFile = path.join(INIT_DIR_PATH, `init-${tenantId}.sql`);
|
|
356
351
|
if (fs.existsSync(initFile)) {
|
|
357
352
|
fs.unlinkSync(initFile);
|
|
358
353
|
}
|
|
@@ -371,7 +366,7 @@ function removeTenant(tenantId) {
|
|
|
371
366
|
|
|
372
367
|
function executeCommand(command) {
|
|
373
368
|
try {
|
|
374
|
-
execSync(command, { stdio: 'inherit', cwd:
|
|
369
|
+
execSync(command, { stdio: 'inherit', cwd: CONFIG_DIR });
|
|
375
370
|
} catch (error) {
|
|
376
371
|
console.error(`Error executing: ${command}`);
|
|
377
372
|
process.exit(1);
|
|
@@ -389,6 +384,13 @@ program
|
|
|
389
384
|
.description('Manage PostgreSQL tenant instances')
|
|
390
385
|
.version('1.0.0');
|
|
391
386
|
|
|
387
|
+
program
|
|
388
|
+
.command('setup')
|
|
389
|
+
.description('Initial required setup')
|
|
390
|
+
.action(() => {
|
|
391
|
+
ensureDockerPrivilegies()
|
|
392
|
+
})
|
|
393
|
+
|
|
392
394
|
program
|
|
393
395
|
.command('create')
|
|
394
396
|
.description('Create a new PostgreSQL tenant instance')
|
|
@@ -399,7 +401,9 @@ program
|
|
|
399
401
|
.option('--cpu <cpu>', 'CPU limit (cores)', '0.5')
|
|
400
402
|
.option('--memory <memory>', 'Memory limit (MB)', '256')
|
|
401
403
|
.action((...args) => {
|
|
404
|
+
checkSetupStatus()
|
|
402
405
|
try {
|
|
406
|
+
|
|
403
407
|
const providedTenantId = args[0];
|
|
404
408
|
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
405
409
|
const tenantId = `${providedTenantId}_${randomSuffix}`;
|
|
@@ -411,17 +415,18 @@ program
|
|
|
411
415
|
version: null,
|
|
412
416
|
limits: null
|
|
413
417
|
}
|
|
414
|
-
|
|
418
|
+
|
|
415
419
|
if (opts.file) {
|
|
416
420
|
const manifestFilePath = path.resolve(opts.file)
|
|
417
421
|
const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
|
|
418
|
-
|
|
422
|
+
|
|
419
423
|
if (optsFromManifest.type !== 'postgres') {
|
|
420
424
|
throw new Error(`The type "${optsFromManifest.type}" is not valid for this agent.`)
|
|
421
425
|
}
|
|
422
426
|
console.log('optsFromManifest', optsFromManifest)
|
|
423
427
|
|
|
424
428
|
tenantOptions.limits = optsFromManifest.shared_limits
|
|
429
|
+
tenantOptions.version = optsFromManifest.version
|
|
425
430
|
} else {
|
|
426
431
|
tenantOptions.version = opts.version;
|
|
427
432
|
tenantOptions.limits = {
|
|
@@ -429,9 +434,9 @@ program
|
|
|
429
434
|
memory: parseInt(opts.memory, 10),
|
|
430
435
|
};
|
|
431
436
|
}
|
|
432
|
-
|
|
437
|
+
|
|
433
438
|
tenantOptions.password = opts.password || generateRandomPassword();
|
|
434
|
-
|
|
439
|
+
|
|
435
440
|
createTenant(tenantId, tenantOptions);
|
|
436
441
|
} catch (error) {
|
|
437
442
|
console.error(`Error: ${error}`);
|
|
@@ -443,6 +448,7 @@ program
|
|
|
443
448
|
.command('list')
|
|
444
449
|
.description('List all tenant instances')
|
|
445
450
|
.action(() => {
|
|
451
|
+
checkSetupStatus()
|
|
446
452
|
try {
|
|
447
453
|
listTenants();
|
|
448
454
|
} catch (error) {
|
|
@@ -456,6 +462,7 @@ program
|
|
|
456
462
|
.description('Remove a tenant instance')
|
|
457
463
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
458
464
|
.action((tenantId) => {
|
|
465
|
+
checkSetupStatus()
|
|
459
466
|
try {
|
|
460
467
|
removeTenant(tenantId);
|
|
461
468
|
} catch (error) {
|
|
@@ -469,8 +476,9 @@ program
|
|
|
469
476
|
.description('Start all PostgreSQL services')
|
|
470
477
|
.argument('[tenant-id]', 'Tenant identifier (optional, starts all if omitted)')
|
|
471
478
|
.action((tenantId) => {
|
|
479
|
+
checkSetupStatus()
|
|
472
480
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
473
|
-
executeCommand(`
|
|
481
|
+
executeCommand(`docker compose up -d ${service}`.trim());
|
|
474
482
|
});
|
|
475
483
|
|
|
476
484
|
program
|
|
@@ -478,9 +486,10 @@ program
|
|
|
478
486
|
.description('Stop all PostgreSQL services')
|
|
479
487
|
.argument('[tenant-id]', 'Tenant identifier (optional, stops all if omitted)')
|
|
480
488
|
.action((tenantId) => {
|
|
489
|
+
checkSetupStatus()
|
|
481
490
|
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
482
|
-
executeCommand(`
|
|
483
|
-
executeCommand('
|
|
491
|
+
executeCommand(`docker compose stop ${service}`.trim());
|
|
492
|
+
executeCommand('docker restart postgres_proxy');
|
|
484
493
|
});
|
|
485
494
|
|
|
486
495
|
program
|
|
@@ -488,6 +497,7 @@ program
|
|
|
488
497
|
.description('Enable external access for a tenant')
|
|
489
498
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
490
499
|
.action((tenantId) => {
|
|
500
|
+
checkSetupStatus()
|
|
491
501
|
try {
|
|
492
502
|
enableTenantAccess(tenantId);
|
|
493
503
|
} catch (error) {
|
|
@@ -501,6 +511,7 @@ program
|
|
|
501
511
|
.description('Disable external access for a tenant')
|
|
502
512
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
503
513
|
.action((tenantId) => {
|
|
514
|
+
checkSetupStatus()
|
|
504
515
|
try {
|
|
505
516
|
disableTenantAccess(tenantId);
|
|
506
517
|
} catch (error) {
|
package/package.json
CHANGED
package/manifest.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"type": "deploy",
|
|
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
|
-
}
|