@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.
- package/.env.example +16 -0
- package/.github/workflows/coverall.cyberia.ci.yml +87 -0
- package/.github/workflows/cyberia-client.cd.yml +1 -18
- package/.github/workflows/cyberia-server.cd.yml +1 -18
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.cd.yml +3 -78
- package/.github/workflows/ghpkg.ci.yml +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +110 -1
- package/CLI-HELP.md +139 -9
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/bin/build.js +7 -5
- package/bin/cyberia.js +73 -53
- package/bin/deploy.js +1 -1
- package/bin/index.js +73 -53
- package/compose.env +16 -0
- package/conf.js +2 -0
- package/deploy/dd-cyberia/deploy.sh +42 -0
- package/deploy/dd-cyberia/init.sh +63 -0
- package/deploy/dd-cyberia/sync-deploy.sh +194 -0
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/deployment.yaml +1 -1
- package/docker-compose.yml +31 -37
- package/hardhat/package-lock.json +9 -9
- package/hardhat/package.json +3 -3
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +2 -2
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/coverall-test-cyberia.sh +24 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +1 -1
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -1
- package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -3
- package/src/api/object-layer/object-layer.model.js +204 -3
- package/src/api/object-layer/object-layer.service.js +1 -14
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +73 -68
- package/src/cli/db.js +9 -2
- package/src/cli/deploy.js +21 -5
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +0 -1
- package/src/cli/index.js +121 -9
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +11 -9
- package/src/cli/run.js +36 -9
- package/src/cli/ssh.js +198 -77
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
- package/src/client/services/object-layer/object-layer.management.js +4 -4
- package/src/client-builder/client-build.js +20 -14
- package/src/db/mongo/MongooseDB.js +4 -0
- package/src/index.js +25 -1
- package/src/projects/cyberia/catalog-cyberia.js +5 -0
- package/src/projects/cyberia/generate-saga.js +14 -23
- package/src/projects/cyberia/instance-data.js +21 -2
- package/src/projects/cyberia/object-layer.js +11 -21
- package/src/projects/underpost/catalog-underpost.js +4 -1
- package/src/runtime/cyberia-client/Dockerfile +1 -1
- package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
- package/src/runtime/cyberia-server/Dockerfile +1 -1
- package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/compose.env +16 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +18 -108
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +20 -10
- package/src/server/underpost-ingress.js +18 -2
- package/test/cyberia-instance-conf-defaults.test.js +1 -0
- package/test/selinux.test.js +71 -0
- package/test/underpost-gateway.test.js +41 -0
- package/test/underpost-ingress.test.js +52 -0
- 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
|
-
{
|
|
943
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
/**
|
package/src/cli/baremetal.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import { getNpmRootPath
|
|
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';
|
package/src/cli/cloud-init.js
CHANGED
|
@@ -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/
|
|
11
|
+
import { getNpmRootPath } from '../server/environment.js';
|
|
12
12
|
import Underpost from '../index.js';
|
|
13
13
|
|
|
14
14
|
const logger = loggerFactory(import.meta);
|
package/src/cli/cluster.js
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
* @namespace UnderpostCluster
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { clusterTypeFactory, gatewayApiEnabledFactory,
|
|
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(
|
|
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
|
-
? `${
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1370
|
-
|
|
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
|
|
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
|
-
|
|
1419
|
-
|
|
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
|
|
1701
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
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 {
|
|
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 {
|
|
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';
|