go-duck-cli 1.3.0 → 1.3.3

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.
@@ -5,6 +5,9 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
5
5
  const aiDocsDir = path.join(outputDir, 'docs', 'ai');
6
6
  await fs.ensureDir(aiDocsDir);
7
7
 
8
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
9
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
10
+
8
11
  // 1. ARCHITECTURE.md
9
12
  const appName = config.name || 'go-duck-app';
10
13
  const hasMongo = config.datasource?.mongodb?.enabled;
@@ -32,7 +35,7 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
32
35
  archContent += `- **Active**: ${storageActive ? 'Yes' : 'No'}\n`;
33
36
  if (storageActive) {
34
37
  archContent += `- **Enabled Nodes**: ${Object.keys(config.storage).filter(k => config.storage[k]?.enabled).join(', ')}\n`;
35
- archContent += `- **Endpoints**: \n - Upload: \`POST /api/storage/upload?provider=\`\n - Exact Retrieve: \`GET /api/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET /api/storage/scan/*key\`\n`;
38
+ archContent += `- **Endpoints**: \n - Upload: \`POST ${apiPrefix}/storage/upload?provider=\`\n - Exact Retrieve: \`GET ${apiPrefix}/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET ${apiPrefix}/storage/scan/*key\`\n`;
36
39
  }
37
40
  const bootstrapActive = config.storage?.bootstrap?.enabled;
38
41
  if (bootstrapActive) {
@@ -43,29 +46,35 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
43
46
  archContent += `- **Elasticsearch Sync**: ${config.search?.elasticsearch?.enabled ? 'Enabled (via @Searchable)' : 'Disabled'}\n`;
44
47
  archContent += `- **OpenTelemetry**: ${config.telemetry?.otel?.enabled ? 'Enabled' : 'Disabled'}\n`;
45
48
 
49
+ archContent += `\n## Deployment (Kubernetes)\n`;
50
+ archContent += `- **Network Isolation**: Strict per-service isolated namespaces (e.g., \`[shortName]-[service]-k8s\`).\n`;
51
+ archContent += `- **Internal Routing**: Services communicate via explicit Fully Qualified Domain Names (FQDN).\n`;
52
+ archContent += `- **External Exposure**: Infrastructure services are exposed via explicit NodePort mappings (Range: 30000+).\n`;
53
+ archContent += `- **Self-Contained Manifests**: All manifests inject their own \`kind: Namespace\` block for auto-provisioning.\n`;
54
+
46
55
  await fs.writeFile(path.join(aiDocsDir, 'ARCHITECTURE.md'), archContent);
47
56
 
48
57
  // 2. ENDPOINTS.md
49
58
  let endpointsContent = `# REST & gRPC API Surface\n\n`;
50
- endpointsContent += `## Base Path: \`/api\`\n\n`;
59
+ endpointsContent += `## Base Path: \`${apiPrefix}\`\n\n`;
51
60
  endpointsContent += `### Standard Entity Endpoints\n`;
52
61
  for (const entity of entities) {
53
62
  const routeName = entity.name.toLowerCase() + 's';
54
63
  endpointsContent += `\n#### ${entity.name}\n`;
55
- endpointsContent += `- \`GET /api/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
56
- endpointsContent += `- \`GET /api/${routeName}/:id\`\n`;
57
- endpointsContent += `- \`POST /api/${routeName}\`\n`;
58
- endpointsContent += `- \`PUT /api/${routeName}/:id\`\n`;
59
- endpointsContent += `- \`DELETE /api/${routeName}/:id\`\n`;
60
- endpointsContent += `- \`POST /api/${routeName}/bulk\` (Bulk Create/Update)\n`;
64
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
65
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}/:id\`\n`;
66
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}\`\n`;
67
+ endpointsContent += `- \`PUT ${apiPrefix}/${routeName}/:id\`\n`;
68
+ endpointsContent += `- \`DELETE ${apiPrefix}/${routeName}/:id\`\n`;
69
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}/bulk\` (Bulk Create/Update)\n`;
61
70
  if (entity.isSearchable) {
62
- endpointsContent += `- \`GET /api/search/${routeName}?q={query}\` (Elasticsearch)\n`;
71
+ endpointsContent += `- \`GET ${apiPrefix}/search/${routeName}?q={query}\` (Elasticsearch)\n`;
63
72
  }
