@sysnee/pgs 0.1.2 → 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 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 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');
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(COMPOSE_FILE, content);
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(TENANT_ACCESS_FILE)) {
31
- fs.writeFileSync(TENANT_ACCESS_FILE, '{}');
36
+ if (!fs.existsSync(TENANT_ACCESS_FILE_PATH)) {
37
+ fs.writeFileSync(TENANT_ACCESS_FILE_PATH, '{}');
32
38
  }
33
- const content = fs.readFileSync(TENANT_ACCESS_FILE, 'utf8');
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(TENANT_ACCESS_FILE, content);
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
- // 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
70
+ console.debug(`tenants.lenght: ${tenants.length}`)
68
71
 
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
- `;
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
- fs.writeFileSync(HAPROXY_CFG, config);
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('sudo docker restart postgres_proxy');
139
+ executeCommand('docker restart postgres_proxy');
161
140
  }
162
141
 
163
142
  function createInitScript({ tenantId, password, databaseName }) {
164
- if (!fs.existsSync(INIT_DIR)) {
165
- fs.mkdirSync(INIT_DIR, { recursive: true });
143
+ if (!fs.existsSync(INIT_DIR_PATH)) {
144
+ fs.mkdirSync(INIT_DIR_PATH, { recursive: true });
166
145
  }
167
146
 
168
- const initFile = path.join(INIT_DIR, `init-${tenantId}.sql`);
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(`sudo docker compose up -d ${serviceName}`.trim());
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('sudo docker restart postgres_proxy');
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('sudo docker restart postgres_proxy');
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(INIT_DIR, `init-${tenantId}.sql`);
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: process.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.0.0');
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(`sudo docker compose up -d ${service}`.trim());
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(`sudo docker compose stop ${service}`.trim());
483
- executeCommand('sudo docker restart postgres_proxy');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sysnee/pgs",
3
- "version": "0.1.2",
3
+ "version": "0.1.4-rc1",
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
- }