@underpostnet/cyberia 3.2.90 → 3.3.0

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.
Files changed (129) hide show
  1. package/.env.example +16 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +87 -0
  3. package/.github/workflows/cyberia-client.cd.yml +1 -18
  4. package/.github/workflows/cyberia-server.cd.yml +1 -18
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -78
  12. package/.github/workflows/ghpkg.ci.yml +7 -1
  13. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  14. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  15. package/.github/workflows/release.cd.yml +1 -9
  16. package/CHANGELOG.md +110 -1
  17. package/CLI-HELP.md +139 -9
  18. package/Dockerfile +1 -1
  19. package/Dockerfile.dev +1 -1
  20. package/Dockerfile.test +1 -1
  21. package/bin/build.js +7 -5
  22. package/bin/cyberia.js +73 -53
  23. package/bin/deploy.js +1 -1
  24. package/bin/index.js +73 -53
  25. package/compose.env +16 -0
  26. package/conf.js +2 -0
  27. package/deploy/dd-cyberia/deploy.sh +42 -0
  28. package/deploy/dd-cyberia/init.sh +63 -0
  29. package/deploy/dd-cyberia/sync-deploy.sh +194 -0
  30. package/deploy/lib/logging.sh +96 -0
  31. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  32. package/deploy/release/deploy.sh +62 -0
  33. package/deployment.yaml +1 -1
  34. package/docker-compose.yml +31 -37
  35. package/hardhat/package-lock.json +9 -9
  36. package/hardhat/package.json +3 -3
  37. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  38. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  39. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  40. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  41. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  42. package/package.json +2 -2
  43. package/scripts/audit-selinux.sh +64 -0
  44. package/scripts/coverall-test-cyberia.sh +24 -0
  45. package/scripts/coverall-test.sh +24 -0
  46. package/scripts/gpu-diag.sh +0 -0
  47. package/scripts/ip-info.sh +0 -0
  48. package/scripts/k3s-node-setup.sh +18 -15
  49. package/scripts/kubeadm-node-setup.sh +12 -23
  50. package/scripts/link-local-underpost-cli.sh +0 -0
  51. package/scripts/lxd-vm-setup.sh +0 -0
  52. package/scripts/maas-nat-firewalld.sh +0 -0
  53. package/scripts/nat-iptables.sh +2 -0
  54. package/scripts/rhel-grpc-setup.sh +0 -0
  55. package/scripts/rocky-kickstart.sh +25 -9
  56. package/scripts/test-monitor.sh +1 -1
  57. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  58. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -1
  59. package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
  62. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  63. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -3
  64. package/src/api/object-layer/object-layer.model.js +204 -3
  65. package/src/api/object-layer/object-layer.service.js +1 -14
  66. package/src/cli/baremetal.js +1 -2
  67. package/src/cli/cloud-init.js +1 -1
  68. package/src/cli/cluster.js +73 -68
  69. package/src/cli/db.js +9 -2
  70. package/src/cli/deploy.js +21 -5
  71. package/src/cli/docker-compose.js +1 -1
  72. package/src/cli/env.js +1 -1
  73. package/src/cli/image.js +0 -1
  74. package/src/cli/index.js +121 -9
  75. package/src/cli/lxd.js +1 -1
  76. package/src/cli/monitor.js +1 -1
  77. package/src/cli/release.js +57 -22
  78. package/src/cli/repository.js +11 -9
  79. package/src/cli/run.js +36 -9
  80. package/src/cli/ssh.js +198 -77
  81. package/src/cli/system.js +26 -13
  82. package/src/cli/test.js +1 -1
  83. package/src/cli/vultr.js +583 -0
  84. package/src/cli/wireguard.js +2125 -0
  85. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  86. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
  87. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  88. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  89. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  90. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  91. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  92. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  93. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  94. package/src/client/services/object-layer/object-layer.management.js +4 -4
  95. package/src/client-builder/client-build.js +20 -14
  96. package/src/db/mongo/MongooseDB.js +4 -0
  97. package/src/index.js +25 -1
  98. package/src/projects/cyberia/catalog-cyberia.js +5 -0
  99. package/src/projects/cyberia/generate-saga.js +14 -23
  100. package/src/projects/cyberia/instance-data.js +21 -2
  101. package/src/projects/cyberia/object-layer.js +11 -21
  102. package/src/projects/underpost/catalog-underpost.js +4 -1
  103. package/src/runtime/cyberia-client/Dockerfile +1 -1
  104. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  105. package/src/runtime/cyberia-server/Dockerfile +1 -1
  106. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  107. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  108. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  109. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  110. package/src/runtime/engine-cyberia/compose.env +16 -0
  111. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  112. package/src/server/backup.js +1 -1
  113. package/src/server/conf.js +18 -108
  114. package/src/server/cron.js +249 -51
  115. package/src/server/dns.js +100 -6
  116. package/src/server/environment.js +98 -0
  117. package/src/server/forward-proxy.js +549 -0
  118. package/src/server/middlewares.js +56 -1
  119. package/src/server/process.js +0 -1
  120. package/src/server/selinux.js +185 -0
  121. package/src/server/systemd.js +205 -0
  122. package/src/server/underpost-compression.js +186 -0
  123. package/src/server/underpost-gateway.js +20 -10
  124. package/src/server/underpost-ingress.js +18 -2
  125. package/test/cyberia-instance-conf-defaults.test.js +1 -0
  126. package/test/selinux.test.js +71 -0
  127. package/test/underpost-gateway.test.js +41 -0
  128. package/test/underpost-ingress.test.js +52 -0
  129. package/test/wireguard-edge.test.js +1177 -0
