@git.zone/cli 2.23.0 → 2.25.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/gitzone.cli.js +11 -2
- package/dist_ts/mod_services/classes.dockercontainer.d.ts +81 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +205 -10
- package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -0
- package/dist_ts/mod_services/classes.globalregistry.js +23 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +52 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.js +116 -20
- package/dist_ts/mod_services/classes.servicedatamarker.d.ts +93 -0
- package/dist_ts/mod_services/classes.servicedatamarker.js +166 -0
- package/dist_ts/mod_services/classes.servicemanager.d.ts +91 -5
- package/dist_ts/mod_services/classes.servicemanager.js +274 -64
- package/dist_ts/mod_services/classes.serviceoptions.d.ts +58 -0
- package/dist_ts/mod_services/classes.serviceoptions.js +93 -0
- package/dist_ts/mod_services/classes.servicepruner.d.ts +102 -0
- package/dist_ts/mod_services/classes.servicepruner.js +410 -0
- package/dist_ts/mod_services/helpers.d.ts +15 -0
- package/dist_ts/mod_services/helpers.js +57 -1
- package/dist_ts/mod_services/index.js +307 -53
- package/dist_ts/mod_tools/classes.packagemanager.d.ts +13 -0
- package/dist_ts/mod_tools/classes.packagemanager.js +42 -1
- package/dist_ts/mod_tools/index.js +4 -1
- package/package.json +3 -2
- package/readme.hints.md +105 -0
- package/readme.md +123 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +10 -0
- package/ts/mod_services/classes.dockercontainer.ts +244 -13
- package/ts/mod_services/classes.globalregistry.ts +27 -0
- package/ts/mod_services/classes.serviceconfiguration.ts +148 -27
- package/ts/mod_services/classes.servicedatamarker.ts +228 -0
- package/ts/mod_services/classes.servicemanager.ts +394 -72
- package/ts/mod_services/classes.serviceoptions.ts +135 -0
- package/ts/mod_services/classes.servicepruner.ts +532 -0
- package/ts/mod_services/helpers.ts +60 -0
- package/ts/mod_services/index.ts +437 -58
- package/ts/mod_tools/classes.packagemanager.ts +54 -0
- package/ts/mod_tools/index.ts +5 -0
|
@@ -3,7 +3,46 @@ import * as helpers from './helpers.js';
|
|
|
3
3
|
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
|
|
4
4
|
import { DockerContainer } from './classes.dockercontainer.js';
|
|
5
5
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
|
6
|
+
import {
|
|
7
|
+
getServiceContainerLabels,
|
|
8
|
+
recordServiceDataDirectory,
|
|
9
|
+
serviceImages,
|
|
10
|
+
type TServiceName,
|
|
11
|
+
} from './classes.servicedatamarker.js';
|
|
6
12
|
import { logger } from '../gitzone.logging.js';
|
|
13
|
+
import type { ContainerStatus } from './classes.dockercontainer.js';
|
|
14
|
+
import type { TServiceOptionSource } from './classes.serviceoptions.js';
|
|
15
|
+
|
|
16
|
+
export interface IServiceStatus {
|
|
17
|
+
service: TServiceName;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
container: string;
|
|
20
|
+
status: ContainerStatus;
|
|
21
|
+
port: string;
|
|
22
|
+
connectionString: string;
|
|
23
|
+
dataDirectory: string;
|
|
24
|
+
dataSizeBytes: number;
|
|
25
|
+
/** Only meaningful when the container does not exist yet. */
|
|
26
|
+
portAvailable: boolean | null;
|
|
27
|
+
/** MongoDB only: whether the instance enforces authentication. */
|
|
28
|
+
authEnabled?: boolean;
|
|
29
|
+
/** MongoDB only: whether the auth mode is declared in committed config. */
|
|
30
|
+
authSource?: TServiceOptionSource;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IServicesStatus {
|
|
34
|
+
project: {
|
|
35
|
+
name: string;
|
|
36
|
+
path: string;
|
|
37
|
+
enabledServices: string[];
|
|
38
|
+
};
|
|
39
|
+
services: {
|
|
40
|
+
mongodb: IServiceStatus;
|
|
41
|
+
minio: IServiceStatus;
|
|
42
|
+
elasticsearch: IServiceStatus;
|
|
43
|
+
};
|
|
44
|
+
totalDataBytes: number;
|
|
45
|
+
}
|
|
7
46
|
|
|
8
47
|
export class ServiceManager {
|
|
9
48
|
private config: ServiceConfiguration;
|
|
@@ -31,11 +70,51 @@ export class ServiceManager {
|
|
|
31
70
|
await this.config.loadOrCreate();
|
|
32
71
|
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
|
|
33
72
|
|
|
73
|
+
// The unsafe-exposure guard is deliberately NOT enforced here. init() runs
|
|
74
|
+
// for every project command, so refusing at this point would also block
|
|
75
|
+
// stop, remove, clean, status and logs — the commands needed to recover from
|
|
76
|
+
// the misconfiguration. It is enforced where it matters, on the path that
|
|
77
|
+
// actually starts an unauthenticated database.
|
|
78
|
+
if (!this.config.isMongoAuthEnabled()) {
|
|
79
|
+
logger.log(
|
|
80
|
+
'note',
|
|
81
|
+
`⚠️ MongoDB auth is disabled for this project (loopback only) — ${this.describeMongoAuthSource()}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
34
85
|
// Load service selection from .smartconfig.json
|
|
35
86
|
await this.loadServiceConfiguration();
|
|
36
87
|
|
|
37
88
|
// Validate and update ports if needed
|
|
38
89
|
await this.config.validateAndUpdatePorts();
|
|
90
|
+
|
|
91
|
+
// Any services command run in a project counts as activity. Without this
|
|
92
|
+
// lastActive only advanced on start, so an actively used project could look
|
|
93
|
+
// stale to the reaper.
|
|
94
|
+
await this.globalRegistry.touchProject(process.cwd());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Human phrasing for where the auth mode came from. A declared auth-off is
|
|
99
|
+
* committed to the repository and therefore affects everyone who clones it,
|
|
100
|
+
* so it must never read the same as a local-only choice.
|
|
101
|
+
*/
|
|
102
|
+
private describeMongoAuthSource(): string {
|
|
103
|
+
switch (this.config.getMongoAuthSource()) {
|
|
104
|
+
case 'declared':
|
|
105
|
+
return 'declared in .smartconfig.json (applies to every checkout)';
|
|
106
|
+
case 'local':
|
|
107
|
+
return 'set locally in .nogit/env.json';
|
|
108
|
+
default:
|
|
109
|
+
return 'default';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Expose the resolved service configuration to the command layer.
|
|
115
|
+
*/
|
|
116
|
+
public getConfiguration(): ServiceConfiguration {
|
|
117
|
+
return this.config;
|
|
39
118
|
}
|
|
40
119
|
|
|
41
120
|
/**
|
|
@@ -113,9 +192,32 @@ export class ServiceManager {
|
|
|
113
192
|
}
|
|
114
193
|
|
|
115
194
|
/**
|
|
116
|
-
*
|
|
195
|
+
* Prepare a service's data directory: create it and record tool ownership.
|
|
196
|
+
*
|
|
197
|
+
* The marker written here is what later allows `gitzone services prune` to
|
|
198
|
+
* prove the directory is reclaimable instead of guessing from its path.
|
|
199
|
+
*/
|
|
200
|
+
private async prepareDataDirectory(
|
|
201
|
+
serviceArg: TServiceName,
|
|
202
|
+
dataPathArg: string,
|
|
203
|
+
): Promise<{ [key: string]: string }> {
|
|
204
|
+
await plugins.smartfs.directory(dataPathArg).recursive().create();
|
|
205
|
+
await recordServiceDataDirectory(process.cwd(), serviceArg);
|
|
206
|
+
return getServiceContainerLabels({
|
|
207
|
+
projectPath: process.cwd(),
|
|
208
|
+
service: serviceArg,
|
|
209
|
+
dataPath: dataPathArg,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Register this project with the global registry.
|
|
215
|
+
*
|
|
216
|
+
* Public and called from the command layer after any start path: previously
|
|
217
|
+
* only `startAll` registered, so `gitzone services start mongo` created a
|
|
218
|
+
* container the registry never knew about and prune could never account for.
|
|
117
219
|
*/
|
|
118
|
-
|
|
220
|
+
public async registerWithGlobalRegistry(): Promise<void> {
|
|
119
221
|
const config = this.config.getConfig();
|
|
120
222
|
const containers = this.config.getContainerNames();
|
|
121
223
|
|
|
@@ -157,31 +259,22 @@ export class ServiceManager {
|
|
|
157
259
|
await this.startElasticsearch();
|
|
158
260
|
first = false;
|
|
159
261
|
}
|
|
160
|
-
|
|
161
|
-
// Register with global registry
|
|
162
|
-
await this.registerWithGlobalRegistry();
|
|
163
262
|
}
|
|
164
263
|
|
|
165
264
|
/**
|
|
166
|
-
* Stop
|
|
265
|
+
* Stop every service container belonging to this project.
|
|
266
|
+
*
|
|
267
|
+
* Deliberately not filtered by the enabled-service list: disabling a service
|
|
268
|
+
* previously left its container running with no command able to stop it,
|
|
269
|
+
* which is how projects accumulated forgotten containers. Stopping a service
|
|
270
|
+
* that is not running is a no-op, so covering all three is always safe.
|
|
167
271
|
*/
|
|
168
272
|
public async stopAll(): Promise<void> {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
if (this.isServiceEnabled('minio')) {
|
|
176
|
-
if (!first) console.log();
|
|
177
|
-
await this.stopMinIO();
|
|
178
|
-
first = false;
|
|
179
|
-
}
|
|
180
|
-
if (this.isServiceEnabled('elasticsearch')) {
|
|
181
|
-
if (!first) console.log();
|
|
182
|
-
await this.stopElasticsearch();
|
|
183
|
-
first = false;
|
|
184
|
-
}
|
|
273
|
+
await this.stopMongoDB();
|
|
274
|
+
console.log();
|
|
275
|
+
await this.stopMinIO();
|
|
276
|
+
console.log();
|
|
277
|
+
await this.stopElasticsearch();
|
|
185
278
|
}
|
|
186
279
|
|
|
187
280
|
/**
|
|
@@ -199,20 +292,29 @@ export class ServiceManager {
|
|
|
199
292
|
const containers = this.config.getContainerNames();
|
|
200
293
|
const directories = this.config.getDataDirectories();
|
|
201
294
|
|
|
202
|
-
// Ensure data directory exists
|
|
203
|
-
await
|
|
295
|
+
// Ensure data directory exists and is marked as tool-owned
|
|
296
|
+
await this.prepareDataDirectory('mongodb', directories.mongo);
|
|
204
297
|
|
|
205
298
|
const status = await this.docker.getStatus(containers.mongo);
|
|
206
299
|
|
|
207
|
-
// Containers created before replica-set support
|
|
208
|
-
// must be recreated; the data
|
|
300
|
+
// Containers created before replica-set support, with a changed port, or
|
|
301
|
+
// running the other auth mode must be recreated; the data is preserved.
|
|
209
302
|
let needsRecreate = false;
|
|
210
303
|
if (status !== 'not_exists') {
|
|
211
|
-
const
|
|
304
|
+
const containerCmd = await this.getMongoContainerCmd(containers.mongo);
|
|
305
|
+
const hasReplSet = containerCmd.includes('--replSet');
|
|
306
|
+
const hasKeyFile = containerCmd.includes('--keyFile');
|
|
212
307
|
const portMappings = await this.docker.getPortMappings(containers.mongo);
|
|
213
308
|
const portMatches =
|
|
214
309
|
!!portMappings && portMappings[config.MONGODB_PORT] === config.MONGODB_PORT;
|
|
215
|
-
|
|
310
|
+
const authMatches = hasKeyFile === this.config.isMongoAuthEnabled();
|
|
311
|
+
needsRecreate = !hasReplSet || !portMatches || !authMatches;
|
|
312
|
+
if (!authMatches) {
|
|
313
|
+
logger.log(
|
|
314
|
+
'note',
|
|
315
|
+
` Auth mode changed to ${this.config.isMongoAuthEnabled() ? 'enabled' : 'disabled'}, recreating container...`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
216
318
|
}
|
|
217
319
|
|
|
218
320
|
switch (status) {
|
|
@@ -246,16 +348,26 @@ export class ServiceManager {
|
|
|
246
348
|
break;
|
|
247
349
|
}
|
|
248
350
|
|
|
351
|
+
await this.ensureMongoRootUser();
|
|
249
352
|
await this.ensureMongoReplicaSetInitiated();
|
|
250
353
|
|
|
251
354
|
logger.log('info', ` Container: ${containers.mongo}`);
|
|
252
355
|
logger.log('info', ` Port: ${config.MONGODB_PORT}`);
|
|
356
|
+
if (!this.config.isMongoAuthEnabled()) {
|
|
357
|
+
logger.log(
|
|
358
|
+
'note',
|
|
359
|
+
` ⚠️ Auth: DISABLED — published on loopback only (127.0.0.1), ${this.describeMongoAuthSource()}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
253
362
|
logger.log('info', ` Connection: ${this.config.getMongoConnectionString()}`);
|
|
254
363
|
|
|
255
|
-
// Show Compass connection string
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
364
|
+
// Show Compass connection string. Only meaningful while the database is
|
|
365
|
+
// reachable off-host, which is exactly the auth-enabled case.
|
|
366
|
+
if (this.config.isMongoAuthEnabled()) {
|
|
367
|
+
const networkIp = await helpers.getLocalNetworkIp();
|
|
368
|
+
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin&directConnection=true`;
|
|
369
|
+
logger.log('ok', ` Compass: ${compassString}`);
|
|
370
|
+
}
|
|
259
371
|
}
|
|
260
372
|
|
|
261
373
|
/**
|
|
@@ -265,25 +377,51 @@ export class ServiceManager {
|
|
|
265
377
|
const config = this.config.getConfig();
|
|
266
378
|
const containers = this.config.getContainerNames();
|
|
267
379
|
const directories = this.config.getDataDirectories();
|
|
380
|
+
const authEnabled = this.config.isMongoAuthEnabled();
|
|
381
|
+
|
|
382
|
+
// Never create an unauthenticated instance that is reachable off-host.
|
|
383
|
+
this.config.assertMongoExposureIsSafe();
|
|
268
384
|
|
|
269
|
-
|
|
385
|
+
const labels = getServiceContainerLabels({
|
|
386
|
+
projectPath: process.cwd(),
|
|
387
|
+
service: 'mongodb',
|
|
388
|
+
dataPath: directories.mongo,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Auth + replSet requires a keyfile. Without auth no keyfile is needed, and
|
|
392
|
+
// the root-user env vars must be omitted because the official entrypoint
|
|
393
|
+
// turns them into `--auth`.
|
|
394
|
+
const command = authEnabled
|
|
395
|
+
? `--replSet rs0 --keyFile /data/db/mongo-keyfile --port ${config.MONGODB_PORT} --bind_ip_all`
|
|
396
|
+
: `--replSet rs0 --port ${config.MONGODB_PORT} --bind_ip_all`;
|
|
397
|
+
const environment: { [key: string]: string } = {
|
|
398
|
+
MONGO_INITDB_DATABASE: config.MONGODB_NAME,
|
|
399
|
+
};
|
|
400
|
+
if (authEnabled) {
|
|
401
|
+
environment.MONGO_INITDB_ROOT_USERNAME = config.MONGODB_USER;
|
|
402
|
+
environment.MONGO_INITDB_ROOT_PASSWORD = config.MONGODB_PASS;
|
|
403
|
+
}
|
|
404
|
+
// Publishing scope is the actual exposure control: mongod's --bind_ip_all
|
|
405
|
+
// only binds inside the container's netns.
|
|
406
|
+
const publishHost = authEnabled ? '0.0.0.0' : '127.0.0.1';
|
|
407
|
+
|
|
408
|
+
if (authEnabled) {
|
|
409
|
+
await this.ensureMongoKeyfile(directories.mongo);
|
|
410
|
+
}
|
|
270
411
|
|
|
271
412
|
const success = await this.docker.run({
|
|
272
413
|
name: containers.mongo,
|
|
273
414
|
image: 'mongo:7.0',
|
|
274
415
|
ports: {
|
|
275
|
-
[
|
|
416
|
+
[`${publishHost}:${config.MONGODB_PORT}`]: config.MONGODB_PORT
|
|
276
417
|
},
|
|
277
418
|
volumes: {
|
|
278
419
|
[directories.mongo]: '/data/db'
|
|
279
420
|
},
|
|
280
|
-
environment
|
|
281
|
-
|
|
282
|
-
MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS,
|
|
283
|
-
MONGO_INITDB_DATABASE: config.MONGODB_NAME
|
|
284
|
-
},
|
|
421
|
+
environment,
|
|
422
|
+
labels,
|
|
285
423
|
restart: 'unless-stopped',
|
|
286
|
-
command
|
|
424
|
+
command
|
|
287
425
|
});
|
|
288
426
|
|
|
289
427
|
if (success) {
|
|
@@ -314,12 +452,76 @@ export class ServiceManager {
|
|
|
314
452
|
}
|
|
315
453
|
|
|
316
454
|
/**
|
|
317
|
-
*
|
|
455
|
+
* The mongod argv the container was created with
|
|
318
456
|
*/
|
|
319
|
-
private async
|
|
457
|
+
private async getMongoContainerCmd(containerName: string): Promise<string[]> {
|
|
320
458
|
const info = await this.docker.inspect(containerName);
|
|
321
|
-
|
|
322
|
-
|
|
459
|
+
return info?.[0]?.Config?.Cmd ?? [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Make sure the configured root user exists when auth is enabled.
|
|
464
|
+
*
|
|
465
|
+
* `MONGO_INITDB_ROOT_USERNAME` only takes effect on an empty dbpath, so data
|
|
466
|
+
* that was first created with auth disabled has no user at all. Enabling auth
|
|
467
|
+
* over it would leave an unusable database. MongoDB's localhost exception is
|
|
468
|
+
* the designed bootstrap path for exactly this: while zero users exist, a
|
|
469
|
+
* connection from localhost — which, inside the container, is what this is —
|
|
470
|
+
* may create the first one. If users already exist the exception does not
|
|
471
|
+
* apply and creation fails, so this can never overwrite or escalate anything.
|
|
472
|
+
*/
|
|
473
|
+
private async ensureMongoRootUser(): Promise<void> {
|
|
474
|
+
if (!this.config.isMongoAuthEnabled()) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const config = this.config.getConfig();
|
|
478
|
+
const containers = this.config.getContainerNames();
|
|
479
|
+
|
|
480
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
481
|
+
const probe = await this.docker.execCombined(
|
|
482
|
+
containers.mongo,
|
|
483
|
+
`mongosh --quiet --port ${config.MONGODB_PORT} ` +
|
|
484
|
+
`-u "${config.MONGODB_USER}" -p "${config.MONGODB_PASS}" --authenticationDatabase admin ` +
|
|
485
|
+
`--eval 'print("AUTH_OK")'`,
|
|
486
|
+
);
|
|
487
|
+
if (probe.output.includes('AUTH_OK')) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (probe.output.includes('Authentication failed')) {
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
// A missing or dead container will never become reachable; retrying 30
|
|
494
|
+
// times only delays a failure that is already terminal.
|
|
495
|
+
if (probe.output.includes('No such container') || probe.output.includes('is restarting')) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`MongoDB container "${containers.mongo}" is not running (${probe.output.trim()}). ` +
|
|
498
|
+
`Inspect it with \`gitzone services logs mongo 100\`.`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
// mongod not accepting connections yet
|
|
502
|
+
await plugins.smartdelay.delayFor(1000);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
logger.log('note', ' No usable root user in existing data, bootstrapping...');
|
|
506
|
+
const createScript =
|
|
507
|
+
`db.getSiblingDB("admin").createUser({user: "${config.MONGODB_USER}", ` +
|
|
508
|
+
`pwd: "${config.MONGODB_PASS}", roles: [{role: "root", db: "admin"}]}); ` +
|
|
509
|
+
'print("USER_CREATED");';
|
|
510
|
+
const create = await this.docker.execCombined(
|
|
511
|
+
containers.mongo,
|
|
512
|
+
`mongosh --quiet --port ${config.MONGODB_PORT} --eval '${createScript}'`,
|
|
513
|
+
);
|
|
514
|
+
if (create.output.includes('USER_CREATED')) {
|
|
515
|
+
logger.log('ok', ' Root user created ✓');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
throw new Error(
|
|
520
|
+
`MongoDB rejected the configured credentials for "${containers.mongo}" and the root user could not be ` +
|
|
521
|
+
'bootstrapped (the localhost exception only applies while no users exist). Either correct ' +
|
|
522
|
+
'MONGODB_USER/MONGODB_PASS in .nogit/env.json to match the existing data, or discard the data with ' +
|
|
523
|
+
'`gitzone services clean`.',
|
|
524
|
+
);
|
|
323
525
|
}
|
|
324
526
|
|
|
325
527
|
/**
|
|
@@ -333,9 +535,12 @@ export class ServiceManager {
|
|
|
333
535
|
'if (e.codeName === "NotYetInitialized") { ' +
|
|
334
536
|
`rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:${config.MONGODB_PORT}"}]}); ` +
|
|
335
537
|
'print("RS_INITIATED"); } else { print("RS_ERROR: " + e.message); } }';
|
|
538
|
+
const authArgs = this.config.isMongoAuthEnabled()
|
|
539
|
+
? `-u "${config.MONGODB_USER}" -p "${config.MONGODB_PASS}" --authenticationDatabase admin `
|
|
540
|
+
: '';
|
|
336
541
|
const command =
|
|
337
542
|
`mongosh --quiet --port ${config.MONGODB_PORT} ` +
|
|
338
|
-
|
|
543
|
+
authArgs +
|
|
339
544
|
`--eval '${evalScript}'`;
|
|
340
545
|
|
|
341
546
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
@@ -363,9 +568,9 @@ export class ServiceManager {
|
|
|
363
568
|
const containers = this.config.getContainerNames();
|
|
364
569
|
const directories = this.config.getDataDirectories();
|
|
365
570
|
|
|
366
|
-
// Ensure data directory exists
|
|
367
|
-
await
|
|
368
|
-
|
|
571
|
+
// Ensure data directory exists and is marked as tool-owned
|
|
572
|
+
const minioLabels = await this.prepareDataDirectory('minio', directories.minio);
|
|
573
|
+
|
|
369
574
|
const status = await this.docker.getStatus(containers.minio);
|
|
370
575
|
|
|
371
576
|
switch (status) {
|
|
@@ -396,10 +601,11 @@ export class ServiceManager {
|
|
|
396
601
|
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
|
397
602
|
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
|
398
603
|
},
|
|
604
|
+
labels: minioLabels,
|
|
399
605
|
restart: 'unless-stopped',
|
|
400
606
|
command: 'server /data --console-address ":9001"'
|
|
401
607
|
});
|
|
402
|
-
|
|
608
|
+
|
|
403
609
|
if (success) {
|
|
404
610
|
logger.log('ok', ' Recreated with new ports ✓');
|
|
405
611
|
|
|
@@ -448,10 +654,11 @@ export class ServiceManager {
|
|
|
448
654
|
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
|
449
655
|
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
|
450
656
|
},
|
|
657
|
+
labels: minioLabels,
|
|
451
658
|
restart: 'unless-stopped',
|
|
452
659
|
command: 'server /data --console-address ":9001"'
|
|
453
660
|
});
|
|
454
|
-
|
|
661
|
+
|
|
455
662
|
if (success) {
|
|
456
663
|
logger.log('ok', ' Created and started ✓');
|
|
457
664
|
|
|
@@ -493,8 +700,8 @@ export class ServiceManager {
|
|
|
493
700
|
const containers = this.config.getContainerNames();
|
|
494
701
|
const directories = this.config.getDataDirectories();
|
|
495
702
|
|
|
496
|
-
// Ensure data directory exists
|
|
497
|
-
await
|
|
703
|
+
// Ensure data directory exists and is marked as tool-owned
|
|
704
|
+
const esLabels = await this.prepareDataDirectory('elasticsearch', directories.elasticsearch);
|
|
498
705
|
|
|
499
706
|
const status = await this.docker.getStatus(containers.elasticsearch);
|
|
500
707
|
|
|
@@ -525,6 +732,7 @@ export class ServiceManager {
|
|
|
525
732
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
|
526
733
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
|
527
734
|
},
|
|
735
|
+
labels: esLabels,
|
|
528
736
|
restart: 'unless-stopped'
|
|
529
737
|
});
|
|
530
738
|
|
|
@@ -561,6 +769,7 @@ export class ServiceManager {
|
|
|
561
769
|
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
|
|
562
770
|
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
|
|
563
771
|
},
|
|
772
|
+
labels: esLabels,
|
|
564
773
|
restart: 'unless-stopped'
|
|
565
774
|
});
|
|
566
775
|
|
|
@@ -639,18 +848,89 @@ export class ServiceManager {
|
|
|
639
848
|
}
|
|
640
849
|
}
|
|
641
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Collect service status as structured data.
|
|
853
|
+
*
|
|
854
|
+
* This is the machine-readable surface behind `gitzone services status
|
|
855
|
+
* --json`; consumers such as test suites can read a connection string from it
|
|
856
|
+
* without scraping human output or re-deriving it from `.nogit/env.json`.
|
|
857
|
+
*/
|
|
858
|
+
public async collectStatus(): Promise<IServicesStatus> {
|
|
859
|
+
const config = this.config.getConfig();
|
|
860
|
+
const containers = this.config.getContainerNames();
|
|
861
|
+
const directories = this.config.getDataDirectories();
|
|
862
|
+
|
|
863
|
+
const describe = async (
|
|
864
|
+
service: TServiceName,
|
|
865
|
+
containerName: string,
|
|
866
|
+
dataPath: string,
|
|
867
|
+
port: string,
|
|
868
|
+
connectionString: string,
|
|
869
|
+
): Promise<IServiceStatus> => {
|
|
870
|
+
const status = await this.docker.getStatus(containerName);
|
|
871
|
+
const dataExists = await plugins.smartfs.directory(dataPath).exists();
|
|
872
|
+
return {
|
|
873
|
+
service,
|
|
874
|
+
enabled: this.isServiceEnabled(service),
|
|
875
|
+
container: containerName,
|
|
876
|
+
status,
|
|
877
|
+
port,
|
|
878
|
+
connectionString,
|
|
879
|
+
dataDirectory: dataPath,
|
|
880
|
+
dataSizeBytes: dataExists ? await helpers.getDirectorySize(dataPath) : 0,
|
|
881
|
+
portAvailable:
|
|
882
|
+
status === 'not_exists' ? await helpers.isPortAvailable(parseInt(port)) : null,
|
|
883
|
+
};
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const mongo = await describe(
|
|
887
|
+
'mongodb',
|
|
888
|
+
containers.mongo,
|
|
889
|
+
directories.mongo,
|
|
890
|
+
config.MONGODB_PORT,
|
|
891
|
+
this.config.getMongoConnectionString(),
|
|
892
|
+
);
|
|
893
|
+
mongo.authEnabled = this.config.isMongoAuthEnabled();
|
|
894
|
+
mongo.authSource = this.config.getMongoAuthSource();
|
|
895
|
+
|
|
896
|
+
const minio = await describe(
|
|
897
|
+
'minio',
|
|
898
|
+
containers.minio,
|
|
899
|
+
directories.minio,
|
|
900
|
+
config.S3_PORT,
|
|
901
|
+
`http://${config.S3_HOST}:${config.S3_PORT}`,
|
|
902
|
+
);
|
|
903
|
+
const elasticsearch = await describe(
|
|
904
|
+
'elasticsearch',
|
|
905
|
+
containers.elasticsearch,
|
|
906
|
+
directories.elasticsearch,
|
|
907
|
+
config.ELASTICSEARCH_PORT,
|
|
908
|
+
config.ELASTICSEARCH_URL,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
project: {
|
|
913
|
+
name: config.PROJECT_NAME,
|
|
914
|
+
path: process.cwd(),
|
|
915
|
+
enabledServices: this.enabledServices || [],
|
|
916
|
+
},
|
|
917
|
+
services: { mongodb: mongo, minio, elasticsearch },
|
|
918
|
+
totalDataBytes: mongo.dataSizeBytes + minio.dataSizeBytes + elasticsearch.dataSizeBytes,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
642
922
|
/**
|
|
643
923
|
* Show service status
|
|
644
924
|
*/
|
|
645
925
|
public async showStatus(): Promise<void> {
|
|
646
926
|
helpers.printHeader('Service Status');
|
|
647
|
-
|
|
927
|
+
|
|
648
928
|
const config = this.config.getConfig();
|
|
649
929
|
const containers = this.config.getContainerNames();
|
|
650
|
-
|
|
930
|
+
|
|
651
931
|
logger.log('info', `Project: ${config.PROJECT_NAME}`);
|
|
652
932
|
console.log();
|
|
653
|
-
|
|
933
|
+
|
|
654
934
|
// MongoDB status
|
|
655
935
|
const mongoStatus = await this.docker.getStatus(containers.mongo);
|
|
656
936
|
switch (mongoStatus) {
|
|
@@ -658,8 +938,14 @@ export class ServiceManager {
|
|
|
658
938
|
logger.log('ok', '📦 MongoDB: 🟢 Running');
|
|
659
939
|
logger.log('info', ` ├─ Container: ${containers.mongo}`);
|
|
660
940
|
logger.log('info', ` ├─ Port: ${config.MONGODB_PORT}`);
|
|
941
|
+
if (!this.config.isMongoAuthEnabled()) {
|
|
942
|
+
logger.log(
|
|
943
|
+
'note',
|
|
944
|
+
` ├─ ⚠️ Auth: DISABLED (loopback only), ${this.describeMongoAuthSource()}`,
|
|
945
|
+
);
|
|
946
|
+
}
|
|
661
947
|
logger.log('info', ` ├─ Connection: ${this.config.getMongoConnectionString()}`);
|
|
662
|
-
|
|
948
|
+
|
|
663
949
|
// Show Compass connection string
|
|
664
950
|
const networkIp = await helpers.getLocalNetworkIp();
|
|
665
951
|
const compassString = `mongodb://${config.MONGODB_USER}:${config.MONGODB_PASS}@${networkIp}:${config.MONGODB_PORT}/${config.MONGODB_NAME}?authSource=admin`;
|
|
@@ -752,6 +1038,44 @@ export class ServiceManager {
|
|
|
752
1038
|
}
|
|
753
1039
|
break;
|
|
754
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
await this.showDiskUsage();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Report on-disk size of this project's service data.
|
|
1047
|
+
*
|
|
1048
|
+
* Reporting comes before any deletion: knowing what a project holds is what
|
|
1049
|
+
* makes reclaiming it a decision rather than a guess.
|
|
1050
|
+
*/
|
|
1051
|
+
public async showDiskUsage(): Promise<void> {
|
|
1052
|
+
const directories = this.config.getDataDirectories();
|
|
1053
|
+
const entries: Array<{ label: string; path: string }> = [
|
|
1054
|
+
{ label: 'MongoDB', path: directories.mongo },
|
|
1055
|
+
{ label: 'S3/MinIO', path: directories.minio },
|
|
1056
|
+
{ label: 'Elasticsearch', path: directories.elasticsearch },
|
|
1057
|
+
];
|
|
1058
|
+
|
|
1059
|
+
console.log();
|
|
1060
|
+
logger.log('note', 'Data on disk:');
|
|
1061
|
+
let total = 0;
|
|
1062
|
+
for (const entry of entries) {
|
|
1063
|
+
if (!(await plugins.smartfs.directory(entry.path).exists())) {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const size = await helpers.getDirectorySize(entry.path);
|
|
1067
|
+
total += size;
|
|
1068
|
+
logger.log('info', ` ${entry.label}: ${helpers.formatBytes(size)} (${entry.path})`);
|
|
1069
|
+
}
|
|
1070
|
+
if (total === 0) {
|
|
1071
|
+
logger.log('info', ' No service data directories present');
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
logger.log('info', ` Total: ${helpers.formatBytes(total)}`);
|
|
1075
|
+
logger.log(
|
|
1076
|
+
'note',
|
|
1077
|
+
' Remove containers but keep data: `gitzone services remove`; remove data too: `gitzone services clean`',
|
|
1078
|
+
);
|
|
755
1079
|
}
|
|
756
1080
|
|
|
757
1081
|
/**
|
|
@@ -927,23 +1251,21 @@ export class ServiceManager {
|
|
|
927
1251
|
*/
|
|
928
1252
|
public async cleanData(): Promise<void> {
|
|
929
1253
|
const directories = this.config.getDataDirectories();
|
|
930
|
-
|
|
1254
|
+
const targets: Array<{ label: string; service: TServiceName; path: string }> = [
|
|
1255
|
+
{ label: 'MongoDB', service: 'mongodb', path: directories.mongo },
|
|
1256
|
+
{ label: 'S3/MinIO', service: 'minio', path: directories.minio },
|
|
1257
|
+
{ label: 'Elasticsearch', service: 'elasticsearch', path: directories.elasticsearch },
|
|
1258
|
+
];
|
|
931
1259
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
await
|
|
940
|
-
logger.log('ok',
|
|
941
|
-
cleaned = true;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (await plugins.smartfs.directory(directories.elasticsearch).exists()) {
|
|
945
|
-
await plugins.smartfs.directory(directories.elasticsearch).recursive().delete();
|
|
946
|
-
logger.log('ok', ' Elasticsearch data removed ✓');
|
|
1260
|
+
let cleaned = false;
|
|
1261
|
+
for (const target of targets) {
|
|
1262
|
+
if (!(await plugins.smartfs.directory(target.path).exists())) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
// Escalates to a privileged one-off container when needed; throws rather
|
|
1266
|
+
// than leaving a partially deleted (and for MongoDB, corrupt) directory.
|
|
1267
|
+
await this.docker.removeDataDirectory(target.path, serviceImages[target.service]);
|
|
1268
|
+
logger.log('ok', ` ${target.label} data removed ✓`);
|
|
947
1269
|
cleaned = true;
|
|
948
1270
|
}
|
|
949
1271
|
|