hologit 0.43.2 → 0.45.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/README.md +69 -286
- package/index.d.ts +113 -76
- package/lib/Lens.js +218 -31
- package/lib/Projection.js +2 -2
- package/lib/Studio.js +150 -126
- package/package.json +3 -4
package/lib/Studio.js
CHANGED
|
@@ -1,16 +1,54 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
1
2
|
const stream = require('stream');
|
|
2
|
-
const Docker = require('dockerode');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const exitHook = require('async-exit-hook');
|
|
5
5
|
const fs = require('mz/fs');
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
const logger = require('./logger');
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
const studioCache = new Map();
|
|
12
|
-
let hab
|
|
10
|
+
let hab;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper function to execute a Docker CLI command.
|
|
14
|
+
* @param {Array<string>} args - The arguments to pass to the docker command.
|
|
15
|
+
* @param {Object} options - Options for child_process.spawn.
|
|
16
|
+
* @returns {Promise<string>} - Resolves with stdout data.
|
|
17
|
+
*/
|
|
18
|
+
function execDocker(args, options = { }) {
|
|
19
|
+
logger.debug(`docker ${args.join(' ')}`);
|
|
20
|
+
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const dockerProcess = spawn('docker', args, { stdio: 'pipe', ...options });
|
|
13
23
|
|
|
24
|
+
if (options.$relayStderr) {
|
|
25
|
+
dockerProcess.stderr.pipe(process.stderr);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.$relayStdout) {
|
|
29
|
+
dockerProcess.stdout.pipe(process.stdout);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
|
|
35
|
+
dockerProcess.stdout.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
dockerProcess.stderr.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
dockerProcess.on('close', (code) => {
|
|
44
|
+
if (code === 0) {
|
|
45
|
+
resolve(stdout.trim());
|
|
46
|
+
} else {
|
|
47
|
+
reject(new Error(stderr.trim()));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
14
52
|
|
|
15
53
|
/**
|
|
16
54
|
* A studio session that can be used to run multiple commands, using chroot if available or docker container
|
|
@@ -23,11 +61,15 @@ class Studio {
|
|
|
23
61
|
for (const [gitDir, studio] of studioCache) {
|
|
24
62
|
const { container } = studio;
|
|
25
63
|
|
|
26
|
-
if (container && container.type
|
|
64
|
+
if (container && container.type !== 'studio') {
|
|
27
65
|
logger.info(`terminating studio container: ${container.id}`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
66
|
+
try {
|
|
67
|
+
await execDocker(['stop', container.id]);
|
|
68
|
+
await execDocker(['rm', container.id]);
|
|
69
|
+
cleanupCount++;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
logger.error(`Failed to stop/remove container ${container.id}: ${err.message}`);
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
74
|
|
|
33
75
|
studioCache.delete(gitDir);
|
|
@@ -46,23 +88,19 @@ class Studio {
|
|
|
46
88
|
return hab;
|
|
47
89
|
}
|
|
48
90
|
|
|
49
|
-
static async getDocker () {
|
|
50
|
-
if (!docker) {
|
|
51
|
-
const { DOCKER_HOST: dockerHost } = process.env;
|
|
52
|
-
const dockerHostMatch = dockerHost && dockerHost.match(/^unix:\/\/(\/.*)$/);
|
|
53
|
-
const socketPath = dockerHostMatch ? dockerHostMatch[1] : '/var/run/docker.sock';
|
|
54
|
-
|
|
55
|
-
docker = new Docker({ socketPath });
|
|
56
|
-
logger.info(`connected to docker on: ${socketPath}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return docker;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
91
|
static async isEnvironmentStudio () {
|
|
63
92
|
return Boolean(process.env.STUDIO_TYPE);
|
|
64
93
|
}
|
|
65
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Execute a Docker CLI command.
|
|
97
|
+
* @param {Array<string>} args - The arguments to pass to the docker command.
|
|
98
|
+
* @param {Object} options - Options for child_process.spawn.
|
|
99
|
+
* @returns {Promise<string>} - Resolves with stdout data.
|
|
100
|
+
*/
|
|
101
|
+
static execDocker(args, options = {}) {
|
|
102
|
+
return execDocker(args, options);
|
|
103
|
+
}
|
|
66
104
|
|
|
67
105
|
static async get (gitDir) {
|
|
68
106
|
const cachedStudio = studioCache.get(gitDir);
|
|
@@ -91,39 +129,9 @@ class Studio {
|
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
|
|
94
|
-
// connect with Docker API
|
|
95
|
-
const docker = await Studio.getDocker();
|
|
96
|
-
|
|
97
|
-
|
|
98
132
|
// pull latest studio container
|
|
99
133
|
try {
|
|
100
|
-
await
|
|
101
|
-
docker.pull('jarvus/hologit-studio:latest', (streamErr, stream) => {
|
|
102
|
-
if (streamErr) {
|
|
103
|
-
reject(streamErr);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let lastStatus;
|
|
108
|
-
|
|
109
|
-
docker.modem.followProgress(
|
|
110
|
-
stream,
|
|
111
|
-
(err, output) => {
|
|
112
|
-
if (err) {
|
|
113
|
-
reject(err);
|
|
114
|
-
} else {
|
|
115
|
-
resolve(output);
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
event => {
|
|
119
|
-
if (event.status != lastStatus) {
|
|
120
|
-
logger.info(`docker pull: ${event.status}`);
|
|
121
|
-
lastStatus = event.status;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
134
|
+
await execDocker(['pull', 'jarvus/hologit-studio:latest'], { $relayStdout: true, $relayStderr: true });
|
|
127
135
|
} catch (err) {
|
|
128
136
|
logger.error(`failed to pull studio image via docker: ${err.message}`);
|
|
129
137
|
}
|
|
@@ -157,65 +165,70 @@ class Studio {
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
|
|
160
|
-
//
|
|
161
|
-
let
|
|
168
|
+
// create studio container
|
|
169
|
+
let containerId;
|
|
170
|
+
let defaultUser;
|
|
162
171
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// }
|
|
191
|
-
// ]
|
|
192
|
-
// }
|
|
193
|
-
}
|
|
194
|
-
});
|
|
172
|
+
// Prepare environment variables
|
|
173
|
+
const envArgs = [
|
|
174
|
+
'--env', 'STUDIO_TYPE=holo',
|
|
175
|
+
'--env', 'GIT_DIR=/git',
|
|
176
|
+
'--env', 'GIT_WORK_TREE=/hab/cache',
|
|
177
|
+
'--env', `DEBUG=${process.env.DEBUG || ''}`,
|
|
178
|
+
'--env', 'HAB_LICENSE=accept-no-persist'
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// Prepare volume bindings
|
|
182
|
+
const volumeArgs = [];
|
|
183
|
+
for (const bind of bindsConfig) {
|
|
184
|
+
volumeArgs.push('-v', bind);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Create container
|
|
188
|
+
const createArgs = [
|
|
189
|
+
'create',
|
|
190
|
+
'--label', 'sh.holo.studio=yes',
|
|
191
|
+
'--workdir', '/git',
|
|
192
|
+
...envArgs,
|
|
193
|
+
...volumeArgs,
|
|
194
|
+
'jarvus/hologit-studio:latest'
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
containerId = await execDocker(createArgs);
|
|
198
|
+
containerId = containerId.split('\n').pop().trim(); // Get the container ID from output
|
|
195
199
|
|
|
196
200
|
logger.info('starting studio container');
|
|
197
|
-
await
|
|
201
|
+
await execDocker(['start', containerId]);
|
|
198
202
|
|
|
199
203
|
const { uid, gid, username } = os.userInfo();
|
|
200
204
|
|
|
201
205
|
if (uid && gid && username) {
|
|
202
206
|
logger.info(`configuring container to use user: ${username}`);
|
|
203
|
-
await containerExec(
|
|
204
|
-
await containerExec(
|
|
205
|
-
await containerExec(
|
|
206
|
-
|
|
207
|
+
await containerExec({ id: containerId }, 'adduser', '-u', `${uid}`, '-G', 'developer', '-D', username);
|
|
208
|
+
await containerExec({ id: containerId }, 'mkdir', '-p', `/home/${username}/.hab`);
|
|
209
|
+
await containerExec({ id: containerId }, 'ln', '-sf', '/hab/cache', `/home/${username}/.hab/`);
|
|
210
|
+
if (!artifactCachePath) await containerExec({ id: containerId }, 'chown', '-R', `${uid}:${gid}`, '/hab/cache');
|
|
211
|
+
defaultUser = `${uid}`;
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
const studio = new Studio({ gitDir, container });
|
|
214
|
+
const studio = new Studio({ gitDir, container: { id: containerId, defaultUser } });
|
|
210
215
|
studioCache.set(gitDir, studio);
|
|
211
216
|
return studio;
|
|
212
217
|
|
|
213
218
|
} catch (err) {
|
|
214
|
-
logger.error(
|
|
219
|
+
logger.error(`container failed: ${err.message}`);
|
|
215
220
|
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
if (containerId) {
|
|
222
|
+
try {
|
|
223
|
+
await execDocker(['stop', containerId]);
|
|
224
|
+
} catch (stopErr) {
|
|
225
|
+
logger.error(`Failed to stop container ${containerId}: ${stopErr.message}`);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
await execDocker(['rm', containerId]);
|
|
229
|
+
} catch (rmErr) {
|
|
230
|
+
logger.error(`Failed to remove container ${containerId}: ${rmErr.message}`);
|
|
231
|
+
}
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
234
|
}
|
|
@@ -227,14 +240,14 @@ class Studio {
|
|
|
227
240
|
}
|
|
228
241
|
|
|
229
242
|
isLocal () {
|
|
230
|
-
return this.container.type
|
|
243
|
+
return this.container.type === 'studio';
|
|
231
244
|
}
|
|
232
245
|
|
|
233
246
|
/**
|
|
234
247
|
* Run a command in the studio
|
|
235
248
|
*/
|
|
236
249
|
async habExec (...command) {
|
|
237
|
-
const options = typeof command[command.length-1]
|
|
250
|
+
const options = typeof command[command.length-1] === 'object'
|
|
238
251
|
? command.pop()
|
|
239
252
|
: {};
|
|
240
253
|
|
|
@@ -274,7 +287,7 @@ class Studio {
|
|
|
274
287
|
// $env: { PATH }
|
|
275
288
|
// }
|
|
276
289
|
// );
|
|
277
|
-
if (logger.level
|
|
290
|
+
if (logger.level === 'debug') {
|
|
278
291
|
command.unshift('--debug');
|
|
279
292
|
}
|
|
280
293
|
|
|
@@ -286,55 +299,66 @@ class Studio {
|
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
async getPackage (query, { install } = { install: false }) {
|
|
289
|
-
let packagePath
|
|
302
|
+
let packagePath;
|
|
303
|
+
try {
|
|
304
|
+
packagePath = await this.habExec('pkg', 'path', query, { $nullOnError: true, $relayStderr: false });
|
|
305
|
+
} catch (err) {
|
|
306
|
+
packagePath = null;
|
|
307
|
+
}
|
|
290
308
|
|
|
291
309
|
if (!packagePath && install) {
|
|
292
310
|
await this.habExec('pkg', 'install', query);
|
|
293
|
-
|
|
311
|
+
try {
|
|
312
|
+
packagePath = await this.habExec('pkg', 'path', query);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
packagePath = null;
|
|
315
|
+
}
|
|
294
316
|
}
|
|
295
317
|
|
|
296
318
|
return packagePath ? packagePath.substr(10) : null;
|
|
297
319
|
}
|
|
298
320
|
}
|
|
299
321
|
|
|
300
|
-
|
|
301
322
|
exitHook(callback => Studio.cleanup().then(callback));
|
|
302
323
|
|
|
303
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Executes a command inside the specified Docker container.
|
|
326
|
+
* @param {Object} container - The container object containing at least the `id` and optionally `defaultUser`.
|
|
327
|
+
* @param {...string} command - The command and its arguments to execute.
|
|
328
|
+
* @returns {Promise<string>} - Resolves with the command's stdout output.
|
|
329
|
+
*/
|
|
304
330
|
async function containerExec (container, ...command) {
|
|
305
|
-
const options = typeof command[command.length-1]
|
|
331
|
+
const options = typeof command[command.length-1] === 'object'
|
|
306
332
|
? command.pop()
|
|
307
333
|
: {};
|
|
308
334
|
|
|
309
335
|
logger.info(`studio-exec: ${command.join(' ')}`);
|
|
310
336
|
|
|
311
|
-
const
|
|
337
|
+
const execArgs = ['exec'];
|
|
338
|
+
|
|
339
|
+
if (options.$user) {
|
|
340
|
+
execArgs.push('--user', options.$user);
|
|
341
|
+
} else if (container.defaultUser) {
|
|
342
|
+
execArgs.push('--user', container.defaultUser);
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
if (options.$env) {
|
|
313
|
-
for (const key of Object.
|
|
314
|
-
|
|
346
|
+
for (const [key, value] of Object.entries(options.$env)) {
|
|
347
|
+
execArgs.push('--env', `${key}=${value}`);
|
|
315
348
|
}
|
|
316
349
|
}
|
|
317
350
|
|
|
318
|
-
|
|
319
|
-
Cmd: command,
|
|
320
|
-
AttachStdout: true,
|
|
321
|
-
AttachStderr: options.$relayStderr !== false,
|
|
322
|
-
Env: env,
|
|
323
|
-
User: `${container.defaultUser || options.$user || ''}`
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const execStream = await exec.start();
|
|
351
|
+
execArgs.push(container.id, ...command);
|
|
327
352
|
|
|
328
|
-
|
|
329
|
-
const output =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
353
|
+
try {
|
|
354
|
+
const output = await execDocker(execArgs, { $relayStdout: true, $relayStderr: true });
|
|
355
|
+
return output;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (options.$nullOnError) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
337
362
|
}
|
|
338
363
|
|
|
339
|
-
|
|
340
364
|
module.exports = Studio;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hologit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Hologit automates the projection of layered composite file trees based on flat, declarative plans",
|
|
5
5
|
"repository": "https://github.com/EmergencePlatform/hologit",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
"@iarna/toml": "^2.2.5",
|
|
13
13
|
"async-exit-hook": "^2.0.1",
|
|
14
14
|
"axios": "^1.7.7",
|
|
15
|
-
"chokidar": "^
|
|
15
|
+
"chokidar": "^4.0.1",
|
|
16
16
|
"debounce": "^2.0.0",
|
|
17
|
-
"dockerode": "^4.0.2",
|
|
18
17
|
"fb-watchman": "^2.0.1",
|
|
19
|
-
"git-client": "^1.
|
|
18
|
+
"git-client": "^1.9.3",
|
|
20
19
|
"hab-client": "^1.1.3",
|
|
21
20
|
"handlebars": "^4.7.6",
|
|
22
21
|
"minimatch": "^10.0.1",
|