@@ -909,7 +909,6 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
909
909
  { itemId: 'anon', active: true, quantity: 1 },
910
910
  { itemId: 'atlas_pistol_mk2', active: true, quantity: 1 },
911
911
  { itemId: 'coin', active: false, quantity: 0 },
912
- { itemId: 'eiri', active: false, quantity: 1 },
913
912
  ],
914
913
  },
915
914
  {
@@ -939,8 +938,18 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
939
938
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['alex'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
940
939
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['agent'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
941
940
  { entityType: ENTITY_TYPES.bot, liveItemIds: ['punk'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
942
- { entityType: ENTITY_TYPES.bot, liveItemIds: ['eiri'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider-static' },
943
- { entityType: ENTITY_TYPES.bot, liveItemIds: ['kaneki'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider-static' },
941
+ {
942
+ entityType: ENTITY_TYPES.bot,
943
+ liveItemIds: ['eiri'],
944
+ deadItemIds: [DEFAULT_DEAD_ITEM_ID],
945
+ behavior: 'provider-static',
946
+ },
947
+ {
948
+ entityType: ENTITY_TYPES.bot,
949
+ liveItemIds: ['kaneki'],
950
+ deadItemIds: [DEFAULT_DEAD_ITEM_ID],
951
+ behavior: 'provider-static',
952
+ },
944
953
  {
945
954
  entityType: ENTITY_TYPES.bot,
946
955
  liveItemIds: ['lain'],
@@ -1027,6 +1036,10 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
1027
1036
  defaultPlayerHeight: 2,
1028
1037
  playerBaseLifeRegenMin: 0.5,
1029
1038
  playerBaseLifeRegenMax: 1.5,
1039
+ // Movement speed for the player entity only, in grid cells per second. Bots,
1040
+ // projectiles and every other entity keep entityBaseSpeed. 0 falls back to
1041
+ // entityBaseSpeed.
1042
+ playerBaseSpeed: 8,
1030
1043
  sumStatsLimit: 500,
1031
1044
  maxActiveLayers: 4,
1032
1045
  initialLifeFraction: 1.0,
@@ -3,6 +3,8 @@
3
3
  * @module src/api/object-layer/object-layer.model.js
4
4
  * @namespace CyberiaObjectLayerModel
5
5
  */
6
+ import crypto from 'crypto';
7
+ import stringify from 'fast-json-stable-stringify';
6
8
  import { Schema, model } from 'mongoose';
7
9
  /**
8
10
  * @typedef {Object} Stats
@@ -132,8 +134,19 @@ const ObjectLayerSchema = new Schema(
132
134
  toObject: { virtuals: true },
133
135
  },
134
136
  );
135
- // Index for faster querying
136
- ObjectLayerSchema.index({ 'data.item.id': 1 });
137
+ /**
138
+ * Name of the `data.item.id` index. Kept stable across the non-unique → unique
139
+ * upgrade so {@link ObjectLayerModel.ensureUniqueItemIdIndex} can detect and
140
+ * replace the legacy definition instead of leaving two overlapping indexes.
141
+ * @memberof CyberiaObjectLayerModel
142
+ */
143
+ const ITEM_ID_INDEX_NAME = 'data.item.id_1';
144
+ // `data.item.id` is the natural key: exactly one document per item id.
145
+ // autoIndex runs at model-compile time, so on a collection that still holds
146
+ // legacy duplicates (or a legacy non-unique index) this build fails and is
147
+ // logged by the model's `error` listener. ensureUniqueItemIdIndex() then
148
+ // dedupes and rebuilds it, after which autoIndex agrees and stays quiet.
149
+ ObjectLayerSchema.index({ 'data.item.id': 1 }, { name: ITEM_ID_INDEX_NAME, unique: true });
137
150
  ObjectLayerSchema.index({ 'data.item.type': 1 });
138
151
  // Add text index for searchable fields
139
152
  ObjectLayerSchema.index(
@@ -158,6 +171,186 @@ ObjectLayerSchema.pre('save', function () {
158
171
  }
159
172
  // cid (object layer data JSON) and data.render.cid (atlas PNG) are optional – default to ''
160
173
  });
174
+
175
+ /**
176
+ * Computes the canonical SHA-256 of an object layer `data` sub-document using
177
+ * deterministic JSON serialisation. Single source of truth for the hash;
178
+ * `ObjectLayerEngine.computeSha256` delegates here.
179
+ *
180
+ * @param {Object} data - The `data` sub-document (item, stats, ledger, render).
181
+ * @returns {string} Hex-encoded SHA-256 hash.
182
+ * @memberof CyberiaObjectLayerModel
183
+ */
184
+ const computeObjectLayerSha256 = (data) => crypto.createHash('sha256').update(stringify(data)).digest('hex');
185
+
186
+ const isMergeableObject = (value) => {
187
+ if (value === null || typeof value !== 'object') return false;
188
+ const proto = Object.getPrototypeOf(value);
189
+ return proto === Object.prototype || proto === null;
190
+ };
191
+
192
+ /**
193
+ * An incoming attribute only wins when it actually carries a value. `null`,
194
+ * `undefined` and `''` mean "not provided by this writer" — importers emit them
195
+ * whenever a stage is skipped or degraded (IPFS unreachable, atlas not
196
+ * generated, saga metadata with no render yet), and they must never erase a
197
+ * value another writer already persisted. `false` and `0` are real values.
198
+ *
199
+ * @param {*} value - Candidate attribute value.
200
+ * @returns {boolean} Whether the value should override the stored one.
201
+ * @memberof CyberiaObjectLayerModel
202
+ */
203
+ const hasValue = (value) => value !== null && value !== undefined && value !== '';
204
+
205
+ /**
206
+ * Deep-merges `incoming` over `existing`, keeping the last attribute that
207
+ * actually carries a value. Plain objects merge key by key; every other value
208
+ * (scalars, arrays, ObjectIds, Dates, Buffers) is replaced atomically.
209
+ *
210
+ * @param {*} existing - Currently persisted value.
211
+ * @param {*} incoming - Value received from the writer.
212
+ * @returns {*} The merged value.
213
+ * @memberof CyberiaObjectLayerModel
214
+ */
215
+ function mergeObjectLayerData(existing, incoming) {
216
+ if (isMergeableObject(incoming)) {
217
+ if (!isMergeableObject(existing)) return incoming;
218
+ const merged = { ...existing };
219
+ for (const key of Object.keys(incoming)) merged[key] = mergeObjectLayerData(existing[key], incoming[key]);
220
+ return merged;
221
+ }
222
+ return hasValue(incoming) ? incoming : existing;
223
+ }
224
+
225
+ /**
226
+ * Document fields an upsert may carry. `sha256` is excluded on purpose: it is
227
+ * always recomputed from the merged `data`, so it can never drift from the
228
+ * payload the writer hashed before merging.
229
+ * @memberof CyberiaObjectLayerModel
230
+ */
231
+ const UPSERTABLE_FIELDS = ['data', 'cid', 'objectLayerRenderFramesId', 'atlasSpriteSheetId'];
232
+
233
+ /**
234
+ * Collapses any pre-existing duplicates of a given `data.item.id` down to a
235
+ * single document, keeping the oldest one so `_id` references held elsewhere
236
+ * (instances, inventories, atlas metadata) stay valid.
237
+ *
238
+ * @param {import('mongoose').Model} Model - The bound ObjectLayer model.
239
+ * @param {string} itemId - The item id to collapse.
240
+ * @returns {Promise<{ survivor: Object|null, removedIds: Array }>} Survivor and removed document ids.
241
+ * @memberof CyberiaObjectLayerModel
242
+ */
243
+ async function collapseItemIdDuplicates(Model, itemId) {
244
+ const [survivor, ...duplicates] = await Model.find({ 'data.item.id': itemId }).sort({ createdAt: 1, _id: 1 });
245
+ if (!survivor || duplicates.length === 0) return { survivor: survivor || null, removedIds: [] };
246
+
247
+ const removedIds = duplicates.map((duplicate) => duplicate._id);
248
+ await Model.deleteMany({ _id: { $in: removedIds } });
249
+
250
+ // Orphaned render frames have no other cleanup path once their owner is gone.
251
+ const renderFramesIds = duplicates.map((duplicate) => duplicate.objectLayerRenderFramesId).filter(Boolean);
252
+ if (renderFramesIds.length > 0 && Model.db.models.ObjectLayerRenderFrames) {
253
+ await Model.db.models.ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFramesIds } });
254
+ }
255
+
256
+ return { survivor, removedIds };
257
+ }
258
+
259
+ /**
260
+ * Resolves the canonical document for an item id. While a legacy collection can
261
+ * still hold duplicates, an unsorted `findOne` may hand back a document that the
262
+ * next upsert collapses away, so readers and writers must agree on the same
263
+ * survivor: the oldest one.
264
+ *
265
+ * @param {string} itemId - The `data.item.id` to look up.
266
+ * @returns {import('mongoose').Query} Query resolving to the canonical document or `null`.
267
+ * @memberof CyberiaObjectLayerModel
268
+ */
269
+ ObjectLayerSchema.statics.findByItemId = function (itemId) {
270
+ return this.findOne({ 'data.item.id': itemId }).sort({ createdAt: 1, _id: 1 });
271
+ };
272
+
273
+ /**
274
+ * Dedupes the whole collection by `data.item.id`.
275
+ *
276
+ * @returns {Promise<Array>} Ids of the removed duplicate documents.
277
+ * @memberof CyberiaObjectLayerModel
278
+ */
279
+ ObjectLayerSchema.statics.dedupeByItemId = async function () {
280
+ const duplicated = await this.aggregate([
281
+ { $group: { _id: '$data.item.id', count: { $sum: 1 } } },
282
+ { $match: { count: { $gt: 1 } } },
283
+ ]);
284
+ const removedIds = [];
285
+ for (const { _id: itemId } of duplicated) {
286
+ if (itemId === null || itemId === undefined) continue;
287
+ const { removedIds: removed } = await collapseItemIdDuplicates(this, itemId);
288
+ removedIds.push(...removed);
289
+ }
290
+ return removedIds;
291
+ };
292
+
293
+ /**
294
+ * Idempotent migration that enforces the one-document-per-`data.item.id`
295
+ * invariant at the storage layer: dedupes first, then upgrades the legacy
296
+ * non-unique index to a unique one. Safe to rerun; a no-op once applied.
297
+ *
298
+ * @returns {Promise<{ removedIds: Array, indexUpgraded: boolean }>} Migration outcome.
299
+ * @memberof CyberiaObjectLayerModel
300
+ */
301
+ ObjectLayerSchema.statics.ensureUniqueItemIdIndex = async function () {
302
+ const removedIds = await this.dedupeByItemId();
303
+
304
+ const indexes = await this.collection.indexes().catch(() => []);
305
+ const current = indexes.find((index) => index.name === ITEM_ID_INDEX_NAME);
306
+ if (current?.unique) return { removedIds, indexUpgraded: false };
307
+
308
+ if (current) await this.collection.dropIndex(ITEM_ID_INDEX_NAME);
309
+ await this.collection.createIndex({ 'data.item.id': 1 }, { name: ITEM_ID_INDEX_NAME, unique: true });
310
+ return { removedIds, indexUpgraded: true };
311
+ };
312
+
313
+ /**
314
+ * The single write path for object layers keyed by item id.
315
+ *
316
+ * Guarantees that exactly one document exists per `data.item.id`: pre-existing
317
+ * duplicates are collapsed onto the oldest document, which is then updated in
318
+ * place. Attributes are merged with {@link mergeObjectLayerData}, so a writer
319
+ * that omits a value (or sends `null` / `''` because a stage was skipped) keeps
320
+ * whatever a previous writer stored instead of erasing it. `sha256` is always
321
+ * recomputed from the merged result.
322
+ *
323
+ * @param {Object} payload - Document payload; `data.item.id` is required.
324
+ * @param {Object} payload.data - Object layer data (item, stats, ledger, render).
325
+ * @param {Object} [options] - Upsert options.
326
+ * @param {Object} [options.setOnInsert=null] - Partial document applied only when the item is new,
327
+ * for fields a writer wants to seed but never refresh (mirrors Mongo's `$setOnInsert`).
328
+ * @returns {Promise<Object>} The single surviving ObjectLayer document.
329
+ * @memberof CyberiaObjectLayerModel
330
+ */
331
+ ObjectLayerSchema.statics.upsertByItemId = async function (payload, { setOnInsert = null } = {}) {
332
+ const itemId = payload?.data?.item?.id;
333
+ if (!itemId) throw new Error('ObjectLayer.upsertByItemId requires data.item.id');
334
+
335
+ const { survivor } = await collapseItemIdDuplicates(this, itemId);
336
+
337
+ if (!survivor) {
338
+ const inserted = setOnInsert ? mergeObjectLayerData(setOnInsert, payload) : payload;
339
+ return await this.create({ ...inserted, sha256: computeObjectLayerSha256(inserted.data) });
340
+ }
341
+
342
+ const existing = survivor.toObject({ virtuals: false, depopulate: true });
343
+ const update = {};
344
+ for (const field of UPSERTABLE_FIELDS) {
345
+ if (!(field in payload)) continue;
346
+ const merged = mergeObjectLayerData(existing[field], payload[field]);
347
+ if (merged !== undefined) update[field] = merged;
348
+ }
349
+ update.sha256 = computeObjectLayerSha256(update.data ?? existing.data);
350
+
351
+ return await this.findByIdAndUpdate(survivor._id, { $set: update }, { returnDocument: 'after' });
352
+ };
353
+
161
354
  // Create and export the model
162
355
  const ObjectLayerModel = model('ObjectLayer', ObjectLayerSchema);
163
356
  const ProviderSchema = ObjectLayerSchema;
@@ -197,4 +390,12 @@ class ObjectLayerDto {
197
390
  },
198
391
  };
199
392
  }
200
- export { ObjectLayerSchema, ObjectLayerModel, ProviderSchema, ObjectLayerDto };
393
+ export {
394
+ ObjectLayerSchema,
395
+ ObjectLayerModel,
396
+ ProviderSchema,
397
+ ObjectLayerDto,
398
+ computeObjectLayerSha256,
399
+ mergeObjectLayerData,
400
+ ITEM_ID_INDEX_NAME,
401
+ };
@@ -215,9 +215,6 @@ class ObjectLayerService {
215
215
  }
216
216
  }
217
217
 
218
- // Compute final SHA-256 with all CIDs
219
- bodyData.sha256 = ObjectLayerEngine.computeSha256(bodyData.data);
220
-
221
218
  // Pin data JSON to IPFS
222
219
  try {
223
220
  const itemId = bodyData.data.item.id;
@@ -232,17 +229,7 @@ class ObjectLayerService {
232
229
  }
233
230
 
234
231
  // Atomic create/replace – ObjectLayer is fully populated with all CIDs
235
- let newObjectLayer;
236
- const existingByItemId = await ObjectLayer.findOne({ 'data.item.id': bodyData.data.item.id });
237
- if (existingByItemId) {
238
- newObjectLayer = await ObjectLayer.findByIdAndUpdate(existingByItemId._id, bodyData, {
239
- returnDocument: 'after',
240
- }).populate('objectLayerRenderFramesId');
241
- } else {
242
- newObjectLayer = await (await new ObjectLayer(bodyData).save()).populate('objectLayerRenderFramesId');
243
- }
244
-
245
- return newObjectLayer;
232
+ return await (await ObjectLayer.upsertByItemId(bodyData)).populate('objectLayerRenderFramesId');
246
233
  };
247
234
 
248
235
  /**
@@ -5,9 +5,8 @@
5
5
  */
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
- import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
8
+ import { getNpmRootPath } from '../server/environment.js';
9
9
  import { pbcopy, shellExec } from '../server/process.js';
10
- import dotenv from 'dotenv';
11
10
  import { loggerFactory, loggerMiddleware } from '../server/logger.js';
12
11
  import fs from 'fs-extra';
13
12
  import path from 'path';
@@ -8,7 +8,7 @@
8
8
  import { shellExec } from '../server/process.js';
9
9
  import fs from 'fs-extra';
10
10
  import { loggerFactory } from '../server/logger.js';
11
- import { getNpmRootPath } from '../server/conf.js';
11
+ import { getNpmRootPath } from '../server/environment.js';
12
12
  import Underpost from '../index.js';
13
13
 
14
14
  const logger = loggerFactory(import.meta);
@@ -4,10 +4,18 @@
4
4
  * @namespace UnderpostCluster
5
5
  */
6
6
 
7
- import { clusterTypeFactory, gatewayApiEnabledFactory, getNpmRootPath, resolveReplicaCount } from '../server/conf.js';
7
+ import { clusterTypeFactory, gatewayApiEnabledFactory, resolveReplicaCount } from '../server/conf.js';
8
+ import { getNpmRootPath, HOST_VOLUME_ROOT } from '../server/environment.js';
8
9
  import { loggerFactory } from '../server/logger.js';
9
10
  import { shellExec } from '../server/process.js';
10
11
  import { crictlCommandFactory, resolveCriSocket } from '../server/cri.js';
12
+ import {
13
+ runSELinuxCommands,
14
+ selinuxContainerSharedContextCommandsFactory,
15
+ selinuxEnforcingCommandsFactory,
16
+ selinuxPackagesCommandFactory,
17
+ selinuxRestoreconCommandFactory,
18
+ } from '../server/selinux.js';
11
19
  import { UNDERPOST_GATEWAY, seedDefaultStatusPage } from '../server/underpost-gateway.js';
12
20
  import {
13
21
  UNDERPOST_INGRESS,
@@ -24,17 +32,31 @@ import Underpost from '../index.js';
24
32
 
25
33
  const logger = loggerFactory(import.meta);
26
34
 
27
- // Pinned Gateway API control plane. The CRD release and the implementation are
28
- // upgraded together, and the pairing is not free choice: Envoy Gateway builds
29
- // against one `sigs.k8s.io/gateway-api` version (v1.8.3 -> v1.5.1) and reads
30
- // fields the older CRD schemas do not define, which the API
31
- // server silently strips. Check the implementation's go.mod before moving
32
- // either pin.
33
35
  const GATEWAY_API_RELEASE = 'v1.5.1';
34
36
  const ENVOY_GATEWAY_VERSION = 'v1.8.3';
35
- // Namespace the upstream Contour render deploys into, and the only one where an
36
- // `app=envoy` selector resolves to its DaemonSet.
37
37
  const CONTOUR_NAMESPACE = 'projectcontour';
38
+ const KUBEADM_CONTAINER_MOUNT_PATHS = ['/etc/kubernetes', '/var/lib/etcd', '/var/lib/calico'];
39
+ const HOST_VOLUME_PATHS = ['/data', '/opt/local-path-provisioner', HOST_VOLUME_ROOT];
40
+ const K3S_SELINUX_PATHS = [
41
+ '/usr/local/bin/k3s',
42
+ '/etc/rancher',
43
+ '/var/lib/rancher',
44
+ '/var/lib/kubelet',
45
+ '/var/lib/cni',
46
+ ];
47
+
48
+ const shareWithContainers = (paths = []) => {
49
+ for (const path of paths) shellExec(`sudo mkdir -p ${path}`);
50
+ runSELinuxCommands(selinuxContainerSharedContextCommandsFactory(paths), { execute: shellExec });
51
+ };
52
+
53
+ const enforceSELinux = (paths = []) => {
54
+ shellExec(
55
+ `if [ -f /etc/redhat-release ] && command -v dnf >/dev/null 2>&1 && { ! command -v restorecon >/dev/null 2>&1 || ! command -v semanage >/dev/null 2>&1; }; then ${selinuxPackagesCommandFactory()}; fi`,
56
+ );
57
+ const commands = selinuxEnforcingCommandsFactory({ restorePaths: paths });
58
+ runSELinuxCommands(commands, { execute: shellExec });
59
+ };
38
60
 
39
61
  /**
40
62
  * @class UnderpostCluster
@@ -227,8 +249,11 @@ class UnderpostCluster {
227
249
  // balancer. The platform exposes services explicitly via Project
228
250
  // Contour / Envoy and NodePort services (see --node-port); leaving the
229
251
  // K3s built-ins enabled would bind the same host ports and conflict.
230
- shellExec(`curl -sfL https://get.k3s.io | sh -s - --disable=traefik --disable=servicelb`);
252
+ shellExec(
253
+ `curl -sfL https://get.k3s.io | sh -s - $(if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then printf '%s' '--selinux'; fi) --disable=traefik --disable=servicelb`,
254
+ );
231
255
  logger.info('K3s installation completed.');
256
+ runSELinuxCommands([selinuxRestoreconCommandFactory(K3S_SELINUX_PATHS)], { execute: shellExec });
232
257
 
233
258
  Underpost.cluster.chown('k3s');
234
259
 
@@ -240,6 +265,7 @@ class UnderpostCluster {
240
265
  } else if (options.kubeadm) {
241
266
  Underpost.cluster.config();
242
267
  Underpost.cluster.natSetup({ underpostRoot });
268
+ shareWithContainers([...KUBEADM_CONTAINER_MOUNT_PATHS, ...HOST_VOLUME_PATHS]);
243
269
  logger.info('Initializing Kubeadm control plane...');
244
270
  // Set default values if not provided
245
271
  const podNetworkCidr = options.podNetworkCidr || '192.168.0.0/16';
@@ -272,6 +298,13 @@ class UnderpostCluster {
272
298
  shellExec(
273
299
  `kubectl apply -f https://cdn.jsdelivr.net/gh/rancher/local-path-provisioner@master/deploy/local-path-storage.yaml`,
274
300
  );
301
+
302
+ // The CNI lands asynchronously, and the node stays NotReady until it
303
+ // does. Block here so the deploy steps that follow (MongoDB and the
304
+ // application workloads) schedule onto a node that can actually run
305
+ // them, instead of racing the Calico rollout.
306
+ logger.info('Waiting for the node to become Ready (Calico rollout)...');
307
+ shellExec(`kubectl wait --for=condition=Ready node --all --timeout=300s`);
275
308
  } else {
276
309
  Underpost.cluster.config();
277
310
  Underpost.cluster.natSetup({ underpostRoot });
@@ -1198,11 +1231,13 @@ EOF
1198
1231
  }
1199
1232
  }
1200
1233
  const liveConf = canHotReload
1201
- ? `${execIngress('cat /tmp/nginx.conf', {
1202
- stdout: true,
1203
- silent: true,
1204
- silentOnError: true,
1205
- }) || ''}`
1234
+ ? `${
1235
+ execIngress('cat /tmp/nginx.conf', {
1236
+ stdout: true,
1237
+ silent: true,
1238
+ silentOnError: true,
1239
+ }) || ''
1240
+ }`
1206
1241
  : '';
1207
1242
  const shouldHotReload = canHotReload && liveConf.trimEnd() !== `${conf}`.trimEnd();
1208
1243
 
@@ -1222,10 +1257,10 @@ EOF
1222
1257
  { silent: true },
1223
1258
  );
1224
1259
  } catch (error) {
1225
- execIngress(
1226
- `sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`,
1227
- { silent: true, silentOnError: true },
1228
- );
1260
+ execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {
1261
+ silent: true,
1262
+ silentOnError: true,
1263
+ });
1229
1264
  throw error;
1230
1265
  }
1231
1266
  }
@@ -1241,10 +1276,10 @@ EOF
1241
1276
  `);
1242
1277
  } catch (error) {
1243
1278
  if (shouldHotReload)
1244
- execIngress(
1245
- `sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`,
1246
- { silent: true, silentOnError: true },
1247
- );
1279
+ execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {
1280
+ silent: true,
1281
+ silentOnError: true,
1282
+ });
1248
1283
  throw error;
1249
1284
  }
1250
1285
  if (shouldHotReload)
@@ -1257,18 +1292,16 @@ EOF
1257
1292
  // ready replica that made canHotReload true. Do not let the caller delete
1258
1293
  // the old route kind until the replacement edge is actually Available.
1259
1294
  if (!canHotReload || changesNode)
1260
- shellExec(
1261
- `kubectl rollout status deployment/${UNDERPOST_INGRESS.name} -n ${namespace} --timeout=5m`,
1262
- { silent: true },
1263
- );
1295
+ shellExec(`kubectl rollout status deployment/${UNDERPOST_INGRESS.name} -n ${namespace} --timeout=5m`, {
1296
+ silent: true,
1297
+ });
1264
1298
  else if (shouldHotReload)
1265
1299
  // `nginx -s reload` returns after signalling the master. Give it one
1266
1300
  // scheduling turn to start the new workers before the caller removes
1267
1301
  // the old stack's route object.
1268
1302
  shellExec('sleep 1', { silent: true });
1269
1303
  shellExec(
1270
- `kubectl wait --for=condition=Available deployment/${UNDERPOST_INGRESS.name} ` +
1271
- `-n ${namespace} --timeout=5m`,
1304
+ `kubectl wait --for=condition=Available deployment/${UNDERPOST_INGRESS.name} ` + `-n ${namespace} --timeout=5m`,
1272
1305
  { silent: true },
1273
1306
  );
1274
1307
  execIngress('nginx -t -c /tmp/nginx.conf', { silent: true });
@@ -1350,26 +1383,17 @@ EOF
1350
1383
  config(options = { underpostRoot: '.' }) {
1351
1384
  const { underpostRoot } = options;
1352
1385
  console.log('Applying host configuration: SELinux, Docker, Containerd, and Sysctl settings.');
1353
- // Disable SELinux (permissive mode)
1354
- shellExec(`sudo setenforce 0`, {
1355
- silentOnError: true,
1356
- });
1357
- shellExec(`sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config`, {
1358
- silentOnError: true,
1359
- });
1386
+ enforceSELinux(['/var/lib/kubelet', '/var/lib/containerd']);
1360
1387
 
1361
1388
  // Enable and start Docker and Kubelet services
1362
1389
  shellExec(`sudo systemctl enable --now docker`); // Docker might not be needed for K3s
1363
1390
  shellExec(`sudo systemctl enable --now kubelet`); // Kubelet might not be needed for K3s (K3s uses its own agent)
1364
1391
 
1365
- // Configure containerd for SystemdCgroup and explicitly disable SELinux
1366
- // This is crucial for kubelet/k3s to interact correctly with containerd
1392
+ // Configure containerd for systemd cgroups and SELinux labeling.
1367
1393
  shellExec(`containerd config default | sudo tee /etc/containerd/config.toml > /dev/null`);
1368
1394
  shellExec(`sudo sed -i -e "s/SystemdCgroup = false/SystemdCgroup = true/g" /etc/containerd/config.toml`);
1369
- // Add a new line to disable SELinux for the runc runtime
1370
- // shellExec(
1371
- // `sudo sed -i '/SystemdCgroup = true/a selinux_disabled = true' /etc/containerd/config.toml`,
1372
- // );
1395
+ shellExec(`sudo sed -i -e "s/enable_selinux = false/enable_selinux = true/g" /etc/containerd/config.toml`);
1396
+ runSELinuxCommands([selinuxRestoreconCommandFactory('/etc/containerd')], { execute: shellExec });
1373
1397
  // Restart docker after containerd config changes. Rocky 9 uses systemctl,
1374
1398
  // not the legacy service command.
1375
1399
  shellExec(`sudo systemctl restart docker || sudo service docker restart || true`);
@@ -1400,7 +1424,7 @@ EOF
1400
1424
  * This applies only what K3s genuinely requires, and every step is guarded
1401
1425
  * so it is a no-op when the relevant tooling is absent (e.g. minimal images
1402
1426
  * without SELinux userspace):
1403
- * - SELinux permissive (only if SELinux tooling is present).
1427
+ * - SELinux remains Enforcing.
1404
1428
  * - swap off (Kubernetes best practice).
1405
1429
  * - br_netfilter + bridge/forward sysctls (pod networking).
1406
1430
  * - inotify limits.
@@ -1415,16 +1439,8 @@ EOF
1415
1439
  // forever (the upstream unit ships TimeoutStartSec=0).
1416
1440
  shellExec(`if systemctl is-active --quiet firewalld; then sudo systemctl disable --now firewalld; fi`);
1417
1441
 
1418
- // SELinux → permissive, but only when the tooling exists. Rocky has it;
1419
- // minimal LXD images may not. K3s also installs k3s-selinux for enforcing
1420
- // mode, so this is a best-effort dev convenience, not a hard requirement.
1421
- shellExec(`if command -v setenforce >/dev/null 2>&1; then sudo setenforce 0; fi`, {
1422
- silentOnError: true,
1423
- });
1424
- shellExec(
1425
- `if [ -f /etc/selinux/config ]; then sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config; fi`,
1426
- { silentOnError: true },
1427
- );
1442
+ enforceSELinux(K3S_SELINUX_PATHS);
1443
+ shareWithContainers(HOST_VOLUME_PATHS);
1428
1444
 
1429
1445
  // Disable swap. `swapoff -a` is a no-op without swap; the sed only edits
1430
1446
  // fstab when a swap line is present.
@@ -1697,17 +1713,8 @@ fi`);
1697
1713
  if (options.removeVolumeHostPaths) Underpost.cluster._cleanHostPathPvs();
1698
1714
  else logger.info(' -> Skipping (pass --remove-volume-host-paths to enable).');
1699
1715
 
1700
- logger.info('Phase 2/7: SELinux permissive + restore contexts (when present)...');
1701
- shellExec(`if command -v setenforce >/dev/null 2>&1; then sudo setenforce 0; fi`);
1702
- shellExec(
1703
- `if [ -f /etc/selinux/config ]; then sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config; fi`,
1704
- );
1705
- shellExec(
1706
- `if command -v restorecon >/dev/null 2>&1 && [ -d /var/lib/kubelet ]; then sudo restorecon -Rv /var/lib/kubelet; fi`,
1707
- );
1708
- shellExec(
1709
- `if command -v restorecon >/dev/null 2>&1 && [ -d /var/lib/containerd ]; then sudo restorecon -Rv /var/lib/containerd; fi`,
1710
- );
1716
+ logger.info('Phase 2/7: Enforcing SELinux and restoring runtime contexts...');
1717
+ enforceSELinux(['/var/lib/kubelet', '/var/lib/containerd']);
1711
1718
 
1712
1719
  logger.info('Phase 3/7: Stopping host kubelet and container runtimes (kubeadm-scope only)...');
1713
1720
  shellExec(`if systemctl is-active --quiet kubelet; then sudo systemctl stop kubelet; fi`);
@@ -1981,10 +1988,8 @@ EOF`);
1981
1988
  shellExec(`sudo rm -f /etc/sysctl.d/99-k8s-ipforward.conf`);
1982
1989
  shellExec(`sudo rm -f /etc/sysctl.d/99-k8s.conf`);
1983
1990
 
1984
- // Restore SELinux to enforcing
1985
- console.log('Restoring SELinux to enforcing mode...');
1986
- // shellExec(`sudo setenforce 1`);
1987
- // shellExec(`sudo sed -i 's/^SELINUX=permissive$/SELINUX=enforcing/' /etc/selinux/config`);
1991
+ console.log('Keeping SELinux in enforcing mode...');
1992
+ enforceSELinux();
1988
1993
 
1989
1994
  console.log('Uninstall process completed.');
1990
1995
  },
package/src/cli/db.js CHANGED
@@ -6,12 +6,19 @@
6
6
  * Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
7
7
  */
8
8
 
9
- import { mergeFile, splitFileFactory, loadConfServerJson, resolveConfSecrets, loadConfInstances } from '../server/conf.js';
9
+ import {
10
+ loadConfInstances,
11
+ loadConfServerJson,
12
+ loadReplicas,
13
+ mergeFile,
14
+ pathPortAssignmentFactory,
15
+ splitFileFactory,
16
+ } from '../server/conf.js';
10
17
  import { loggerFactory } from '../server/logger.js';
11
18
  import { shellExec } from '../server/process.js';
12
19
  import fs from 'fs-extra';
13
20
  import { DataBaseProviderService } from '../db/DataBaseProvider.js';
14
- import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
21
+ import { loadCronDeployEnv } from '../server/cron.js';
15
22
  import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
16
23
  import Underpost from '../index.js';
17
24
  import { timer } from '../client/components/core/CommonJs.js';