@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 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 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');
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(COMPOSE_FILE)) {
16
- throw new Error('docker-compose.yml not found');
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(COMPOSE_FILE, 'utf8');
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(COMPOSE_FILE, content);
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(TENANT_ACCESS_FILE)) {
31
- fs.writeFileSync(TENANT_ACCESS_FILE, '{}');
35
+ if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
36
+ fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
32
37
  }
33
- const content = fs.readFileSync(TENANT_ACCESS_FILE, 'utf8');
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(TENANT_ACCESS_FILE, content);
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
- // 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
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(HAPROXY_CFG, config);
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('sudo docker restart postgres_proxy');
131
+ executeCommand('docker restart postgres_proxy');
161
132
  }
162
133
 
163
134
  function createInitScript({ tenantId, password, databaseName }) {
164
- if (!fs.existsSync(INIT_DIR)) {
165
- fs.mkdirSync(INIT_DIR, { recursive: true });
135
+ if (!fs.existsSync(INIT_DIR_PATH)) {
136
+ fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
166
137
  }
167
138
 
168
- const initFile = path.join(INIT_DIR, `init-${tenantId}.sql`);
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(`sudo docker compose up -d ${serviceName}`.trim());
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('sudo docker restart postgres_proxy');
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('sudo docker restart postgres_proxy');
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(INIT_DIR, `init-${tenantId}.sql`);
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: process.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(`sudo docker compose up -d ${service}`.trim());
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(`sudo docker compose stop ${service}`.trim());
483
- executeCommand('sudo docker restart postgres_proxy');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sysnee/pgs",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": "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
- }