@git.zone/cli 2.22.0 → 2.24.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/mod_services/classes.dockercontainer.d.ts +92 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +225 -6
- 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 +39 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.js +89 -22
- 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 +103 -3
- package/dist_ts/mod_services/classes.servicemanager.js +352 -100
- 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/package.json +4 -3
- package/readme.hints.md +88 -0
- package/readme.md +71 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_services/classes.dockercontainer.ts +269 -9
- package/ts/mod_services/classes.globalregistry.ts +27 -0
- package/ts/mod_services/classes.serviceconfiguration.ts +105 -24
- package/ts/mod_services/classes.servicedatamarker.ts +228 -0
- package/ts/mod_services/classes.servicemanager.ts +480 -117
- 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
|
@@ -2,6 +2,14 @@ import * as plugins from './mod.plugins.js';
|
|
|
2
2
|
import * as helpers from './helpers.js';
|
|
3
3
|
import { logger } from '../gitzone.logging.js';
|
|
4
4
|
import { DockerContainer } from './classes.dockercontainer.js';
|
|
5
|
+
import { getServiceDataDirectory } from './classes.servicedatamarker.js';
|
|
6
|
+
|
|
7
|
+
/** Hosts that are considered local for the purposes of the no-auth guard. */
|
|
8
|
+
const localMongoHosts = ['localhost', '127.0.0.1', '::1'];
|
|
9
|
+
|
|
10
|
+
export const isLocalMongoHost = (hostArg: string): boolean => {
|
|
11
|
+
return localMongoHosts.includes(hostArg.trim().toLowerCase());
|
|
12
|
+
};
|
|
5
13
|
|
|
6
14
|
export interface IServiceConfig {
|
|
7
15
|
PROJECT_NAME: string;
|
|
@@ -11,6 +19,12 @@ export interface IServiceConfig {
|
|
|
11
19
|
MONGODB_USER: string;
|
|
12
20
|
MONGODB_PASS: string;
|
|
13
21
|
MONGODB_URL: string;
|
|
22
|
+
/**
|
|
23
|
+
* When false, mongod runs without authentication and is published on
|
|
24
|
+
* loopback only. Opt-in, for runtimes whose `node:crypto` cannot complete a
|
|
25
|
+
* SCRAM handshake (notably Deno). Defaults to true.
|
|
26
|
+
*/
|
|
27
|
+
MONGODB_AUTH_ENABLED: boolean;
|
|
14
28
|
S3_HOST: string;
|
|
15
29
|
S3_PORT: string;
|
|
16
30
|
S3_CONSOLE_PORT: string;
|
|
@@ -61,6 +75,67 @@ export class ServiceConfiguration {
|
|
|
61
75
|
public getConfig(): IServiceConfig {
|
|
62
76
|
return this.config;
|
|
63
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether mongod should enforce authentication. Defaults to true; only an
|
|
81
|
+
* explicit `false` in `.nogit/env.json` disables it.
|
|
82
|
+
*/
|
|
83
|
+
public isMongoAuthEnabled(): boolean {
|
|
84
|
+
return this.config.MONGODB_AUTH_ENABLED !== false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build MONGODB_URL from the current fields.
|
|
89
|
+
*
|
|
90
|
+
* Credentials are omitted entirely when auth is disabled, so the stored URL
|
|
91
|
+
* never advertises a username that mongod would not accept.
|
|
92
|
+
*/
|
|
93
|
+
private buildMongoUrl(): string {
|
|
94
|
+
const credentials = this.isMongoAuthEnabled()
|
|
95
|
+
? `${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@`
|
|
96
|
+
: '';
|
|
97
|
+
const authQuery = this.isMongoAuthEnabled() ? '?authSource=admin' : '';
|
|
98
|
+
return `mongodb://${credentials}${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}${authQuery}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Recompute every derived field from the primitive ones.
|
|
103
|
+
*/
|
|
104
|
+
private updateDerivedFields(): void {
|
|
105
|
+
this.config.MONGODB_URL = this.buildMongoUrl();
|
|
106
|
+
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
|
107
|
+
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Refuse an unauthenticated mongod that would be reachable off-host.
|
|
112
|
+
*
|
|
113
|
+
* Disabling auth is only defensible while the database is bound to loopback.
|
|
114
|
+
* Rather than trusting the operator to keep MONGODB_HOST local, this fails
|
|
115
|
+
* closed on the combination.
|
|
116
|
+
*/
|
|
117
|
+
public assertMongoExposureIsSafe(): void {
|
|
118
|
+
if (this.isMongoAuthEnabled()) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!isLocalMongoHost(this.config.MONGODB_HOST)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Refusing to run MongoDB without authentication on non-local host "${this.config.MONGODB_HOST}". ` +
|
|
124
|
+
'Set MONGODB_HOST to localhost in .nogit/env.json, or re-enable auth with ' +
|
|
125
|
+
'`gitzone services auth mongodb on`.',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Persist the MongoDB auth mode and recompute derived fields.
|
|
132
|
+
*/
|
|
133
|
+
public async setMongoAuthEnabled(enabledArg: boolean): Promise<void> {
|
|
134
|
+
this.config.MONGODB_AUTH_ENABLED = enabledArg;
|
|
135
|
+
this.updateDerivedFields();
|
|
136
|
+
this.assertMongoExposureIsSafe();
|
|
137
|
+
await this.saveConfig();
|
|
138
|
+
}
|
|
64
139
|
|
|
65
140
|
/**
|
|
66
141
|
* Save the configuration to file
|
|
@@ -132,6 +207,7 @@ export class ServiceConfiguration {
|
|
|
132
207
|
MONGODB_USER: mongoUser,
|
|
133
208
|
MONGODB_PASS: mongoPass,
|
|
134
209
|
MONGODB_URL: `mongodb://${mongoUser}:${mongoPass}@${mongoHost}:${mongoPortStr}/${mongoName}?authSource=admin`,
|
|
210
|
+
MONGODB_AUTH_ENABLED: true,
|
|
135
211
|
S3_HOST: s3Host,
|
|
136
212
|
S3_PORT: s3PortStr,
|
|
137
213
|
S3_CONSOLE_PORT: s3ConsolePort.toString(),
|
|
@@ -202,9 +278,15 @@ export class ServiceConfiguration {
|
|
|
202
278
|
updated = true;
|
|
203
279
|
}
|
|
204
280
|
|
|
281
|
+
if (this.config.MONGODB_AUTH_ENABLED === undefined) {
|
|
282
|
+
this.config.MONGODB_AUTH_ENABLED = true;
|
|
283
|
+
fieldsAdded.push('MONGODB_AUTH_ENABLED');
|
|
284
|
+
updated = true;
|
|
285
|
+
}
|
|
286
|
+
|
|
205
287
|
// Always update MONGODB_URL based on current settings
|
|
206
288
|
const oldUrl = this.config.MONGODB_URL;
|
|
207
|
-
this.config.MONGODB_URL =
|
|
289
|
+
this.config.MONGODB_URL = this.buildMongoUrl();
|
|
208
290
|
if (oldUrl !== this.config.MONGODB_URL) {
|
|
209
291
|
fieldsAdded.push('MONGODB_URL');
|
|
210
292
|
updated = true;
|
|
@@ -254,7 +336,9 @@ export class ServiceConfiguration {
|
|
|
254
336
|
updated = true;
|
|
255
337
|
}
|
|
256
338
|
|
|
257
|
-
|
|
339
|
+
// `undefined`, not falsy: a stored `false` is a valid value, and treating it
|
|
340
|
+
// as missing made every run report S3_USESSL as newly added.
|
|
341
|
+
if (this.config.S3_USESSL === undefined) {
|
|
258
342
|
this.config.S3_USESSL = false;
|
|
259
343
|
fieldsAdded.push('S3_USESSL');
|
|
260
344
|
updated = true;
|
|
@@ -313,7 +397,11 @@ export class ServiceConfiguration {
|
|
|
313
397
|
*/
|
|
314
398
|
public getMongoConnectionString(useNetworkIp: boolean = false): string {
|
|
315
399
|
const host = useNetworkIp ? '${networkIp}' : this.config.MONGODB_HOST;
|
|
316
|
-
|
|
400
|
+
const credentials = this.isMongoAuthEnabled()
|
|
401
|
+
? `${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@`
|
|
402
|
+
: '';
|
|
403
|
+
const authQuery = this.isMongoAuthEnabled() ? 'authSource=admin&' : '';
|
|
404
|
+
return `mongodb://${credentials}${host}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?${authQuery}directConnection=true`;
|
|
317
405
|
}
|
|
318
406
|
|
|
319
407
|
/**
|
|
@@ -328,13 +416,16 @@ export class ServiceConfiguration {
|
|
|
328
416
|
}
|
|
329
417
|
|
|
330
418
|
/**
|
|
331
|
-
* Get data directories
|
|
419
|
+
* Get data directories.
|
|
420
|
+
*
|
|
421
|
+
* Derived through `getServiceDataDirectory` so that creation here and
|
|
422
|
+
* reclamation in `ServicePruner` can never disagree about the shape.
|
|
332
423
|
*/
|
|
333
424
|
public getDataDirectories() {
|
|
334
425
|
return {
|
|
335
|
-
mongo:
|
|
336
|
-
minio:
|
|
337
|
-
elasticsearch:
|
|
426
|
+
mongo: getServiceDataDirectory(process.cwd(), 'mongodb'),
|
|
427
|
+
minio: getServiceDataDirectory(process.cwd(), 'minio'),
|
|
428
|
+
elasticsearch: getServiceDataDirectory(process.cwd(), 'elasticsearch')
|
|
338
429
|
};
|
|
339
430
|
}
|
|
340
431
|
|
|
@@ -349,8 +440,10 @@ export class ServiceConfiguration {
|
|
|
349
440
|
const mongoStatus = await this.docker.getStatus(containers.mongo);
|
|
350
441
|
if (mongoStatus !== 'not_exists') {
|
|
351
442
|
const portMappings = await this.docker.getPortMappings(containers.mongo);
|
|
352
|
-
|
|
353
|
-
|
|
443
|
+
// legacy containers map 27017; replica-set containers map port:port
|
|
444
|
+
const dockerPort =
|
|
445
|
+
portMappings && (portMappings['27017'] ?? Object.values(portMappings)[0]);
|
|
446
|
+
if (dockerPort) {
|
|
354
447
|
if (this.config.MONGODB_PORT !== dockerPort) {
|
|
355
448
|
logger.log('note', `📍 Syncing MongoDB port from Docker: ${dockerPort}`);
|
|
356
449
|
this.config.MONGODB_PORT = dockerPort;
|
|
@@ -398,11 +491,7 @@ export class ServiceConfiguration {
|
|
|
398
491
|
}
|
|
399
492
|
|
|
400
493
|
if (updated) {
|
|
401
|
-
|
|
402
|
-
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
|
403
|
-
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
|
404
|
-
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
|
405
|
-
|
|
494
|
+
this.updateDerivedFields();
|
|
406
495
|
await this.saveConfig();
|
|
407
496
|
logger.log('ok', '✅ Configuration synced with Docker containers');
|
|
408
497
|
}
|
|
@@ -468,11 +557,7 @@ export class ServiceConfiguration {
|
|
|
468
557
|
}
|
|
469
558
|
|
|
470
559
|
if (updated) {
|
|
471
|
-
|
|
472
|
-
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
|
473
|
-
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
|
474
|
-
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
|
475
|
-
|
|
560
|
+
this.updateDerivedFields();
|
|
476
561
|
await this.saveConfig();
|
|
477
562
|
}
|
|
478
563
|
|
|
@@ -502,11 +587,7 @@ export class ServiceConfiguration {
|
|
|
502
587
|
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
|
|
503
588
|
this.config.ELASTICSEARCH_PORT = esPort;
|
|
504
589
|
|
|
505
|
-
|
|
506
|
-
this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`;
|
|
507
|
-
this.config.S3_ENDPOINT = this.config.S3_HOST;
|
|
508
|
-
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
|
|
509
|
-
|
|
590
|
+
this.updateDerivedFields();
|
|
510
591
|
await this.saveConfig();
|
|
511
592
|
|
|
512
593
|
logger.log('ok', '✅ New port configuration:');
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as plugins from './mod.plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical service identifiers used across mod_services.
|
|
5
|
+
*
|
|
6
|
+
* These intentionally mirror the values stored in `@git.zone/cli.services`.
|
|
7
|
+
* That configuration array is a *deployment* input consumed by
|
|
8
|
+
* `@git.zone/tsdeploy` (`deriveRequiredCapabilities`), so the strings and the
|
|
9
|
+
* array shape must never change here.
|
|
10
|
+
*/
|
|
11
|
+
export type TServiceName = 'mongodb' | 'minio' | 'elasticsearch';
|
|
12
|
+
|
|
13
|
+
export const serviceNames: TServiceName[] = ['mongodb', 'minio', 'elasticsearch'];
|
|
14
|
+
|
|
15
|
+
/** Owner recorded in markers and container labels. */
|
|
16
|
+
export const serviceDataOwner = '@git.zone/cli';
|
|
17
|
+
|
|
18
|
+
/** Value of the `git.zone.tool` label on containers created by `gitzone services`. */
|
|
19
|
+
export const serviceToolLabel = 'gitzone-services';
|
|
20
|
+
|
|
21
|
+
/** Marker file name, stored in `<projectPath>/.nogit/`. */
|
|
22
|
+
export const serviceDataMarkerFile = '.gitzone-services.json';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Directory basename used below `.nogit/` for each service's bind-mounted data.
|
|
26
|
+
* This mapping is the only shape `gitzone services` ever creates, and prune
|
|
27
|
+
* refuses to touch anything that does not match it exactly.
|
|
28
|
+
*/
|
|
29
|
+
export const serviceDataDirectoryNames: { [key in TServiceName]: string } = {
|
|
30
|
+
mongodb: 'mongodata',
|
|
31
|
+
minio: 'miniodata',
|
|
32
|
+
elasticsearch: 'esdata',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Image used to run each service, and therefore the image available to perform
|
|
37
|
+
* a privileged cleanup of that service's data directory: if the data exists,
|
|
38
|
+
* the service ran, so its image is present locally.
|
|
39
|
+
*/
|
|
40
|
+
export const serviceImages: { [key in TServiceName]: string } = {
|
|
41
|
+
mongodb: 'mongo:7.0',
|
|
42
|
+
minio: 'minio/minio',
|
|
43
|
+
elasticsearch: 'elasticsearch:8.11.0',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface IServiceDataMarkerEntry {
|
|
47
|
+
service: TServiceName;
|
|
48
|
+
path: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface IServiceDataMarker {
|
|
52
|
+
owner: string;
|
|
53
|
+
kind: 'service-data';
|
|
54
|
+
schemaVersion: 1;
|
|
55
|
+
projectPath: string;
|
|
56
|
+
safeToPrune: true;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
dataDirectories: IServiceDataMarkerEntry[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
|
62
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Absolute path of the marker file for a project. */
|
|
66
|
+
export const getServiceDataMarkerPath = (projectPathArg: string): string => {
|
|
67
|
+
return plugins.path.join(plugins.path.resolve(projectPathArg), '.nogit', serviceDataMarkerFile);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Expected absolute data directory path for a service in a project.
|
|
72
|
+
* `mod_services` must always derive data paths through this helper so that
|
|
73
|
+
* creation and reclamation agree on a single shape.
|
|
74
|
+
*/
|
|
75
|
+
export const getServiceDataDirectory = (
|
|
76
|
+
projectPathArg: string,
|
|
77
|
+
serviceArg: TServiceName,
|
|
78
|
+
): string => {
|
|
79
|
+
return plugins.path.join(
|
|
80
|
+
plugins.path.resolve(projectPathArg),
|
|
81
|
+
'.nogit',
|
|
82
|
+
serviceDataDirectoryNames[serviceArg],
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* True only when `candidatePathArg` is exactly one of the three data
|
|
88
|
+
* directories `gitzone services` creates for `projectPathArg`.
|
|
89
|
+
*
|
|
90
|
+
* This is an allowlist, not a denylist: any path that is not byte-for-byte one
|
|
91
|
+
* of the expected resolved paths is rejected. It is the last line of defence
|
|
92
|
+
* before a recursive delete.
|
|
93
|
+
*/
|
|
94
|
+
export const isSafeServiceDataPath = (
|
|
95
|
+
projectPathArg: string,
|
|
96
|
+
candidatePathArg: string,
|
|
97
|
+
): boolean => {
|
|
98
|
+
const candidate = plugins.path.resolve(candidatePathArg);
|
|
99
|
+
for (const service of serviceNames) {
|
|
100
|
+
if (getServiceDataDirectory(projectPathArg, service) === candidate) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Validate an unknown value as a usable marker. */
|
|
108
|
+
export const isValidServiceDataMarker = (markerArg: unknown): markerArg is IServiceDataMarker => {
|
|
109
|
+
if (!isPlainObject(markerArg)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (markerArg.owner !== serviceDataOwner) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (markerArg.kind !== 'service-data') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (markerArg.schemaVersion !== 1) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (markerArg.safeToPrune !== true) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (typeof markerArg.projectPath !== 'string' || !markerArg.projectPath) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(markerArg.dataDirectories)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
for (const entry of markerArg.dataDirectories) {
|
|
131
|
+
if (!isPlainObject(entry)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (!serviceNames.includes(entry.service)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (typeof entry.path !== 'string' || !entry.path) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Read and validate the marker for a project, or undefined when absent/invalid. */
|
|
145
|
+
export const readServiceDataMarker = async (
|
|
146
|
+
projectPathArg: string,
|
|
147
|
+
): Promise<IServiceDataMarker | undefined> => {
|
|
148
|
+
const markerPath = getServiceDataMarkerPath(projectPathArg);
|
|
149
|
+
try {
|
|
150
|
+
if (!(await plugins.smartfs.file(markerPath).exists())) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const content = (await plugins.smartfs.file(markerPath).encoding('utf8').read()) as string;
|
|
154
|
+
const parsed = JSON.parse(content);
|
|
155
|
+
return isValidServiceDataMarker(parsed) ? parsed : undefined;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Record that `gitzone services` owns a data directory for this project.
|
|
163
|
+
*
|
|
164
|
+
* The marker lives in `.nogit/` rather than inside the bind-mounted data
|
|
165
|
+
* directory itself, so it can never confuse mongod, MinIO or Elasticsearch at
|
|
166
|
+
* runtime. Entries are merged so enabling a service later does not drop the
|
|
167
|
+
* claim on previously created directories.
|
|
168
|
+
*/
|
|
169
|
+
export const recordServiceDataDirectory = async (
|
|
170
|
+
projectPathArg: string,
|
|
171
|
+
serviceArg: TServiceName,
|
|
172
|
+
): Promise<void> => {
|
|
173
|
+
const projectPath = plugins.path.resolve(projectPathArg);
|
|
174
|
+
const dataPath = getServiceDataDirectory(projectPath, serviceArg);
|
|
175
|
+
const existingMarker = await readServiceDataMarker(projectPath);
|
|
176
|
+
|
|
177
|
+
const entries: IServiceDataMarkerEntry[] = existingMarker
|
|
178
|
+
? existingMarker.dataDirectories.filter((entry) => entry.service !== serviceArg)
|
|
179
|
+
: [];
|
|
180
|
+
entries.push({ service: serviceArg, path: dataPath });
|
|
181
|
+
entries.sort((entryA, entryB) => entryA.service.localeCompare(entryB.service));
|
|
182
|
+
|
|
183
|
+
const marker: IServiceDataMarker = {
|
|
184
|
+
owner: serviceDataOwner,
|
|
185
|
+
kind: 'service-data',
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
projectPath,
|
|
188
|
+
safeToPrune: true,
|
|
189
|
+
createdAt: existingMarker?.createdAt || new Date().toISOString(),
|
|
190
|
+
dataDirectories: entries,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const markerPath = getServiceDataMarkerPath(projectPath);
|
|
194
|
+
await plugins.smartfs.directory(plugins.path.dirname(markerPath)).recursive().create();
|
|
195
|
+
await plugins.smartfs
|
|
196
|
+
.file(markerPath)
|
|
197
|
+
.encoding('utf8')
|
|
198
|
+
.write(`${JSON.stringify(marker, null, 2)}\n`);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Labels applied to every container `gitzone services` creates.
|
|
203
|
+
*
|
|
204
|
+
* Mirrors the `git.zone.*` namespace already established by
|
|
205
|
+
* `@git.zone/tsdocker` so prune logic can identify tool-owned resources
|
|
206
|
+
* without ever matching on image or bare name.
|
|
207
|
+
*/
|
|
208
|
+
export const getServiceContainerLabels = (optionsArg: {
|
|
209
|
+
projectPath: string;
|
|
210
|
+
service: TServiceName;
|
|
211
|
+
dataPath: string;
|
|
212
|
+
}): { [key: string]: string } => {
|
|
213
|
+
return {
|
|
214
|
+
'git.zone.tool': serviceToolLabel,
|
|
215
|
+
'git.zone.service': optionsArg.service,
|
|
216
|
+
'git.zone.project-path': plugins.path.resolve(optionsArg.projectPath),
|
|
217
|
+
'git.zone.data-path': plugins.path.resolve(optionsArg.dataPath),
|
|
218
|
+
'git.zone.safe-to-prune': 'true',
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** True when a container's labels prove `gitzone services` created it. */
|
|
223
|
+
export const isServiceOwnedByLabels = (labelsArg: { [key: string]: string }): boolean => {
|
|
224
|
+
return (
|
|
225
|
+
labelsArg['git.zone.tool'] === serviceToolLabel &&
|
|
226
|
+
labelsArg['git.zone.safe-to-prune'] === 'true'
|
|
227
|
+
);
|
|
228
|
+
};
|