64
73
  }
65
74
 
66
75
  endpointsContent += `\n## Infrastructure & Management APIs (SuperAdmin Protected)\n`;
67
- endpointsContent += `- \`GET /api/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
68
- endpointsContent += `- \`POST /api/admin/metering/limit\` (Set SaaS Quota limits)\n`;
76
+ endpointsContent += `- \`GET ${apiPrefix}/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
77
+ endpointsContent += `- \`POST ${apiPrefix}/admin/metering/limit\` (Set SaaS Quota limits)\n`;
69
78
  endpointsContent += `- \`POST /management/tenant/assign\` (Dynamically initialize new Tenant DBs)\n`;
70
79
 
71
80
  endpointsContent += `\n## Open APIs (No JWT required)\n`;
@@ -14,6 +14,8 @@ export const generateDeploymentArtifacts = async (config, projectRootDir) => {
14
14
  await fs.ensureDir(githubDir);
15
15
 
16
16
  const appName = config.name || 'go-duck';
17
+ const parts = appName.split(/[-_]/);
18
+ const shortName = (parts.length > 1 ? parts.map(w => w[0]).join('') : appName).substring(0, 10).toLowerCase() || 'goduck';
17
19
  const appPort = config.server?.port || 8080;
18
20
  const keycloakHost = config.security?.['keycloak-host'] || 'http://localhost:8180';
19
21
  const keycloakPort = keycloakHost.includes(':') ? keycloakHost.split(':').pop() : '8080';
@@ -117,6 +119,7 @@ services:
117
119
  mosquitto:
118
120
  image: eclipse-mosquitto:2.0.18
119
121
  container_name: ${appName}-mqtt
122
+ command: ["sh", "-c", "mosquitto_passwd -c -b /tmp/mosquitto_passwd ${config.messaging?.mqtt?.username || 'dev_user'} ${config.messaging?.mqtt?.password || 'dev_password'} && chown 1883:1883 /tmp/mosquitto_passwd && exec /usr/sbin/mosquitto -c /mosquitto/config/mosquitto.conf"]
120
123
  ports:
121
124
  - "${mqttPort}:1883"
122
125
  - "9001:9001"
@@ -207,10 +210,26 @@ services:
207
210
  - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
208
211
  - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
209
212
  - GO_DUCK_DATASOURCE_PORT=5432
213
+ - GO_DUCK_DATASOURCE_MONGODB_ENABLED=${config.datasource?.mongodb?.enabled ? 'true' : 'false'}
214
+ - GO_DUCK_DATASOURCE_MONGODB_URI=mongodb://mongodb:27017
215
+ - GO_DUCK_DATASOURCE_MONGODB_DATABASE=${config.datasource?.mongodb?.database || 'goduck_document_store'}
210
216
  - GO_DUCK_CACHE_REDIS_HOST=redis:6379
217
+ - GO_DUCK_CACHE_REDIS_PASSWORD=${config.cache?.redis?.password || ''}
211
218
  - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
219
+ - GO_DUCK_MESSAGING_MQTT_USERNAME=${config.messaging?.mqtt?.username || 'dev_user'}
220
+ - GO_DUCK_MESSAGING_MQTT_PASSWORD=${config.messaging?.mqtt?.password || 'dev_password'}
212
221
  - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
213
222
  - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
223
+ - GO_DUCK_ELASTICSEARCH_USERNAME=${config.elasticsearch?.username || 'elastic'}
224
+ - GO_DUCK_ELASTICSEARCH_PASSWORD=${config.elasticsearch?.password || 'changeme'}
225
+ - GO_DUCK_SECURITY_KEYCLOAK_HOST=http://keycloak:8080
226
+ - GO_DUCK_SECURITY_KEYCLOAK_REALM=${config.security?.['keycloak-realm'] || 'go-duck-realm'}
227
+ - GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID=${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}
228
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID=${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}
229
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET=${config.security?.['keycloak-service-secret'] || 'service-secret-123'}
230
+ - GO_DUCK_STORAGE_MINIO_ENDPOINT=http://minio:9000
231
+ - GO_DUCK_STORAGE_MINIO_ACCESS_KEY=${config.storage?.minio?.['access-key'] || 'minioadmin'}
232
+ - GO_DUCK_STORAGE_MINIO_SECRET_KEY=${config.storage?.minio?.['secret-key'] || 'minioadmin'}
214
233
  restart: always
215
234
  networks:
216
235
  - go-duck-net
@@ -241,10 +260,26 @@ services:
241
260
  - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
242
261
  - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
243
262
  - GO_DUCK_DATASOURCE_PORT=5432
263
+ - GO_DUCK_DATASOURCE_MONGODB_ENABLED=${config.datasource?.mongodb?.enabled ? 'true' : 'false'}
264
+ - GO_DUCK_DATASOURCE_MONGODB_URI=mongodb://mongodb:27017
265
+ - GO_DUCK_DATASOURCE_MONGODB_DATABASE=${config.datasource?.mongodb?.database || 'goduck_document_store'}
244
266
  - GO_DUCK_CACHE_REDIS_HOST=redis:6379
267
+ - GO_DUCK_CACHE_REDIS_PASSWORD=${config.cache?.redis?.password || ''}
245
268
  - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
269
+ - GO_DUCK_MESSAGING_MQTT_USERNAME=${config.messaging?.mqtt?.username || 'dev_user'}
270
+ - GO_DUCK_MESSAGING_MQTT_PASSWORD=${config.messaging?.mqtt?.password || 'dev_password'}
246
271
  - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
247
272
  - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
273
+ - GO_DUCK_ELASTICSEARCH_USERNAME=${config.elasticsearch?.username || 'elastic'}
274
+ - GO_DUCK_ELASTICSEARCH_PASSWORD=${config.elasticsearch?.password || 'changeme'}
275
+ - GO_DUCK_SECURITY_KEYCLOAK_HOST=http://keycloak:8080
276
+ - GO_DUCK_SECURITY_KEYCLOAK_REALM=${config.security?.['keycloak-realm'] || 'go-duck-realm'}
277
+ - GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID=${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}
278
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID=${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}
279
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET=${config.security?.['keycloak-service-secret'] || 'service-secret-123'}
280
+ - GO_DUCK_STORAGE_MINIO_ENDPOINT=http://minio:9000
281
+ - GO_DUCK_STORAGE_MINIO_ACCESS_KEY=${config.storage?.minio?.['access-key'] || 'minioadmin'}
282
+ - GO_DUCK_STORAGE_MINIO_SECRET_KEY=${config.storage?.minio?.['secret-key'] || 'minioadmin'}
248
283
  depends_on:
249
284
  postgres:
250
285
  condition: service_healthy
@@ -269,7 +304,8 @@ networks:
269
304
  listener 1883
270
305
  listener 9001
271
306
  protocol websockets
272
- allow_anonymous true
307
+ allow_anonymous false
308
+ password_file /tmp/mosquitto_passwd
273
309
  `;
274
310
 
275
311
  // --- 6. GitHub Actions CI/CD ---
@@ -454,7 +490,7 @@ spec:
454
490
  - name: GO_DUCK_SERVER_REST_PORT
455
491
  value: "${appPort}"
456
492
  - name: GO_DUCK_DATASOURCE_HOST
457
- value: postgres
493
+ value: postgres.${shortName}-postgres-k8s.svc.cluster.local
458
494
  - name: GO_DUCK_DATASOURCE_USERNAME
459
495
  value: "${config.datasource?.username || 'postgres'}"
460
496
  - name: GO_DUCK_DATASOURCE_PASSWORD
@@ -463,18 +499,48 @@ spec:
463
499
  value: "${config.datasource?.database || 'go_duck_master'}"
464
500
  - name: GO_DUCK_DATASOURCE_PORT
465
501
  value: "5432"
502
+ - name: GO_DUCK_DATASOURCE_MONGODB_ENABLED
503
+ value: "${config.datasource?.mongodb?.enabled ? 'true' : 'false'}"
504
+ - name: GO_DUCK_DATASOURCE_MONGODB_URI
505
+ value: mongodb://mongodb.${shortName}-mongodb-k8s.svc.cluster.local:27017
506
+ - name: GO_DUCK_DATASOURCE_MONGODB_DATABASE
507
+ value: "${config.datasource?.mongodb?.database || 'goduck_document_store'}"
466
508
  - name: GO_DUCK_CACHE_REDIS_HOST
467
- value: redis:6379
509
+ value: redis.${shortName}-redis-k8s.svc.cluster.local:6379
510
+ - name: GO_DUCK_CACHE_REDIS_PASSWORD
511
+ value: "${config.cache?.redis?.password || ''}"
468
512
  - name: GO_DUCK_MESSAGING_MQTT_BROKER
469
- value: tcp://mosquitto:1883
513
+ value: tcp://mosquitto.${shortName}-mosquitto-k8s.svc.cluster.local:1883
514
+ - name: GO_DUCK_MESSAGING_MQTT_USERNAME
515
+ value: "${config.messaging?.mqtt?.username || 'dev_user'}"
516
+ - name: GO_DUCK_MESSAGING_MQTT_PASSWORD
517
+ value: "${config.messaging?.mqtt?.password || 'dev_password'}"
470
518
  - name: GO_DUCK_MESSAGING_NATS_URL
471
- value: nats://nats:4222
519
+ value: nats://nats.${shortName}-nats-k8s.svc.cluster.local:4222
472
520
  - name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
473
- value: otel-collector:4317
521
+ value: otel-collector.${shortName}-otel-collector-k8s.svc.cluster.local:4317
474
522
  - name: GO_DUCK_ELASTICSEARCH_ADDRESSES
475
- value: http://elasticsearch:9200
523
+ value: http://elasticsearch.${shortName}-elasticsearch-k8s.svc.cluster.local:9200
524
+ - name: GO_DUCK_ELASTICSEARCH_USERNAME
525
+ value: "${config.elasticsearch?.username || 'elastic'}"
526
+ - name: GO_DUCK_ELASTICSEARCH_PASSWORD
527
+ value: "${config.elasticsearch?.password || 'changeme'}"
476
528
  - name: GO_DUCK_SECURITY_KEYCLOAK_HOST
477
- value: http://keycloak:8080
529
+ value: http://keycloak.${shortName}-keycloak-k8s.svc.cluster.local:8080
530
+ - name: GO_DUCK_SECURITY_KEYCLOAK_REALM
531
+ value: "${config.security?.['keycloak-realm'] || 'go-duck-realm'}"
532
+ - name: GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID
533
+ value: "${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}"
534
+ - name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID
535
+ value: "${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}"
536
+ - name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET
537
+ value: "${config.security?.['keycloak-service-secret'] || 'service-secret-123'}"
538
+ - name: GO_DUCK_STORAGE_MINIO_ENDPOINT
539
+ value: "http://minio.${shortName}-minio-k8s.svc.cluster.local:9000"
540
+ - name: GO_DUCK_STORAGE_MINIO_ACCESS_KEY
541
+ value: "${config.storage?.minio?.['access-key'] || 'minioadmin'}"
542
+ - name: GO_DUCK_STORAGE_MINIO_SECRET_KEY
543
+ value: "${config.storage?.minio?.['secret-key'] || 'minioadmin'}"
478
544
  ports:
479
545
  - name: http
480
546
  containerPort: ${appPort}
@@ -486,13 +552,16 @@ kind: Service
486
552
  metadata:
487
553
  name: ${appName}
488
554
  spec:
555
+ type: NodePort
489
556
  ports:
490
557
  - name: http
491
558
  port: ${appPort}
492
559
  targetPort: http
560
+ nodePort: 30080
493
561
  - name: grpc
494
562
  port: 9000
495
563
  targetPort: grpc
564
+ nodePort: 30090
496
565
  selector:
497
566
  app: ${appName}
498
567
  `;
@@ -549,8 +618,10 @@ kind: Service
549
618
  metadata:
550
619
  name: postgres
551
620
  spec:
621
+ type: NodePort
552
622
  ports:
553
623
  - port: 5432
624
+ nodePort: 30432
554
625
  selector:
555
626
  app: postgres
556
627
  `;
@@ -582,8 +653,10 @@ kind: Service
582
653
  metadata:
583
654
  name: redis
584
655
  spec:
656
+ type: NodePort
585
657
  ports:
586
658
  - port: 6379
659
+ nodePort: 30379
587
660
  selector:
588
661
  app: redis
589
662
  `;
@@ -615,8 +688,10 @@ kind: Service
615
688
  metadata:
616
689
  name: nats
617
690
  spec:
691
+ type: NodePort
618
692
  ports:
619
693
  - port: 4222
694
+ nodePort: 30222
620
695
  selector:
621
696
  app: nats
622
697
  `;
@@ -630,7 +705,8 @@ data:
630
705
  listener 1883
631
706
  listener 9001
632
707
  protocol websockets
633
- allow_anonymous true
708
+ allow_anonymous false
709
+ password_file /tmp/mosquitto_passwd
634
710
  ---
635
711
  apiVersion: apps/v1
636
712
  kind: Deployment
@@ -651,6 +727,7 @@ spec:
651
727
  containers:
652
728
  - name: mosquitto
653
729
  image: eclipse-mosquitto:2.0.18
730
+ command: ["sh", "-c", "mosquitto_passwd -c -b /tmp/mosquitto_passwd ${config.messaging?.mqtt?.username || 'dev_user'} ${config.messaging?.mqtt?.password || 'dev_password'} && chown 1883:1883 /tmp/mosquitto_passwd && exec /usr/sbin/mosquitto -c /mosquitto/config/mosquitto.conf"]
654
731
  ports:
655
732
  - containerPort: 1883
656
733
  - containerPort: 9001
@@ -668,11 +745,14 @@ kind: Service
668
745
  metadata:
669
746
  name: mosquitto
670
747
  spec:
748
+ type: NodePort
671
749
  ports:
672
750
  - name: mqtt
673
751
  port: 1883
752
+ nodePort: 31883
674
753
  - name: ws
675
754
  port: 9001
755
+ nodePort: 30001
676
756
  selector:
677
757
  app: mosquitto
678
758
  `;
@@ -711,8 +791,10 @@ kind: Service
711
791
  metadata:
712
792
  name: elasticsearch
713
793
  spec:
794
+ type: NodePort
714
795
  ports:
715
796
  - port: 9200
797
+ nodePort: 30200
716
798
  selector:
717
799
  app: elasticsearch
718
800
  `;
@@ -748,11 +830,14 @@ kind: Service
748
830
  metadata:
749
831
  name: jaeger
750
832
  spec:
833
+ type: NodePort
751
834
  ports:
752
835
  - name: ui
753
836
  port: 16686
837
+ nodePort: 30686
754
838
  - name: otlp-grpc
755
839
  port: 4317
840
+ nodePort: 30317
756
841
  selector:
757
842
  app: jaeger
758
843
  `;
@@ -824,11 +909,14 @@ kind: Service
824
909
  metadata:
825
910
  name: otel-collector
826
911
  spec:
912
+ type: NodePort
827
913
  ports:
828
914
  - name: otlp-grpc
829
915
  port: 4317
916
+ nodePort: 30317
830
917
  - name: otlp-http
831
918
  port: 4318
919
+ nodePort: 30318
832
920
  selector:
833
921
  app: otel-collector
834
922
  `;
@@ -890,8 +978,10 @@ kind: Service
890
978
  metadata:
891
979
  name: keycloak
892
980
  spec:
981
+ type: NodePort
893
982
  ports:
894
983
  - port: 8080
984
+ nodePort: 30880
895
985
  selector:
896
986
  app: keycloak
897
987
  `;
@@ -909,17 +999,33 @@ spec:
909
999
  await fs.writeFile(path.join(devopsDir, 'app.yml'), appYaml);
910
1000
  await fs.writeFile(path.join(devopsDir, 'docker-compose.yml'), dockerCompose);
911
1001
  await fs.writeFile(path.join(k8sDir, 'mosquitto.conf'), mosquittoConf);
1002
+ // Namespace calculation: unique namespace per service
1003
+ const applyNs = (yamlString, serviceName) => {
1004
+ const nsName = `${shortName}-${serviceName}-k8s`;
1005
+ const nsBlock = `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${nsName}\n---\n`;
1006
+ return nsBlock + yamlString.replace(/^metadata:$/gm, `metadata:\n namespace: ${nsName}`);
1007
+ };
1008
+
1009
+ const servicesList = ['app', 'postgres', 'redis', 'nats', 'mosquitto', 'elasticsearch', 'jaeger', 'otel-collector', 'keycloak'];
1010
+ if (config.datasource?.mongodb?.enabled) servicesList.push('mongodb');
912
1011
 
1012
+ // We conditionally add minio if storage is enabled (or any blob provider), but for safety we'll just generate its namespace always.
1013
+ servicesList.push('minio');
1014
+
1015
+ const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${shortName}-${svc}-k8s`).join('\n---\n');
1016
+
1017
+ await fs.writeFile(path.join(k8sDir, 'namespace.yaml'), k8sNamespaceYaml);
1018
+
913
1019
  // Write Kubernetes manifest assets
914
- await fs.writeFile(path.join(k8sDir, 'app.yaml'), k8sAppYaml);
915
- await fs.writeFile(path.join(k8sDir, 'postgres.yaml'), k8sPostgresYaml);
916
- await fs.writeFile(path.join(k8sDir, 'redis.yaml'), k8sRedisYaml);
917
- await fs.writeFile(path.join(k8sDir, 'nats.yaml'), k8sNatsYaml);
918
- await fs.writeFile(path.join(k8sDir, 'mosquitto.yaml'), k8sMosquittoYaml);
919
- await fs.writeFile(path.join(k8sDir, 'elasticsearch.yaml'), k8sElasticsearchYaml);
920
- await fs.writeFile(path.join(k8sDir, 'jaeger.yaml'), k8sJaegerYaml);
921
- await fs.writeFile(path.join(k8sDir, 'otel-collector-k8s.yaml'), k8sOtelCollectorYaml);
922
- await fs.writeFile(path.join(k8sDir, 'keycloak.yaml'), k8sKeycloakYaml);
1020
+ await fs.writeFile(path.join(k8sDir, 'app.yaml'), applyNs(k8sAppYaml, 'app'));
1021
+ await fs.writeFile(path.join(k8sDir, 'postgres.yaml'), applyNs(k8sPostgresYaml, 'postgres'));
1022
+ await fs.writeFile(path.join(k8sDir, 'redis.yaml'), applyNs(k8sRedisYaml, 'redis'));
1023
+ await fs.writeFile(path.join(k8sDir, 'nats.yaml'), applyNs(k8sNatsYaml, 'nats'));
1024
+ await fs.writeFile(path.join(k8sDir, 'mosquitto.yaml'), applyNs(k8sMosquittoYaml, 'mosquitto'));
1025
+ await fs.writeFile(path.join(k8sDir, 'elasticsearch.yaml'), applyNs(k8sElasticsearchYaml, 'elasticsearch'));
1026
+ await fs.writeFile(path.join(k8sDir, 'jaeger.yaml'), applyNs(k8sJaegerYaml, 'jaeger'));
1027
+ await fs.writeFile(path.join(k8sDir, 'otel-collector-k8s.yaml'), applyNs(k8sOtelCollectorYaml, 'otel-collector'));
1028
+ await fs.writeFile(path.join(k8sDir, 'keycloak.yaml'), applyNs(k8sKeycloakYaml, 'keycloak'));
923
1029
 
924
1030
  // Additional optional services manifests
925
1031
  const k8sMongoYaml = `apiVersion: apps/v1
@@ -949,8 +1055,10 @@ kind: Service
949
1055
  metadata:
950
1056
  name: mongodb
951
1057
  spec:
1058
+ type: NodePort
952
1059
  ports:
953
1060
  - port: 27017
1061
+ nodePort: 30017
954
1062
  selector:
955
1063
  app: mongodb`;
956
1064
  const k8sMinioYaml = `apiVersion: apps/v1
@@ -988,12 +1096,14 @@ kind: Service
988
1096
  metadata:
989
1097
  name: minio
990
1098
  spec:
1099
+ type: NodePort
991
1100
  ports:
992
1101
  - port: 9000
1102
+ nodePort: 30900
993
1103
  selector:
994
1104
  app: minio`;
995
- await fs.writeFile(path.join(k8sDir, 'mongodb.yaml'), k8sMongoYaml);
996
- await fs.writeFile(path.join(k8sDir, 'minio.yaml'), k8sMinioYaml);
1105
+ await fs.writeFile(path.join(k8sDir, 'mongodb.yaml'), applyNs(k8sMongoYaml, 'mongodb'));
1106
+ await fs.writeFile(path.join(k8sDir, 'minio.yaml'), applyNs(k8sMinioYaml, 'minio'));
997
1107
 
998
1108
  await fs.writeFile(path.join(githubDir, 'ci.yml'), ciWorkflow);
999
1109
  await fs.writeFile(path.join(githubDir, 'cd.yml'), cdWorkflow);
@@ -4,6 +4,9 @@ import chalk from 'chalk';
4
4
 
5
5
  export const generatePostmanCollection = async (config, entities, outputDir, openEntities = []) => {
6
6
  const docsDir = path.join(outputDir, 'docs');
7
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
8
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
9
+ const apiPathArr = apiPrefix.split('/').filter(p => p !== '');
7
10
  await fs.ensureDir(docsDir);
8
11
 
9
12
  const collection = {
@@ -140,11 +143,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
140
143
  { key: "X-Tenant-ID", value: "{{tenant}}" }
141
144
  ],
142
145
  url: {
143
- raw: "http://{{host}}:{{port}}/api/admin/audit",
146
+ raw: `http://{{host}}:{{port}}${apiPrefix}/admin/audit`,
144
147
  protocol: "http",
145
148
  host: ["{{host}}"],
146
149
  port: "{{port}}",
147
- path: ["api", "admin", "audit"]
150
+ path: [...apiPathArr, "admin", "audit"]
148
151
  }
