@sysnee/pgs 0.1.7-rc.11 → 0.1.7-rc.12

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.
@@ -0,0 +1,282 @@
1
+ # Local Dev Environment — PGS
2
+
3
+ Simulates the production setup (Node.js + `pgs` CLI + Traefik + PostgreSQL containers) locally, without installing the package globally or needing real DNS/SSL certificates.
4
+
5
+ ## Overview
6
+
7
+ | Concern | Production | Local Dev |
8
+ |---|---|---|
9
+ | Domain | `pgs.br-sp-1.sysnee.com` | `pgs.local` |
10
+ | DNS | Real DNS wildcard A record | dnsmasq (`*.pgs.local → 127.0.0.1`) |
11
+ | TLS cert | Let's Encrypt wildcard | Self-signed wildcard CA |
12
+ | CLI | `pgs` (global npm install) | `node ./manager.js` (source directly) |
13
+ | Setup | `pgs setup` (certbot, installs Docker) | `dev-setup.sh` (manual bootstrap) |
14
+
15
+ ---
16
+
17
+ ## Prerequisites
18
+
19
+ - Docker & Docker Compose installed
20
+ - Node.js 24
21
+ - `dnsmasq` (install below)
22
+ - `openssl` (usually pre-installed)
23
+
24
+ ---
25
+
26
+ ## Step 1 — Make `DOMAIN` configurable in `manager.js`
27
+
28
+ Change line 20 from:
29
+
30
+ ```js
31
+ const DOMAIN = 'pgs.br-sp-1.sysnee.com';
32
+ ```
33
+
34
+ To:
35
+
36
+ ```js
37
+ const DOMAIN = process.env.PGS_DOMAIN || 'pgs.br-sp-1.sysnee.com';
38
+ ```
39
+
40
+ This is the only code change needed. Production behavior is unaffected (env var not set → falls back to real domain).
41
+
42
+ ---
43
+
44
+ ## Step 2 — Install and configure dnsmasq
45
+
46
+ dnsmasq resolves all `*.pgs.local` subdomains to `127.0.0.1` automatically, without needing to edit `/etc/hosts` per tenant.
47
+
48
+ ```bash
49
+ sudo apt-get install -y dnsmasq
50
+ ```
51
+
52
+ Add the wildcard rule:
53
+
54
+ ```bash
55
+ echo "address=/.pgs.local/127.0.0.1" | sudo tee /etc/dnsmasq.d/pgs-local.conf
56
+ sudo systemctl restart dnsmasq
57
+ ```
58
+
59
+ On systems using `systemd-resolved` (Ubuntu 20.04+), you need to tell it to use dnsmasq for `.local` queries. Check if it's active:
60
+
61
+ ```bash
62
+ systemctl is-active systemd-resolved
63
+ ```
64
+
65
+ If active, add to `/etc/systemd/resolved.conf`:
66
+
67
+ ```ini
68
+ [Resolve]
69
+ DNS=127.0.0.1
70
+ Domains=~local
71
+ ```
72
+
73
+ Then restart:
74
+
75
+ ```bash
76
+ sudo systemctl restart systemd-resolved
77
+ ```
78
+
79
+ Verify resolution works:
80
+
81
+ ```bash
82
+ dig tenant1.pgs.local @127.0.0.1
83
+ # or
84
+ nslookup tenant1.pgs.local 127.0.0.1
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Step 3 — Bootstrap `~/.sysnee-config/` (replaces `pgs setup`)
90
+
91
+ Create the file `pgs/dev-setup.sh`:
92
+
93
+ ```bash
94
+ #!/usr/bin/env bash
95
+ set -e
96
+
97
+ CONFIG_DIR="$HOME/.sysnee-config"
98
+ CERTS_DIR="$CONFIG_DIR/certs"
99
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
100
+
101
+ echo "── Bootstrapping ~/.sysnee-config/ ──"
102
+
103
+ mkdir -p "$CERTS_DIR"
104
+
105
+ # Copy template files from source
106
+ cp "$SCRIPT_DIR/docker-compose.yml" "$CONFIG_DIR/docker-compose.yml"
107
+ cp "$SCRIPT_DIR/traefik.yml" "$CONFIG_DIR/traefik.yml"
108
+
109
+ [ -f "$CONFIG_DIR/tenant-access.json" ] || echo '{}' > "$CONFIG_DIR/tenant-access.json"
110
+
111
+ # Write setup status so checkSetupStatus() passes
112
+ echo -n "container:ok" > "$CONFIG_DIR/status.txt"
113
+
114
+ echo "── Generating self-signed wildcard cert for *.pgs.local ──"
115
+
116
+ # CA
117
+ openssl genrsa -out "$CERTS_DIR/ca.key" 4096 2>/dev/null
118
+ openssl req -x509 -new -nodes \
119
+ -key "$CERTS_DIR/ca.key" \
120
+ -sha256 -days 825 \
121
+ -out "$CERTS_DIR/ca.pem" \
122
+ -subj "/CN=PGS Dev CA/O=Local Dev"
123
+
124
+ # Wildcard cert
125
+ openssl genrsa -out "$CERTS_DIR/privkey.pem" 4096 2>/dev/null
126
+ openssl req -new \
127
+ -key "$CERTS_DIR/privkey.pem" \
128
+ -out /tmp/pgs-dev.csr \
129
+ -subj "/CN=*.pgs.local"
130
+
131
+ # SAN required by modern TLS clients (Chrome, DBeaver, psql)
132
+ cat > /tmp/pgs-dev-ext.cnf <<EOF
133
+ subjectAltName=DNS:*.pgs.local,DNS:pgs.local
134
+ basicConstraints=CA:FALSE
135
+ keyUsage=digitalSignature,keyEncipherment
136
+ extendedKeyUsage=serverAuth
137
+ EOF
138
+
139
+ openssl x509 -req \
140
+ -in /tmp/pgs-dev.csr \
141
+ -CA "$CERTS_DIR/ca.pem" \
142
+ -CAkey "$CERTS_DIR/ca.key" \
143
+ -CAcreateserial \
144
+ -out "$CERTS_DIR/fullchain.pem" \
145
+ -days 825 \
146
+ -sha256 \
147
+ -extfile /tmp/pgs-dev-ext.cnf
148
+
149
+ echo "── Generating initial dynamic.yml ──"
150
+ cat > "$CONFIG_DIR/dynamic.yml" <<'EOF'
151
+ tcp:
152
+ routers: {}
153
+ services: {}
154
+ tls:
155
+ certificates:
156
+ - certFile: /etc/traefik/certs/fullchain.pem
157
+ keyFile: /etc/traefik/certs/privkey.pem
158
+ EOF
159
+
160
+ echo ""
161
+ echo "✓ Dev environment ready at $CONFIG_DIR"
162
+ echo ""
163
+ echo "Cert files:"
164
+ echo " CA: $CERTS_DIR/ca.pem ← import this into DBeaver / OS trust store"
165
+ echo " Cert: $CERTS_DIR/fullchain.pem"
166
+ echo " Key: $CERTS_DIR/privkey.pem"
167
+ echo ""
168
+ echo "Next: PGS_DOMAIN=pgs.local node ./manager.js create <tenant-id>"
169
+ ```
170
+
171
+ Make it executable:
172
+
173
+ ```bash
174
+ chmod +x pgs/dev-setup.sh
175
+ ```
176
+
177
+ Run once:
178
+
179
+ ```bash
180
+ cd pgs && ./dev-setup.sh
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Step 4 — Workflow: creating and starting a tenant
186
+
187
+ All commands use `node ./manager.js` directly from source. Changes are reflected instantly.
188
+
189
+ ```bash
190
+ # From the pgs/ directory
191
+ export PGS_DOMAIN=pgs.local
192
+
193
+ node ./manager.js create tenant1
194
+ node ./manager.js start tenant1
195
+
196
+ # Verify
197
+ node ./manager.js list
198
+ ```
199
+
200
+ No reinstall required between code changes.
201
+
202
+ ---
203
+
204
+ ## Step 5 — Connect from DBeaver
205
+
206
+ ### Trust the dev CA (once)
207
+
208
+ DBeaver needs to trust the self-signed CA to accept the TLS connection.
209
+
210
+ In DBeaver: **Window → Preferences → Connections → SSL** → add `~/.sysnee-config/certs/ca.pem` as a trusted CA.
211
+
212
+ Alternatively, add it to the system trust store (so any tool trusts it):
213
+
214
+ ```bash
215
+ sudo cp ~/.sysnee-config/certs/ca.pem /usr/local/share/ca-certificates/pgs-dev-ca.crt
216
+ sudo update-ca-certificates
217
+ ```
218
+
219
+ ### Connection settings
220
+
221
+ | Field | Value |
222
+ |---|---|
223
+ | Host | `tenant1.pgs.local` |
224
+ | Port | `5432` |
225
+ | Database | `tenant1` |
226
+ | Username | `postgres` |
227
+ | Password | (set at create time) |
228
+ | SSL mode | `require` |
229
+ | SSL CA cert | `~/.sysnee-config/certs/ca.pem` |
230
+
231
+ ### psql equivalent
232
+
233
+ ```bash
234
+ psql "postgresql://postgres:PASSWORD@tenant1.pgs.local:5432/tenant1?sslmode=require&sslrootcert=$HOME/.sysnee-config/certs/ca.pem"
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Teardown
240
+
241
+ ```bash
242
+ export PGS_DOMAIN=pgs.local
243
+
244
+ node ./manager.js stop
245
+ node ./manager.js remove tenant1
246
+
247
+ # Full reset (removes all config and containers)
248
+ docker compose -f ~/.sysnee-config/docker-compose.yml down -v
249
+ rm -rf ~/.sysnee-config
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Troubleshooting
255
+
256
+ **DNS not resolving**
257
+
258
+ ```bash
259
+ # Check dnsmasq is running
260
+ sudo systemctl status dnsmasq
261
+
262
+ # Test directly
263
+ dig +short tenant1.pgs.local @127.0.0.1
264
+ ```
265
+
266
+ **TLS handshake error in DBeaver / psql**
267
+
268
+ Make sure the CA cert is trusted and `sslrootcert` points to `ca.pem`, not `fullchain.pem`.
269
+
270
+ **`checkSetupStatus()` fails (exits with "Please run pgs setup")**
271
+
272
+ ```bash
273
+ echo -n "container:ok" > ~/.sysnee-config/status.txt
274
+ ```
275
+
276
+ **Port 5432 already in use**
277
+
278
+ A local PostgreSQL instance may be listening. Stop it temporarily:
279
+
280
+ ```bash
281
+ sudo systemctl stop postgresql
282
+ ```
package/manager.js CHANGED
@@ -8,6 +8,7 @@ import { Command } from 'commander';
8
8
  import { execSync } from 'child_process';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { dirname } from 'path';
