Skip to content

Commit 63d920a

Browse files
Merge pull request #367 from JarvusInnovations/feat/docker-lensing
feat: docker-based lensing
2 parents 4adc298 + 9d753f8 commit 63d920a

File tree

4 files changed

+236
-91
lines changed

4 files changed

+236
-91
lines changed

lib/Lens.js

Lines changed: 218 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ class Lens extends Configurable {
4444
const config = await super.getConfig();
4545

4646
// process lens configuration
47-
if (!config.package) {
48-
throw new Error(`hololens has no package defined: ${this.name}`);
47+
if (config.package) {
48+
config.command = config.command || 'lens-tree {{ input }}';
4949
}
5050

51-
config.command = config.command || 'lens-tree {{ input }}';
52-
5351
if (config.before) {
5452
config.before =
5553
typeof config.before == 'string'
@@ -103,7 +101,62 @@ class Lens extends Configurable {
103101
async buildSpec (inputTree) {
104102
const config = await this.getCachedConfig();
105103

104+
if (config.container) {
105+
return this.buildSpecForContainer(inputTree, config);
106+
} else if (config.package) {
107+
return this.buildSpecForHabitatPackage(inputTree, config);
108+
} else {
109+
throw new Error(`hololens has no package or container defined: ${this.name}`);
110+
}
111+
}
112+
113+
async buildSpecForContainer (inputTree, config) {
114+
const { container: containerQuery } = config;
115+
116+
// check if image exists locally first
117+
let imageHash;
118+
try {
119+
const inspectOutput = await Studio.execDocker(['inspect', containerQuery]);
120+
const imageInfo = JSON.parse(inspectOutput)[0];
121+
imageHash = imageInfo.Id;
122+
logger.info(`found local image: ${containerQuery}@${imageHash}`);
123+
} catch (err) {
124+
// image doesn't exist locally or can't be inspected, try pulling
125+
logger.info(`pulling image: ${containerQuery}`);
126+
127+
try {
128+
await Studio.execDocker(['pull', containerQuery], { $relayStdout: true });
129+
const inspectOutput = await Studio.execDocker(['inspect', containerQuery]);
130+
const imageInfo = JSON.parse(inspectOutput)[0];
131+
imageHash = imageInfo.Id;
132+
} catch (err) {
133+
throw new Error(`failed to pull container image ${containerQuery}: ${err.message}`);
134+
}
135+
}
136+
137+
if (!imageHash) {
138+
throw new Error(`failed to get hash for container image ${containerQuery}`);
139+
}
140+
141+
// build spec
142+
const data = {
143+
...config,
144+
container: `${containerQuery.replace(/:.*$/, '')}@${imageHash}`,
145+
input: await inputTree.write(),
146+
output: null,
147+
before: null,
148+
after: null
149+
};
106150

151+
// write spec and return packet
152+
return {
153+
...await SpecObject.write(this.workspace.getRepo(), 'lens', data),
154+
data,
155+
type: 'container'
156+
};
157+
}
158+
159+
async buildSpecForHabitatPackage (inputTree, config) {
107160
// determine current package version
108161
const { package: packageQuery } = config;
109162
const [pkgOrigin, pkgName, pkgVersion, pkgBuild] = packageQuery.split('/');
@@ -163,14 +216,6 @@ class Lens extends Configurable {
163216
}
164217

165218

166-
// old studio method that might be useful as fallback/debug option
167-
// const setupOutput = await studio.exec('hab', 'pkg', 'install', 'core/hab-plan-build');
168-
// const originOutput = await studio.exec('hab', 'origin', 'key', 'generate', 'holo');
169-
// const buildOutput = await studio.habPkgExec('core/hab-plan-build', 'hab-plan-build', '/src/lenses/compass');
170-
// const studio = await Studio.get(this.workspace.getRepo().gitDir);
171-
// let packageIdent = await studio.getPackage(packageQuery);
172-
173-
174219
// build spec
175220
const data = {
176221
...config,
@@ -185,21 +230,21 @@ class Lens extends Configurable {
185230
// write spec and return packet
186231
return {
187232
...await SpecObject.write(this.workspace.getRepo(), 'lens', data),
188-
data
233+
data,
234+
type: 'habitat'
189235
};
190236
}
191237

192-
async executeSpec (specHash, options) {
193-
return Lens.executeSpec(specHash, {...options, repo: this.workspace.getRepo()});
238+
async executeSpec (specType, specHash, options) {
239+
return Lens.executeSpec(specType, specHash, {...options, repo: this.workspace.getRepo()});
194240
}
195241

196-
static async executeSpec (specHash, { refresh=false, save=true, repo=null, cacheFrom=null, cacheTo=null }) {
242+
static async executeSpec (specType, specHash, options) {
243+
const { refresh=false, cacheFrom=null, cacheTo=null, save=true } = options;
197244

198-
// load holorepo
199-
if (!repo) {
200-
repo = await Repo.getFromEnvironment();
201-
}
202245

246+
// load holorepo
247+
const repo = options.repo || await Repo.getFromEnvironment();
203248
const git = await repo.getGit();
204249

205250

@@ -228,9 +273,161 @@ class Lens extends Configurable {
228273
}
229274

230275

231-
// ensure the rest runs inside a studio environment
276+
// execute lens in container or with habitat package:
277+
let lensedTreeHash;
278+
if (specType == 'container') {
279+
lensedTreeHash = await Lens.executeSpecForContainer(repo, specHash);
280+
} else if (specType == 'habitat') {
281+
lensedTreeHash = await Lens.executeSpecForHabitatPackage(repo, specHash);
282+
}
283+
284+
// save ref to accelerate next projection
285+
if (save) {
286+
await git.updateRef(specRef, lensedTreeHash);
287+
288+
if (cacheTo) {
289+
await _cacheResultTo(repo, specRef, cacheTo);
290+
}
291+
}
292+
293+
return lensedTreeHash;
294+
}
295+
296+
static async executeSpecForContainer (repo, specHash) {
297+
const git = await repo.getGit();
298+
299+
// read and parse spec file
300+
const specToml = await git.catFile({ p: true }, specHash);
301+
const {
302+
holospec: {
303+
lens: spec
304+
}
305+
} = TOML.parse(specToml);
306+
307+
// write commit with input tree and spec content
308+
const commitHash = await git.commitTree(spec.input, {
309+
p: [],
310+
m: specToml
311+
});
312+
313+
// extract repository and hash from container string
314+
const containerMatch = spec.container.match(/^.+@sha256:([a-f0-9]{64})$/);
315+
if (!containerMatch) {
316+
throw new Error(`Invalid container format: ${spec.container}`);
317+
}
318+
const [, sha256Hash] = containerMatch;
319+
320+
// create and start container
321+
const persistentDebugContainer = process.env.HOLO_DEBUG_PERSIST_CONTAINER;
322+
let containerId;
323+
try {
324+
if (persistentDebugContainer) {
325+
try {
326+
const containerInfo = await Studio.execDocker(['inspect', persistentDebugContainer]);
327+
const containerState = JSON.parse(containerInfo)[0].State;
328+
329+
if (containerState.Running) {
330+
logger.info(`Found running debug container: ${persistentDebugContainer}`);
331+
containerId = persistentDebugContainer;
332+
}
333+
} catch (error) {
334+
containerId = null;
335+
}
336+
}
337+
338+
// create container
339+
if (!containerId) {
340+
containerId = await Studio.execDocker([
341+
'create',
342+
'-p', '9000:9000',
343+
...(persistentDebugContainer ? ['--name', persistentDebugContainer] : []),
344+
...(process.env.DEBUG ? ['-e', 'DEBUG=1'] : []),
345+
sha256Hash
346+
]);
347+
containerId = containerId.trim();
348+
349+
logger.info('starting container');
350+
await Studio.execDocker(['start', containerId]);
351+
}
352+
353+
// wait for port 9000 to be available
354+
let attempts = 0;
355+
const maxAttempts = 30;
356+
const waitTime = 1000; // 1 second
357+
358+
while (attempts < maxAttempts) {
359+
try {
360+
const containerInfo = await Studio.execDocker(['inspect', containerId]);
361+
const containerState = JSON.parse(containerInfo)[0].State;
362+
363+
if (containerState.Running) {
364+
// check if port 9000 is listening
365+
try {
366+
await Studio.execDocker([
367+
'exec',
368+
containerId,
369+
'nc',
370+
'-z',
371+
'localhost',
372+
'9000'
373+
]);
374+
break;
375+
} catch (err) {
376+
// ignore error and continue waiting
377+
}
378+
}
379+
} catch (err) {
380+
// ignore error and continue waiting
381+
}
382+
383+
await new Promise(resolve => setTimeout(resolve, waitTime));
384+
attempts++;
385+
}
386+
387+
if (attempts >= maxAttempts) {
388+
throw new Error('Timeout waiting for git server to be ready');
389+
}
390+
391+
// push commit to git server
392+
logger.info(`pushing and executing job: ${commitHash}`);
393+
await git.push(`http://localhost:9000/`, `${commitHash}:refs/heads/lens-input`, {
394+
force: true,
395+
$wait: true,
396+
$onStderr: (line) => process.stderr.write(`\x1b[90m${line}\x1b[0m\n`)
397+
});
398+
399+
// fetch and verify output commit
400+
const outputRef = `refs/lens-jobs/${specHash}`;
401+
logger.info('fetching result');
402+
await git.fetch('http://localhost:9000/', `+refs/heads/lens-input:${outputRef}`);
403+
404+
// verify the output commit's parent matches our input commit
405+
const outputParent = await git.revParse(`${outputRef}^`);
406+
if (outputParent !== commitHash) {
407+
throw new Error(`Output commit parent ${outputParent} does not match input commit ${commitHash}`);
408+
}
409+
410+
return await git.getTreeHash(outputRef);
411+
412+
} finally {
413+
// cleanup
414+
if (containerId && !persistentDebugContainer) {
415+
try {
416+
await Studio.execDocker(['stop', containerId]);
417+
await Studio.execDocker(['rm', containerId]);
418+
} catch (err) {
419+
logger.warn(`Failed to cleanup container: ${err.message}`);
420+
}
421+
}
422+
}
423+
}
424+
425+
static async executeSpecForHabitatPackage (repo, specHash) {
426+
const git = await repo.getGit();
427+
232428
let lensedTreeHash;
233429

430+
// ensure the rest runs inside a studio environment
234431
if (!await Studio.isEnvironmentStudio()) {
235432
const studio = await Studio.get(repo.gitDir);
236433
lensedTreeHash = await studio.holoLensExec(specHash);
@@ -307,16 +504,6 @@ class Lens extends Configurable {
307504
}
308505

309506

310-
// save ref to accelerate next projection
311-
if (save) {
312-
await git.updateRef(specRef, lensedTreeHash);
313-
314-
if (cacheTo) {
315-
await _cacheResultTo(repo, specRef, cacheTo);
316-
}
317-
}
318-
319-
320507
// return tree hash
321508
return lensedTreeHash;
322509
}

lib/Projection.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,11 @@ class Projection {
162162

163163
// build tree of matching files to input to lens
164164
logger.info(`building input tree for lens ${lens.name} from ${inputRoot == '.' ? '' : (path.join(inputRoot, '.')+'/')}{${inputFiles}}`);
165-
const { hash: specHash } = await lens.buildSpec(await lens.buildInputTree(this.output.root));
165+
const { hash: specHash, type: specType } = await lens.buildSpec(await lens.buildInputTree(this.output.root));
166166

167167

168168
// check for existing output tree
169-
const outputTreeHash = await lens.executeSpec(specHash, { cacheFrom, cacheTo });
169+
const outputTreeHash = await lens.executeSpec(specType, specHash, { cacheFrom, cacheTo });
170170

171171

172172
// verify output

0 commit comments

Comments
 (0)