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/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, docker;
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 != 'studio') {
64
+ if (container && container.type !== 'studio') {
27
65
  logger.info(`terminating studio container: ${container.id}`);
28
- await container.stop();
29
- await container.remove();
30
- cleanupCount++;
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 new Promise((resolve, reject) => {
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
- // start studio container
161
- let container;
168
+ // create studio container
169
+ let containerId;
170
+ let defaultUser;
162
171
  try {
163
- container = await docker.createContainer({
164
- Image: 'jarvus/hologit-studio',
165
- Labels: {
166
- 'sh.holo.studio': 'yes'
167
- },
168
- AttachStdin: false,
169
- AttachStdout: true,
170
- AttachStderr: true,
171
- Env: [
172
- 'STUDIO_TYPE=holo',
173
- 'GIT_DIR=/git',
174
- 'GIT_WORK_TREE=/hab/cache',
175
- `DEBUG=${process.env.DEBUG||''}`,
176
- `HAB_LICENSE=accept-no-persist`
177
- ],
178
- WorkingDir: '/git',
179
- Volumes: volumesConfig,
180
- HostConfig: {
181
- Binds: bindsConfig,
182
- // ExposedPorts: {
183
- // "9229/tcp": { }
184
- // },
185
- // PortBindings: {
186
- // '9229/tcp': [
187
- // {
188
- // HostIp: '0.0.0.0',
189
- // HostPort: '9229'
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 container.start();
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(container, 'adduser', '-u', `${uid}`, '-G', 'developer', '-D', username);
204
- await containerExec(container, 'mkdir', '-p', `/home/${username}/.hab`);
205
- await containerExec(container, 'ln', '-sf', '/hab/cache', `/home/${username}/.hab/`);
206
- container.defaultUser = `${uid}`;
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('container failed: %o', err);
219
+ logger.error(`container failed: ${err.message}`);
215
220
 
216
- if (container) {
217
- await container.stop();
218
- await container.remove();
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 == 'studio';
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] == 'object'
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 == 'debug') {
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 = await this.habExec('pkg', 'path', query, { $nullOnError: true, $relayStderr: false });
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
- packagePath = await this.habExec('pkg', 'path', query);
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] == 'object'
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 env = [];
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.keys(options.$env)) {
314
- env.push(`${key}=${options.$env[key]}`);
346
+ for (const [key, value] of Object.entries(options.$env)) {
347
+ execArgs.push('--env', `${key}=${value}`);
315
348
  }
316
349
  }
317
350
 
318
- const exec = await container.exec({
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
- return new Promise((resolve, reject) => {
329
- const output = [];
330
- const outputStream = new stream.PassThrough();
331
-
332
- outputStream.on('data', chunk => output.push(chunk.toString('utf8')));
333
- execStream.on('end', () => resolve(output.join('').trim()));
334
-
335
- container.modem.demuxStream(execStream, outputStream, process.stderr);
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.43.2",
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": "^3.5.3",
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.8.3",
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",