@sysnee/pgs 0.1.7-rc.10 → 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');
@@ -75,7 +77,6 @@ function generateTraefikDynamicConfig(tenantsManifests = {}) {
75
77
  const dynamicConfig = {
76
78
  tcp: {
77
79
  routers: {},
78
- middlewares: {},
79
80
  services: {}
80
81
  },
81
82
  tls: {
@@ -115,7 +116,10 @@ function generateTraefikDynamicConfig(tenantsManifests = {}) {
115
116
 
116
117
  if (sourceRanges.length > 0) {
117
118
  router.middlewares = [middlewareName];
118
-
119
+
120
+ if (!dynamicConfig.tcp.middlewares) {
121
+ dynamicConfig.tcp.middlewares = {};
122
+ }
119
123
  if (!dynamicConfig.tcp.middlewares[middlewareName]) {
120
124
  dynamicConfig.tcp.middlewares[middlewareName] = {
121
125
  ipAllowList: {
@@ -231,15 +235,15 @@ function ensureNetwork(doc) {
231
235
  }
232
236
  }
233
237
 
234
- function initialSetup() {
238
+ async function initialSetup() {
235
239
  installDocker()
236
240
  createInitialFiles()
237
241
  ensureDockerPrivilegies()
242
+ await setupCertificates()
238
243
  console.log('All ready!')
239
244
  }
240
245
 
241
246
  function installDocker() {
242
-
243
247
  try {
244
248
  execSync('docker info', { stdio: 'pipe' })
245
249
  console.log('Docker already installed')
@@ -250,7 +254,7 @@ function installDocker() {
250
254
  execSync('curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh', { stdio: 'inherit' })
251
255
  execSync('rm get-docker.sh')
252
256
  console.log('Docker installed successfully')
253
-
257
+
254
258
  console.log('Installing Docker Compose plugin...')
255
259
  execSync('sudo apt-get install -y docker-compose-plugin', { stdio: 'inherit' })
256
260
  console.log('Docker Compose plugin installed successfully')
@@ -261,6 +265,107 @@ function ensureDockerPrivilegies() {
261
265
  writeFileSync(SETUP_STATUS_PATH, 'container:ok')
262
266
  }
263
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
+
264
369
  function createInitialFiles() {
265
370
  // config dir
266
371
  if (!existsSync(CONFIG_DIR)) {
@@ -446,10 +551,21 @@ function removeTenant(tenantId, manifests = {}) {
446
551
  throw new Error(`Tenant ${tenantId} not found`);
447
552
  }
448
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
+
449
565
  delete doc.services[serviceName];
450
566
 
451
567
  const volumeName = `pgdata_${tenantId}`;
452
- if (doc.volumes && doc.volumes[volumeName]) {
568
+ if (doc.volumes && Object.prototype.hasOwnProperty.call(doc.volumes, volumeName)) {
453
569
  delete doc.volumes[volumeName];
454
570
  }
455
571
 
@@ -464,9 +580,101 @@ function removeTenant(tenantId, manifests = {}) {
464
580
  generateTraefikDynamicConfig(manifests);
465
581
  updateTraefikService();
466
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
+
467
603
  console.log(`✓ Removed tenant: ${tenantId}`);
468
- console.log(` Run: docker compose down ${serviceName}`);
469
- 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');
470
678
  }
471
679
 
472
680
  function executeCommand(command) {
@@ -491,9 +699,33 @@ program
491
699
 
492
700
  program
493
701
  .command('setup')
494
- .description('Initial required setup')
495
- .action(() => {
496
- 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()
497
729
  })
498
730
 
499
731
  program
@@ -510,9 +742,6 @@ program
510
742
  try {
511
743
 
512
744
  const providedTenantId = args[0];
513
- const randomSuffix = Math.random().toString(36).substring(2, 8);
514
- const tenantId = `${providedTenantId}-${randomSuffix}`;
515
-
516
745
  const opts = args[1];
517
746
 
518
747
  const tenantOptions = {
@@ -522,6 +751,8 @@ program
522
751
  manifest: null
523
752
  }
524
753
 
754
+ let tenantId;
755
+
525
756
  if (opts.file) {
526
757
  const manifestFilePath = path.resolve(opts.file)
527
758
  const optsFromManifest = JSON.parse(readFileSync(manifestFilePath))
@@ -534,15 +765,23 @@ program
534
765
  tenantOptions.limits = optsFromManifest.shared_limits
535
766
  tenantOptions.version = optsFromManifest.version
536
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
+ }
537
774
  } else {
538
775
  tenantOptions.version = opts.version;
539
776
  tenantOptions.limits = {
540
777
  cpu: parseFloat(opts.cpu),
541
778
  memory: parseInt(opts.memory, 10),
542
779
  };
543
- }
780
+ tenantOptions.password = opts.password || generateRandomPassword();
544
781
 
545
- tenantOptions.password = opts.password || generateRandomPassword();
782
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
783
+ tenantId = `${providedTenantId}-${randomSuffix}`;
784
+ }
546
785
 
547
786
  createTenant(tenantId, tenantOptions);
548
787
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sysnee/pgs",
3
- "version": "0.1.7-rc.10",
3
+ "version": "0.1.7-rc.12",
4
4
  "description": "Dynamic PostgreSQL service instance manager",
5
5
  "type": "module",
6
6
  "bin": {