@sysnee/pgs 0.1.0
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/README.md +314 -0
- package/docker-compose.yml +16 -0
- package/docs/ARCHITECTURE_RECOMMENDATIONS.md +480 -0
- package/docs/CRITICAL_REVIEW.md +748 -0
- package/docs/EXECUTIVE_SUMMARY.md +210 -0
- package/docs/PROJECT.md +250 -0
- package/haproxy-lua/pg-route.lua +177 -0
- package/manager.js +510 -0
- package/manifest.json +32 -0
- package/package.json +24 -0
package/manager.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs, { readFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import generateRandomPassword from '../common/helpers/password-generator.js';
|
|
9
|
+
|
|
10
|
+
const COMPOSE_FILE = path.join(process.cwd(), 'docker-compose.yml');
|
|
11
|
+
const INIT_DIR = path.join(process.cwd(), 'init');
|
|
12
|
+
const HAPROXY_CFG = path.join(process.cwd(), 'haproxy.cfg');
|
|
13
|
+
const TENANT_ACCESS_FILE = path.join(process.cwd(), 'tenant-access.json');
|
|
14
|
+
|
|
15
|
+
function loadCompose() {
|
|
16
|
+
if (!fs.existsSync(COMPOSE_FILE)) {
|
|
17
|
+
throw new Error('docker-compose.yml not found');
|
|
18
|
+
}
|
|
19
|
+
const content = fs.readFileSync(COMPOSE_FILE, 'utf8');
|
|
20
|
+
return yaml.load(content);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function saveCompose(doc) {
|
|
24
|
+
const content = yaml.dump(doc, { indent: 2, lineWidth: -1 });
|
|
25
|
+
fs.writeFileSync(COMPOSE_FILE, content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ==================== Tenant Access Management ====================
|
|
29
|
+
|
|
30
|
+
function loadTenantAccess() {
|
|
31
|
+
if (!fs.existsSync(TENANT_ACCESS_FILE)) {
|
|
32
|
+
fs.writeFileSync(TENANT_ACCESS_FILE, '{}');
|
|
33
|
+
}
|
|
34
|
+
const content = fs.readFileSync(TENANT_ACCESS_FILE, 'utf8');
|
|
35
|
+
return JSON.parse(content) || {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function saveTenantAccess(access) {
|
|
39
|
+
const content = JSON.stringify(access, null, 2);
|
|
40
|
+
fs.writeFileSync(TENANT_ACCESS_FILE, content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setTenantAccess(tenantId, enabled) {
|
|
44
|
+
const access = loadTenantAccess();
|
|
45
|
+
access[tenantId] = enabled;
|
|
46
|
+
saveTenantAccess(access);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeTenantAccess(tenantId) {
|
|
50
|
+
const access = loadTenantAccess();
|
|
51
|
+
delete access[tenantId];
|
|
52
|
+
saveTenantAccess(access);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ==================== HAProxy Configuration ====================
|
|
56
|
+
|
|
57
|
+
function generateHAProxyConfig() {
|
|
58
|
+
const doc = loadCompose();
|
|
59
|
+
|
|
60
|
+
// Get all tenant services
|
|
61
|
+
const tenants = Object.keys(doc.services || {})
|
|
62
|
+
.filter(name => name.startsWith('pgs_') && name !== 'haproxy');
|
|
63
|
+
|
|
64
|
+
// Generate full HAProxy config
|
|
65
|
+
let config = `global
|
|
66
|
+
log stdout format raw local0 info
|
|
67
|
+
maxconn 4096
|
|
68
|
+
lua-load /etc/haproxy/lua/pg-route.lua
|
|
69
|
+
|
|
70
|
+
defaults
|
|
71
|
+
log global
|
|
72
|
+
mode tcp
|
|
73
|
+
option tcplog
|
|
74
|
+
timeout connect 5s
|
|
75
|
+
timeout client 30s
|
|
76
|
+
timeout server 30s
|
|
77
|
+
retries 3
|
|
78
|
+
|
|
79
|
+
frontend postgres_frontend
|
|
80
|
+
bind *:5432
|
|
81
|
+
mode tcp
|
|
82
|
+
|
|
83
|
+
# Use Lua script to parse PostgreSQL startup packet and route by username
|
|
84
|
+
tcp-request inspect-delay 5s
|
|
85
|
+
tcp-request content lua.pg_route
|
|
86
|
+
tcp-request content reject if { var(txn.pg_blocked) -m bool }
|
|
87
|
+
|
|
88
|
+
# Route to backend based on username extracted by Lua
|
|
89
|
+
use_backend %[var(txn.pg_backend)] if { var(txn.pg_backend) -m found }
|
|
90
|
+
|
|
91
|
+
# Default backend for unknown connections
|
|
92
|
+
default_backend pg_reject
|
|
93
|
+
|
|
94
|
+
backend pg_reject
|
|
95
|
+
mode tcp
|
|
96
|
+
timeout server 1s
|
|
97
|
+
|
|
98
|
+
backend pg_ssl_pool
|
|
99
|
+
mode tcp
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
// Add all tenant backends to SSL pool for SSL negotiation
|
|
103
|
+
// PostgreSQL will respond 'N' (no SSL) during negotiation, then client retries without SSL
|
|
104
|
+
if (tenants.length > 0) {
|
|
105
|
+
for (const serviceName of tenants) {
|
|
106
|
+
config += ` server ${serviceName}_ssl ${serviceName}:5432 check
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
config += `
|
|
110
|
+
`;
|
|
111
|
+
} else {
|
|
112
|
+
config += ` # No tenants configured yet
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generate backend for each tenant
|
|
117
|
+
for (const serviceName of tenants) {
|
|
118
|
+
config += `backend ${serviceName}
|
|
119
|
+
mode tcp
|
|
120
|
+
server pg1 ${serviceName}:5432 check
|
|
121
|
+
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(HAPROXY_CFG, config);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function updateHAProxyDependsOn() {
|
|
129
|
+
const doc = loadCompose();
|
|
130
|
+
|
|
131
|
+
// Get all tenant services
|
|
132
|
+
const tenants = Object.keys(doc.services || {})
|
|
133
|
+
.filter(name => name.startsWith('pgs_') && name !== 'haproxy');
|
|
134
|
+
|
|
135
|
+
// Ensure HAProxy service exists
|
|
136
|
+
if (!doc.services.haproxy) {
|
|
137
|
+
doc.services.haproxy = {
|
|
138
|
+
image: 'haproxy:latest',
|
|
139
|
+
container_name: 'postgres_proxy',
|
|
140
|
+
ports: ['5432:5432'],
|
|
141
|
+
volumes: [
|
|
142
|
+
'./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro',
|
|
143
|
+
'./haproxy-lua:/etc/haproxy/lua:ro',
|
|
144
|
+
'./tenant-access.json:/etc/haproxy/tenant-access.json:ro'
|
|
145
|
+
],
|
|
146
|
+
networks: ['postgres_network'],
|
|
147
|
+
restart: 'unless-stopped'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update depends_on
|
|
152
|
+
if (tenants.length > 0) {
|
|
153
|
+
doc.services.haproxy.depends_on = tenants;
|
|
154
|
+
} else {
|
|
155
|
+
delete doc.services.haproxy.depends_on;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
saveCompose(doc);
|
|
159
|
+
|
|
160
|
+
// Restart HAProxy
|
|
161
|
+
executeCommand('sudo docker restart postgres_proxy');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createInitScript({ tenantId, password, databaseName }) {
|
|
165
|
+
if (!fs.existsSync(INIT_DIR)) {
|
|
166
|
+
fs.mkdirSync(INIT_DIR, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const initFile = path.join(INIT_DIR, `init-${tenantId}.sql`);
|
|
170
|
+
const sql = `CREATE ROLE ${tenantId} WITH LOGIN PASSWORD '${password}' SUPERUSER CREATEDB CREATEROLE;
|
|
171
|
+
CREATE DATABASE ${databaseName} OWNER ${tenantId};
|
|
172
|
+
GRANT ALL PRIVILEGES ON DATABASE ${databaseName} TO ${tenantId};
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
fs.writeFileSync(initFile, sql);
|
|
176
|
+
return initFile;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const DEFAULT_LIMITS = { cpu: 0.5, memory: 256 };
|
|
180
|
+
|
|
181
|
+
function createService({ tenantId, password, databaseName, version = '18', limits = DEFAULT_LIMITS }) {
|
|
182
|
+
const containerName = `pgs_${tenantId}`;
|
|
183
|
+
const volumeName = `pgdata_${tenantId}`;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
image: `postgres:${version}`,
|
|
187
|
+
container_name: containerName,
|
|
188
|
+
environment: {
|
|
189
|
+
POSTGRES_PASSWORD: password,
|
|
190
|
+
POSTGRES_DB: databaseName,
|
|
191
|
+
},
|
|
192
|
+
expose: ['5432'],
|
|
193
|
+
volumes: [
|
|
194
|
+
`${volumeName}:/var/lib/postgresql`,
|
|
195
|
+
`./init/init-${tenantId}.sql:/docker-entrypoint-initdb.d/init-${tenantId}.sql:ro`,
|
|
196
|
+
],
|
|
197
|
+
networks: ['postgres_network'],
|
|
198
|
+
restart: 'unless-stopped',
|
|
199
|
+
deploy: {
|
|
200
|
+
resources: {
|
|
201
|
+
limits: {
|
|
202
|
+
cpus: String(limits.cpu),
|
|
203
|
+
memory: `${limits.memory}M`,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ensureNetwork(doc) {
|
|
211
|
+
if (!doc.networks) {
|
|
212
|
+
doc.networks = {};
|
|
213
|
+
}
|
|
214
|
+
if (!doc.networks.postgres_network) {
|
|
215
|
+
doc.networks.postgres_network = {
|
|
216
|
+
driver: 'bridge'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createTenant(tenantId, options = {}) {
|
|
222
|
+
const { version = '18', password, limits = DEFAULT_LIMITS } = options;
|
|
223
|
+
const doc = loadCompose();
|
|
224
|
+
|
|
225
|
+
if (doc.services && doc.services[`pgs_${tenantId}`]) {
|
|
226
|
+
throw new Error(`Tenant ${tenantId} already exists`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!doc.services) {
|
|
230
|
+
doc.services = {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!doc.volumes) {
|
|
234
|
+
doc.volumes = {};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
createInitScript({ tenantId, password, databaseName: tenantId });
|
|
238
|
+
|
|
239
|
+
const serviceName = `pgs_${tenantId}`;
|
|
240
|
+
doc.services[serviceName] = createService({ tenantId, password, databaseName: tenantId, version, limits });
|
|
241
|
+
|
|
242
|
+
const volumeName = `pgdata_${tenantId}`;
|
|
243
|
+
if (!doc.volumes[volumeName]) {
|
|
244
|
+
doc.volumes[volumeName] = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ensureNetwork(doc);
|
|
248
|
+
|
|
249
|
+
saveCompose(doc);
|
|
250
|
+
|
|
251
|
+
// Add tenant to access control (disabled by default for security)
|
|
252
|
+
setTenantAccess(tenantId, true);
|
|
253
|
+
|
|
254
|
+
// Regenerate HAProxy config and update depends_on
|
|
255
|
+
generateHAProxyConfig();
|
|
256
|
+
updateHAProxyDependsOn();
|
|
257
|
+
|
|
258
|
+
// start the tenant
|
|
259
|
+
executeCommand(`sudo docker compose up -d ${serviceName}`.trim());
|
|
260
|
+
|
|
261
|
+
console.log(`✓ Created tenant: ${tenantId}`);
|
|
262
|
+
console.log(` Service: ${serviceName}`);
|
|
263
|
+
console.log(` Port: 5432`);
|
|
264
|
+
console.log(` Database: ${tenantId}`);
|
|
265
|
+
console.log(` Password: ${password}`);
|
|
266
|
+
console.log(` Access: enabled (use 'disable-access ${tenantId}' to disable)`);
|
|
267
|
+
|
|
268
|
+
return { tenantId, serviceName };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function listTenants() {
|
|
272
|
+
const doc = loadCompose();
|
|
273
|
+
const access = loadTenantAccess();
|
|
274
|
+
|
|
275
|
+
if (!doc.services) {
|
|
276
|
+
console.log('No tenants found');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const tenants = Object.keys(doc.services)
|
|
281
|
+
.filter(name => name.startsWith('pgs_') && name !== 'haproxy')
|
|
282
|
+
.map(name => {
|
|
283
|
+
const service = doc.services[name];
|
|
284
|
+
const db = service.environment?.POSTGRES_DB || 'N/A';
|
|
285
|
+
const tenantId = name.replace('pgs_', '');
|
|
286
|
+
const accessEnabled = access[tenantId] === true;
|
|
287
|
+
|
|
288
|
+
return { tenantId, db, service: name, access: accessEnabled };
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (tenants.length === 0) {
|
|
292
|
+
console.log('No tenants found');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('\nTenants:');
|
|
297
|
+
console.log('─'.repeat(75));
|
|
298
|
+
console.log(` ${'ID'.padEnd(20)} ${'Database'.padEnd(20)} ${'Access'.padEnd(15)}`);
|
|
299
|
+
console.log('─'.repeat(75));
|
|
300
|
+
tenants.forEach(t => {
|
|
301
|
+
const accessStr = t.access ? '✓ enabled' : '✗ disabled';
|
|
302
|
+
console.log(` ${t.tenantId.padEnd(20)} ${t.db.padEnd(20)} ${accessStr}`);
|
|
303
|
+
});
|
|
304
|
+
console.log('─'.repeat(75));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function enableTenantAccess(tenantId) {
|
|
308
|
+
const doc = loadCompose();
|
|
309
|
+
const serviceName = `pgs_${tenantId}`;
|
|
310
|
+
|
|
311
|
+
if (!doc.services || !doc.services[serviceName]) {
|
|
312
|
+
throw new Error(`Tenant ${tenantId} not found`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Update access control
|
|
316
|
+
setTenantAccess(tenantId, true);
|
|
317
|
+
|
|
318
|
+
// Restart HAProxy
|
|
319
|
+
executeCommand('sudo docker restart postgres_proxy');
|
|
320
|
+
|
|
321
|
+
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function disableTenantAccess(tenantId) {
|
|
325
|
+
const doc = loadCompose();
|
|
326
|
+
const serviceName = `pgs_${tenantId}`;
|
|
327
|
+
|
|
328
|
+
if (!doc.services || !doc.services[serviceName]) {
|
|
329
|
+
throw new Error(`Tenant ${tenantId} not found`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Update access control
|
|
333
|
+
setTenantAccess(tenantId, false);
|
|
334
|
+
|
|
335
|
+
// Restart HAProxy
|
|
336
|
+
executeCommand('sudo docker restart postgres_proxy');
|
|
337
|
+
|
|
338
|
+
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function removeTenant(tenantId) {
|
|
342
|
+
const doc = loadCompose();
|
|
343
|
+
const serviceName = `pgs_${tenantId}`;
|
|
344
|
+
|
|
345
|
+
if (!doc.services || !doc.services[serviceName]) {
|
|
346
|
+
throw new Error(`Tenant ${tenantId} not found`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
delete doc.services[serviceName];
|
|
350
|
+
|
|
351
|
+
const volumeName = `pgdata_${tenantId}`;
|
|
352
|
+
if (doc.volumes && doc.volumes[volumeName]) {
|
|
353
|
+
delete doc.volumes[volumeName];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const initFile = path.join(INIT_DIR, `init-${tenantId}.sql`);
|
|
357
|
+
if (fs.existsSync(initFile)) {
|
|
358
|
+
fs.unlinkSync(initFile);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
saveCompose(doc);
|
|
362
|
+
|
|
363
|
+
// Remove from tenant access and regenerate HAProxy config
|
|
364
|
+
removeTenantAccess(tenantId);
|
|
365
|
+
generateHAProxyConfig();
|
|
366
|
+
updateHAProxyDependsOn();
|
|
367
|
+
|
|
368
|
+
console.log(`✓ Removed tenant: ${tenantId}`);
|
|
369
|
+
console.log(` Run: docker compose down ${serviceName}`);
|
|
370
|
+
console.log(` Run: docker volume rm pgs_${volumeName}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function executeCommand(command) {
|
|
374
|
+
try {
|
|
375
|
+
execSync(command, { stdio: 'inherit', cwd: process.cwd() });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error(`Error executing: ${command}`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const program = new Command();
|
|
383
|
+
|
|
384
|
+
program
|
|
385
|
+
.name('postgres-manager')
|
|
386
|
+
.description('Manage PostgreSQL tenant instances')
|
|
387
|
+
.version('1.0.0');
|
|
388
|
+
|
|
389
|
+
program
|
|
390
|
+
.command('create')
|
|
391
|
+
.description('Create a new PostgreSQL tenant instance')
|
|
392
|
+
.argument('<tenant-id>', 'Tenant identifier')
|
|
393
|
+
.option('-f, --file <file>', 'Manifest file path')
|
|
394
|
+
.option('-p, --password <password>', 'Database password')
|
|
395
|
+
.option('-v, --version <version>', 'PostgreSQL version', '18')
|
|
396
|
+
.option('--cpu <cpu>', 'CPU limit (cores)', '0.5')
|
|
397
|
+
.option('--memory <memory>', 'Memory limit (MB)', '256')
|
|
398
|
+
.action((...args) => {
|
|
399
|
+
try {
|
|
400
|
+
const providedTenantId = args[0];
|
|
401
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
402
|
+
const tenantId = `${providedTenantId}_${randomSuffix}`;
|
|
403
|
+
|
|
404
|
+
const opts = args[1];
|
|
405
|
+
|
|
406
|
+
const tenantOptions = {
|
|
407
|
+
password: null,
|
|
408
|
+
version: null,
|
|
409
|
+
limits: null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (opts.file) {
|
|
413
|
+
const manifestFilePath = path.resolve(opts.file)
|
|
414
|
+
const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
|
|
415
|
+
|
|
416
|
+
if (optsFromManifest.type !== 'postgres') {
|
|
417
|
+
throw new Error(`The type "${optsFromManifest.type}" is not valid for this agent.`)
|
|
418
|
+
}
|
|
419
|
+
console.log('optsFromManifest', optsFromManifest)
|
|
420
|
+
|
|
421
|
+
tenantOptions.limits = optsFromManifest.shared_limits
|
|
422
|
+
} else {
|
|
423
|
+
tenantOptions.version = opts.version;
|
|
424
|
+
tenantOptions.limits = {
|
|
425
|
+
cpu: parseFloat(opts.cpu),
|
|
426
|
+
memory: parseInt(opts.memory, 10),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
tenantOptions.password = opts.password || generateRandomPassword();
|
|
431
|
+
|
|
432
|
+
createTenant(tenantId, tenantOptions);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`Error: ${error}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
program
|
|
440
|
+
.command('list')
|
|
441
|
+
.description('List all tenant instances')
|
|
442
|
+
.action(() => {
|
|
443
|
+
try {
|
|
444
|
+
listTenants();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(`Error: ${error.message}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
program
|
|
452
|
+
.command('remove')
|
|
453
|
+
.description('Remove a tenant instance')
|
|
454
|
+
.argument('<tenant-id>', 'Tenant identifier')
|
|
455
|
+
.action((tenantId) => {
|
|
456
|
+
try {
|
|
457
|
+
removeTenant(tenantId);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error(`Error: ${error.message}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
program
|
|
465
|
+
.command('start')
|
|
466
|
+
.description('Start all PostgreSQL services')
|
|
467
|
+
.argument('[tenant-id]', 'Tenant identifier (optional, starts all if omitted)')
|
|
468
|
+
.action((tenantId) => {
|
|
469
|
+
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
470
|
+
executeCommand(`sudo docker compose up -d ${service}`.trim());
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
program
|
|
474
|
+
.command('stop')
|
|
475
|
+
.description('Stop all PostgreSQL services')
|
|
476
|
+
.argument('[tenant-id]', 'Tenant identifier (optional, stops all if omitted)')
|
|
477
|
+
.action((tenantId) => {
|
|
478
|
+
const service = tenantId ? `pgs_${tenantId}` : '';
|
|
479
|
+
executeCommand(`sudo docker compose stop ${service}`.trim());
|
|
480
|
+
executeCommand('sudo docker restart postgres_proxy');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
program
|
|
484
|
+
.command('enable-access')
|
|
485
|
+
.description('Enable external access for a tenant')
|
|
486
|
+
.argument('<tenant-id>', 'Tenant identifier')
|
|
487
|
+
.action((tenantId) => {
|
|
488
|
+
try {
|
|
489
|
+
enableTenantAccess(tenantId);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error(`Error: ${error.message}`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
program
|
|
497
|
+
.command('disable-access')
|
|
498
|
+
.description('Disable external access for a tenant')
|
|
499
|
+
.argument('<tenant-id>', 'Tenant identifier')
|
|
500
|
+
.action((tenantId) => {
|
|
501
|
+
try {
|
|
502
|
+
disableTenantAccess(tenantId);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error(`Error: ${error.message}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
program.parse();
|
|
510
|
+
|
package/manifest.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sysnee/pgs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dynamic PostgreSQL service instance manager",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pgs": "./manager.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"create": "node manager.js create",
|
|
11
|
+
"list": "node manager.js list",
|
|
12
|
+
"remove": "node manager.js remove",
|
|
13
|
+
"start": "node manager.js start",
|
|
14
|
+
"stop": "node manager.js stop",
|
|
15
|
+
"enable-access": "node manager.js enable-access",
|
|
16
|
+
"disable-access": "node manager.js disable-access",
|
|
17
|
+
"reload-haproxy": "node manager.js reload-haproxy"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"js-yaml": "^4.1.0",
|
|
21
|
+
"commander": "^11.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|