11
+ import { createInterface } from 'readline';
11
12
 
12
13
  import packageJson from './package.json' with { type: 'json' };
13
14
 
@@ -16,6 +17,7 @@ const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = dirname(__filename);
17
18
 
18
19
  const CONFIG_DIR = path.join(os.homedir(), '.sysnee-config');
20
+ const DOMAIN = 'pgs.br-sp-1.sysnee.com';
19
21
 
20
22
  const COMPOSE_FILE_PATH = path.join(CONFIG_DIR, 'docker-compose.yml');
21
23
  const INIT_DIR_PATH = path.join(CONFIG_DIR, 'init');
@@ -114,7 +116,10 @@ function generateTraefikDynamicConfig(tenantsManifests = {}) {
114
116
 
115
117
  if (sourceRanges.length > 0) {
116
118
  router.middlewares = [middlewareName];
117
-
119
+
120
+ if (!dynamicConfig.tcp.middlewares) {
121
+ dynamicConfig.tcp.middlewares = {};
122
+ }
118
123
  if (!dynamicConfig.tcp.middlewares[middlewareName]) {
119
124
  dynamicConfig.tcp.middlewares[middlewareName] = {
120
125
  ipAllowList: {
@@ -230,15 +235,15 @@ function ensureNetwork(doc) {
230
235
  }
231
236
  }
232
237
 
233
- function initialSetup() {
238
+ async function initialSetup() {
234
239
  installDocker()
235
240
  createInitialFiles()
236
241
  ensureDockerPrivilegies()
242
+ await setupCertificates()
237
243
  console.log('All ready!')
238
244
  }
239
245
 
240
246
  function installDocker() {
241
-
242
247
  try {
243
248
  execSync('docker info', { stdio: 'pipe' })
244
249
  console.log('Docker already installed')
@@ -249,7 +254,7 @@ function installDocker() {
249
254
  execSync('curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh', { stdio: 'inherit' })
250
255
  execSync('rm get-docker.sh')
251
256
  console.log('Docker installed successfully')
252
-
257
+
253
258
  console.log('Installing Docker Compose plugin...')
254
259
  execSync('sudo apt-get install -y docker-compose-plugin', { stdio: 'inherit' })
255
260
  console.log('Docker Compose plugin installed successfully')
@@ -260,6 +265,107 @@ function ensureDockerPrivilegies() {
260
265
  writeFileSync(SETUP_STATUS_PATH, 'container:ok')
261
266
  }
262
267
 
268
+ function waitForEnter(message) {
269
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
270
+ return new Promise(resolve => {
271
+ rl.question(message, () => {
272
+ rl.close();
273
+ resolve();
274
+ });
275
+ });
276
+ }
277
+
278
+ function installCertbot() {
279
+ try {
280
+ execSync('certbot --version', { stdio: 'pipe' });
281
+ console.log('Certbot already installed');
282
+ return;
283
+ } catch (_) {}
284
+
285
+ console.log('Installing certbot...');
286
+ execSync('sudo apt-get install -y certbot', { stdio: 'inherit' });
287
+ console.log('Certbot installed successfully');
288
+ }
289
+
290
+ function findCertbotLivePath() {
291
+ const basePath = '/etc/letsencrypt/live';
292
+ const candidates = [
293
+ path.join(basePath, DOMAIN),
294
+ path.join(basePath, `*.${DOMAIN}`),
295
+ ];
296
+
297
+ try {
298
+ const dirs = fs.readdirSync(basePath);
299
+ for (const dir of dirs) {
300
+ const p = path.join(basePath, dir);
301
+ if (!candidates.includes(p)) candidates.push(p);
302
+ }
303
+ } catch (_) {}
304
+
305
+ for (const candidate of candidates) {
306
+ const fullchain = path.join(candidate, 'fullchain.pem');
307
+ const privkey = path.join(candidate, 'privkey.pem');
308
+ if (existsSync(fullchain) && existsSync(privkey)) {
309
+ return candidate;
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function copyCertificates(retries = 5, retryDelaySecs = 3) {
316
+ mkdirSync(CERTS_DIR_PATH, { recursive: true });
317
+
318
+ for (let attempt = 1; attempt <= retries; attempt++) {
319
+ const certPath = findCertbotLivePath();
320
+ if (certPath) {
321
+ const username = os.userInfo().username;
322
+ execSync(`sudo cp "${path.join(certPath, 'fullchain.pem')}" "${path.join(CERTS_DIR_PATH, 'fullchain.pem')}"`, { stdio: 'inherit' });
323
+ execSync(`sudo cp "${path.join(certPath, 'privkey.pem')}" "${path.join(CERTS_DIR_PATH, 'privkey.pem')}"`, { stdio: 'inherit' });
324
+ execSync(`sudo chown ${username}:${username} "${CERTS_DIR_PATH}/fullchain.pem" "${CERTS_DIR_PATH}/privkey.pem"`, { stdio: 'inherit' });
325
+ console.log(`✓ Certificates copied from ${certPath}`);
326
+ return;
327
+ }
328
+ if (attempt < retries) {
329
+ console.log(`Certificate path not found, retrying in ${retryDelaySecs}s... (${attempt}/${retries})`);
330
+ execSync(`sleep ${retryDelaySecs}`);
331
+ }
332
+ }
333
+ throw new Error('Could not find certbot certificates. Check /etc/letsencrypt/live/');
334
+ }
335
+
336
+ async function setupCertificates() {
337
+ installCertbot();
338
+
339
+ let serverIp = 'YOUR_SERVER_IP';
340
+ try {
341
+ serverIp = execSync('curl -s --max-time 5 ifconfig.me || hostname -I | awk \'{print $1}\'', { stdio: 'pipe' }).toString().trim();
342
+ } catch (_) {}
343
+
344
+ console.log('\n── DNS Setup Required ────────────────────────────');
345
+ console.log(` Add the following A records in your DNS provider:`);
346
+ console.log(` ${DOMAIN} → ${serverIp}`);
347
+ console.log(` *.${DOMAIN} → ${serverIp}`);
348
+ console.log('──────────────────────────────────────────────────\n');
349
+ await waitForEnter('Press Enter once the A records are set (propagation may take a few minutes)...\n');
350
+
351
+ console.log('Running certbot DNS-01 challenge...');
352
+ console.log(`Certbot will ask you to create a TXT record at _acme-challenge.${DOMAIN}`);
353
+ execSync(
354
+ `sudo certbot certonly --manual --preferred-challenges dns -d "*.${DOMAIN}" --agree-tos`,
355
+ { stdio: 'inherit' }
356
+ );
357
+
358
+ console.log('Copying certificates...');
359
+ copyCertificates();
360
+
361
+ console.log('Restarting Traefik...');
362
+ try {
363
+ execSync('docker restart postgres_proxy', { stdio: 'inherit' });
364
+ } catch (_) {
365
+ console.log('Traefik not running yet, skipping restart');
366
+ }
367
+ }
368
+
263
369
  function createInitialFiles() {
264
370
  // config dir
265
371
  if (!existsSync(CONFIG_DIR)) {
@@ -445,10 +551,21 @@ function removeTenant(tenantId, manifests = {}) {
445
551
  throw new Error(`Tenant ${tenantId} not found`);
446
552
  }
447
553
 
554
+ try {
555
+ execSync(`docker compose stop ${serviceName}`, { stdio: 'inherit', cwd: CONFIG_DIR });
556
+ } catch (err) {
557
+ console.warn(`Warning: failed to stop ${serviceName}: ${err.message}`);
558
+ }
559
+ try {
560
+ execSync(`docker compose rm -f ${serviceName}`, { stdio: 'inherit', cwd: CONFIG_DIR });
561
+ } catch (err) {
562
+ console.warn(`Warning: failed to remove container ${serviceName}: ${err.message}`);
563
+ }
564
+
448
565
  delete doc.services[serviceName];
449
566
 
450
567
  const volumeName = `pgdata_${tenantId}`;
451
- if (doc.volumes && doc.volumes[volumeName]) {
568
+ if (doc.volumes && Object.prototype.hasOwnProperty.call(doc.volumes, volumeName)) {
452
569
  delete doc.volumes[volumeName];
453
570
  }
454
571
 
@@ -463,9 +580,101 @@ function removeTenant(tenantId, manifests = {}) {
463
580
  generateTraefikDynamicConfig(manifests);
464
581
  updateTraefikService();
465
582
 
583
+ try {
584
+ const matching = execSync(
585
+ `docker volume ls --format '{{.Name}}' | grep -E '_${volumeName}$' || true`,
586
+ { stdio: 'pipe', cwd: CONFIG_DIR }
587
+ ).toString().trim();
588
+ if (matching) {
589
+ for (const vol of matching.split('\n')) {
590
+ if (vol.trim()) {
591
+ try {
592
+ execSync(`docker volume rm ${vol.trim()}`, { stdio: 'inherit', cwd: CONFIG_DIR });
593
+ } catch (err) {
594
+ console.warn(`Warning: failed to remove volume ${vol}: ${err.message}`);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ } catch (err) {
600
+ console.warn(`Warning: could not enumerate volumes: ${err.message}`);
601
+ }
602
+
466
603
  console.log(`✓ Removed tenant: ${tenantId}`);
467
- console.log(` Run: docker compose down ${serviceName}`);
468
- console.log(` Run: docker volume rm pgs_${volumeName}`);
604
+ }
605
+
606
+ function getTenantStatus(tenantId) {
607
+ const doc = loadCompose();
608
+ const serviceName = `pgs_${tenantId}`;
609
+ const exists = !!(doc.services && doc.services[serviceName]);
610
+
611
+ if (!exists) {
612
+ return { tenantId, exists: false, running: false };
613
+ }
614
+
615
+ try {
616
+ const inspect = JSON.parse(
617
+ execSync(`docker inspect ${serviceName}`, { stdio: 'pipe' }).toString()
618
+ );
619
+ const state = inspect[0]?.State || {};
620
+ return {
621
+ tenantId,
622
+ exists: true,
623
+ running: state.Running === true,
624
+ status: state.Status,
625
+ startedAt: state.StartedAt,
626
+ };
627
+ } catch (_) {
628
+ return { tenantId, exists: true, running: false, status: 'not-created' };
629
+ }
630
+ }
631
+
632
+ function showStatus() {
633
+ console.log('\n── Traefik Proxy ─────────────────────────────────');
634
+ try {
635
+ const inspect = JSON.parse(execSync('docker inspect postgres_proxy', { stdio: 'pipe' }).toString());
636
+ const state = inspect[0]?.State;
637
+ const running = state?.Running;
638
+ const status = state?.Status;
639
+ const startedAt = state?.StartedAt;
640
+ const exitCode = state?.ExitCode;
641
+
642
+ console.log(` Status: ${running ? '✓ running' : '✗ stopped'} (${status})`);
643
+ if (startedAt) console.log(` Started at: ${new Date(startedAt).toLocaleString()}`);
644
+ if (!running && exitCode !== undefined) console.log(` Exit code: ${exitCode}`);
645
+ } catch (_) {
646
+ console.log(' Status: container not found');
647
+ }
648
+
649
+ console.log('\n── Traefik Errors (last 100 lines) ───────────────');
650
+ try {
651
+ const logs = execSync('docker logs postgres_proxy --tail 100 2>&1', { stdio: 'pipe' }).toString();
652
+ const errorLines = logs
653
+ .split('\n')
654
+ .filter(line => /\b(ERR|error|WARN|warn)\b/i.test(line) && line.trim().length > 0);
655
+
656
+ if (errorLines.length === 0) {
657
+ console.log(' No errors or warnings found');
658
+ } else {
659
+ errorLines.forEach(line => console.log(` ${line}`));
660
+ }
661
+ } catch (_) {
662
+ console.log(' Could not read container logs');
663
+ }
664
+
665
+ console.log('\n── Certificates ──────────────────────────────────');
666
+ const fullchainPath = path.join(CERTS_DIR_PATH, 'fullchain.pem');
667
+ if (existsSync(fullchainPath)) {
668
+ try {
669
+ const certInfo = execSync(`openssl x509 -in "${fullchainPath}" -noout -enddate -subject 2>/dev/null`, { stdio: 'pipe' }).toString().trim();
670
+ certInfo.split('\n').forEach(line => console.log(` ${line}`));
671
+ } catch (_) {
672
+ console.log(' Could not read certificate info');
673
+ }
674
+ } else {
675
+ console.log(' ✗ fullchain.pem not found');
676
+ }
677
+ console.log('──────────────────────────────────────────────────\n');
469
678
  }
470
679
 
471
680
  function executeCommand(command) {
@@ -490,9 +699,33 @@ program
490
699
 
491
700
  program
492
701
  .command('setup')
493
- .description('Initial required setup')
494
- .action(() => {
495
- initialSetup()
702
+ .description('Initial required setup (installs Docker, certbot, configures SSL)')
703
+ .action(async () => {
704
+ await initialSetup()
705
+ })
706
+
707
+ program
708
+ .command('status')
709
+ .description('Show status of postgres_proxy container (or a tenant container if tenant-id is given)')
710
+ .argument('[tenant-id]', 'Tenant identifier (optional)')
711
+ .option('--json', 'Output tenant status as JSON (only with tenant-id)')
712
+ .action((tenantId, opts) => {
713
+ if (tenantId) {
714
+ checkSetupStatus()
715
+ try {
716
+ const info = getTenantStatus(tenantId);
717
+ if (opts.json) {
718
+ process.stdout.write(JSON.stringify(info));
719
+ return;
720
+ }
721
+ console.log(info);
722
+ } catch (error) {
723
+ console.error(`Error: ${error.message}`);
724
+ process.exit(1);
725
+ }
726
+ return;
727
+ }
728
+ showStatus()
496
729
  })
497
730
 
498
731
  program
@@ -509,9 +742,6 @@ program
509
742
  try {
510
743
 
511
744
  const providedTenantId = args[0];
512
- const randomSuffix = Math.random().toString(36).substring(2, 8);
513
- const tenantId = `${providedTenantId}-${randomSuffix}`;
514
-
515
745
  const opts = args[1];
516
746
 
517
747
  const tenantOptions = {
@@ -521,6 +751,8 @@ program
521
751
  manifest: null
522
752
  }
523
753
 
754
+ let tenantId;
755
+
524
756
  if (opts.file) {
525
757
  const manifestFilePath = path.resolve(opts.file)
526
758
  const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
@@ -533,15 +765,23 @@ program
533
765
  tenantOptions.limits = optsFromManifest.shared_limits
534
766
  tenantOptions.version = optsFromManifest.version
535
767
  tenantOptions.manifest = optsFromManifest
768
+ tenantOptions.password = opts.password || optsFromManifest.postgres?.password || generateRandomPassword();
769
+
770
+ tenantId = optsFromManifest.slug || providedTenantId;
771
+ if (!tenantId) {
772
+ throw new Error('Manifest must contain "slug" or tenant-id must be provided');
773
+ }
536
774
  } else {
537
775
  tenantOptions.version = opts.version;
538
776
  tenantOptions.limits = {
539
777
  cpu: parseFloat(opts.cpu),
540
778
  memory: parseInt(opts.memory, 10),
541
779
  };
542
- }
780
+ tenantOptions.password = opts.password || generateRandomPassword();
543
781
 
544
- tenantOptions.password = opts.password || generateRandomPassword();
782
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
783
+ tenantId = `${providedTenantId}-${randomSuffix}`;
784
+ }
545
785
 
546
786
  createTenant(tenantId, tenantOptions);
547
787
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sysnee/pgs",
3
- "version": "0.1.7-rc.11",
3
+ "version": "0.1.7-rc.12",
4
4
  "description": "Dynamic PostgreSQL service instance manager",
5
5
  "type": "module",
6
6
  "bin": {