@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 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 COMPOSE_FILE = path.join(process.cwd(), 'docker-compose.yml');
10
- const INIT_DIR = path.join(process.cwd(), 'init');
11
- const HAPROXY_CFG = path.join(process.cwd(), 'haproxy.cfg');
12
- const TENANT_ACCESS_FILE = path.join(process.cwd(), 'tenant-access.json');
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(COMPOSE_FILE)) {
16
- throw new Error('docker-compose.yml not found');
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(COMPOSE_FILE, 'utf8');
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(COMPOSE_FILE, content);
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(TENANT_ACCESS_FILE)) {
31
- fs.writeFileSync(TENANT_ACCESS_FILE, '{}');
41
+ if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
42
+ fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
32
43
  }
33
- const content = fs.readFileSync(TENANT_ACCESS_FILE, 'utf8');
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(TENANT_ACCESS_FILE, content);
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
- // Generate full HAProxy config
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
- 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
- `;
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
- fs.writeFileSync(HAPROXY_CFG, config);
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('sudo docker restart postgres_proxy');
144
+ executeCommand('docker restart postgres_proxy');
161
145
  }
162
146
 
163
147
  function createInitScript({ tenantId, password, databaseName }) {
164
- if (!fs.existsSync(INIT_DIR)) {
165
- fs.mkdirSync(INIT_DIR, { recursive: true });
148
+ if (!fs.existsSync(INIT_DIR_PATH)) {
149
+ fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
166
150
  }
167
151
 
168
- const initFile = path.join(INIT_DIR, `init-${tenantId}.sql`);
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(`sudo docker compose up -d ${serviceName}`.trim());
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('sudo docker restart postgres_proxy');
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('sudo docker restart postgres_proxy');
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(INIT_DIR, `init-${tenantId}.sql`);
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: process.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('1.0.0');
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(`sudo docker compose up -d ${service}`.trim());
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(`sudo docker compose stop ${service}`.trim());
483
- executeCommand('sudo docker restart postgres_proxy');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sysnee/pgs",
3
- "version": "0.1.3",
3
+ "version": "0.1.4-rc2",
4
4
  "description": "Dynamic PostgreSQL service instance manager",
5
5
  "type": "module",
6
6
  "bin": {
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
- }