149
152
  }
150
153
  },
@@ -157,11 +160,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
157
160
  { key: "X-Tenant-ID", value: "{{tenant}}" }
158
161
  ],
159
162
  url: {
160
- raw: "http://{{host}}:{{port}}/api/metering/usage",
163
+ raw: `http://{{host}}:{{port}}${apiPrefix}/metering/usage`,
161
164
  protocol: "http",
162
165
  host: ["{{host}}"],
163
166
  port: "{{port}}",
164
- path: ["api", "metering", "usage"]
167
+ path: [...apiPathArr, "metering", "usage"]
165
168
  }
166
169
  }
167
170
  }
@@ -390,7 +393,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
390
393
  request: {
391
394
  method: "GET",
392
395
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
393
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
396
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
394
397
  }
395
398
  });
396
399
  publicItems.push({
@@ -398,7 +401,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
398
401
  request: {
399
402
  method: "GET",
400
403
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
401
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`, "1"] }
404
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`, "1"] }
402
405
  }
403
406
  });
404
407
  }
@@ -409,7 +412,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
409
412
  method: "POST",
410
413
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
411
414
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
412
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`] }
415
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`] }
413
416
  }
414
417
  });
415
418
  }
@@ -425,7 +428,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
425
428
  request: {
426
429
  method: "GET",
427
430
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
428
- url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
431
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
429
432
  }
430
433
  },
431
434
  {
@@ -434,7 +437,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
434
437
  method: "POST",
435
438
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
436
439
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
437
- url: { raw: `http://{{host}}:{{port}}/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`] }
440
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`] }
438
441
  }
439
442
  },
440
443
  {
@@ -442,7 +445,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
442
445
  request: {
443
446
  method: "GET",
444
447
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
445
- url: { raw: `http://{{host}}:{{port}}/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`, "1"] }
448
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`, "1"] }
446
449
  }
447
450
  },
448
451
  {
@@ -450,7 +453,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
450
453
  request: {
451
454
  method: "GET",
452
455
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
453
- url: { raw: `http://{{host}}:{{port}}/api/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
456
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
454
457
  }
455
458
  }
456
459
  ];
@@ -461,7 +464,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
461
464
  request: {
462
465
  method: "GET",
463
466
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
464
- url: { raw: `http://{{host}}:{{port}}/api/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "search", name], query: [ { key: "q", value: "Sample" } ] }
467
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "search", name], query: [ { key: "q", value: "Sample" } ] }
465
468
  }
466
469
  });
467
470
  }
@@ -513,11 +516,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
513
516
  ]
514
517
  },
515
518
  url: {
516
- raw: "http://{{host}}:{{port}}/api/storage/upload?provider=sftp",
519
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/upload?provider=sftp`,
517
520
  protocol: "http",
518
521
  host: ["{{host}}"],
519
522
  port: "{{port}}",
520
- path: ["api", "storage", "upload"],
523
+ path: [...apiPathArr, "storage", "upload"],
521
524
  query: [{ key: "provider", value: "sftp" }]
522
525
  }
523
526
  }
@@ -531,11 +534,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
531
534
  { key: "X-Tenant-ID", value: "{{tenant}}" }
532
535
  ],
533
536
  url: {
534
- raw: "http://{{host}}:{{port}}/api/storage/download/farm/animals/photo.jpg?provider=sftp",
537
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/download/farm/animals/photo.jpg?provider=sftp`,
535
538
  protocol: "http",
536
539
  host: ["{{host}}"],
537
540
  port: "{{port}}",
538
- path: ["api", "storage", "download", "farm", "animals", "photo.jpg"],
541
+ path: [...apiPathArr, "storage", "download", "farm", "animals", "photo.jpg"],
539
542
  query: [{ key: "provider", value: "sftp" }]
540
543
  }
541
544
  }
@@ -549,11 +552,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
549
552
  { key: "X-Tenant-ID", value: "{{tenant}}" }
550
553
  ],
551
554
  url: {
552
- raw: "http://{{host}}:{{port}}/api/storage/scan/farm/animals/photo.jpg",
555
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/scan/farm/animals/photo.jpg`,
553
556
  protocol: "http",
554
557
  host: ["{{host}}"],
555
558
  port: "{{port}}",
556
- path: ["api", "storage", "scan", "farm", "animals", "photo.jpg"]
559
+ path: [...apiPathArr, "storage", "scan", "farm", "animals", "photo.jpg"]
557
560
  }
558
561
  }
559
562
  }
@@ -6,6 +6,9 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
6
6
  const docsDir = path.join(outputDir, 'docs');
7
7
  await fs.ensureDir(docsDir);
8
8
 
9
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
10
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
11
+
9
12
  const swagger = {
10
13
  openapi: '3.0.0',
11
14
  info: {
@@ -201,14 +204,14 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
201
204
  };
202
205
 
203
206
  // 1a. Secured Paths
204
- addEntityOperations('/api', false);
207
+ addEntityOperations(apiPrefix, false);
205
208
 
206
209
  // 1b. Public Paths (if marked as open)
207
- addEntityOperations('/open/api', true);
210
+ addEntityOperations('/open' + apiPrefix, true);
208
211
  }
209
212
 
210
213
  // 2. Add System Paths
211
- swagger.paths['/api/rpc/{table}'] = {
214
+ swagger.paths[`${apiPrefix}/rpc/{table}`] = {
212
215
  get: {
213
216
  tags: ['Search Engine'],
214
217
  summary: 'Generic PostgREST RPC Engine',
@@ -240,7 +243,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
240
243
  }
241
244
  };
242
245
 
243
- swagger.paths['/api/admin/audit'] = {
246
+ swagger.paths[`${apiPrefix}/admin/audit`] = {
244
247
  get: {
245
248
  tags: ['Observability'],
246
249
  summary: 'Fetch Audit Trail',
@@ -281,7 +284,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
281
284
  };
282
285
 
283
286
  if (config.elasticsearch?.enabled) {
284
- swagger.paths['/api/search/{entity}'] = {
287
+ swagger.paths[`${apiPrefix}/search/{entity}`] = {
285
288
  get: {
286
289
  tags: ['Search Engine'],
287
290
  summary: 'Elasticsearch Global Search',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.3",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -120,6 +120,16 @@
120
120
  <span class="px-4 py-1.5 bg-orange-100 text-orange-700 text-[9px] font-black rounded-lg">WSO2 REST PROXY</span>
121
121
  </div>
122
122
  </div>
123
+
124
+ <div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-fuchsia-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-fuchsia-50/30">
125
+ <p class="text-[9px] font-bold text-fuchsia-600 uppercase tracking-widest mb-4">Elite Extension (+12%)</p>
126
+ <h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">K8s Network Isolation & NodePorts</h4>
127
+ <p class="text-slate-600 leading-relaxed mb-8">Deploys every microservice component into its own dynamically generated namespace. Uses internal FQDN routing for secure communication and predictable NodePorts for host access.</p>
128
+ <div class="flex gap-3">
129
+ <span class="px-4 py-1.5 bg-fuchsia-100 text-fuchsia-700 text-[9px] font-black rounded-lg">ISOLATED NAMESPACES</span>
130
+ <span class="px-4 py-1.5 bg-fuchsia-100 text-fuchsia-700 text-[9px] font-black rounded-lg">FQDN ROUTING</span>
131
+ </div>
132
+ </div>
123
133
  </div>
124
134
  </div>
125
135
  </section>