-
Notifications
You must be signed in to change notification settings - Fork 238
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wire up fetching a Template from an OCI registry (#225)
* teach cli how to fetch a template from an OCI registry * commit new file * dont update packagejson * update test * add api to getBlob to ignore certain files. Return list of extracted files to caller * expose files fetched to caller of fetchTemplate * also ignore NOTES.md * Update containerTemplatesOCI.ts * fix import * change temp dir name * nest * add test * move template substitution to CLI * add anaconda test and better file detection * code review and update test
- Loading branch information
1 parent
702e099
commit 848d348
Showing
5 changed files
with
190 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Log, LogLevel } from '../spec-utils/log'; | ||
import * as os from 'os'; | ||
import * as path from 'path'; | ||
import { fetchOCIManifestIfExists, getBlob, getRef, OCIManifest } from './containerCollectionsOCI'; | ||
import { isLocalFile, readLocalFile, writeLocalFile } from '../spec-utils/pfs'; | ||
|
||
export interface TemplateOptions { | ||
[name: string]: string; | ||
} | ||
|
||
export interface SelectedTemplate { | ||
id: string; | ||
options: TemplateOptions; | ||
} | ||
|
||
export async function fetchTemplate(output: Log, selectedTemplate: SelectedTemplate, templateDestPath: string): Promise<string[] | undefined> { | ||
const { id, options } = selectedTemplate; | ||
const templateRef = getRef(output, id); | ||
if (!templateRef) { | ||
output.write(`Failed to parse template ref for ${id}`, LogLevel.Error); | ||
return; | ||
} | ||
|
||
const ociManifest = await fetchOCITemplateManifestIfExistsFromUserIdentifier(output, process.env, id); | ||
if (!ociManifest) { | ||
output.write(`Failed to fetch template manifest for ${id}`, LogLevel.Error); | ||
return; | ||
} | ||
|
||
const blobUrl = `https://${templateRef.registry}/v2/${templateRef.path}/blobs/${ociManifest?.layers[0].digest}`; | ||
output.write(`blob url: ${blobUrl}`, LogLevel.Trace); | ||
|
||
const tmpDir = path.join(os.tmpdir(), 'vsch-template-temp', `${Date.now()}`); | ||
const files = await getBlob(output, process.env, blobUrl, tmpDir, templateDestPath, templateRef, undefined, ['devcontainer-template.json', 'README.md', 'NOTES.md']); | ||
|
||
if (!files) { | ||
throw new Error(`Failed to download package for ${templateRef.resource}`); | ||
} | ||
|
||
// Scan all template files and replace any templated values. | ||
for (const f of files) { | ||
output.write(`Scanning file '${f}'`, LogLevel.Trace); | ||
const filePath = path.join(templateDestPath, f); | ||
if (await isLocalFile(filePath)) { | ||
const fileContents = await readLocalFile(filePath); | ||
const fileContentsReplaced = replaceTemplatedValues(output, fileContents.toString(), options); | ||
await writeLocalFile(filePath, Buffer.from(fileContentsReplaced)); | ||
} else { | ||
output.write(`Could not find templated file '${f}'.`, LogLevel.Error); | ||
} | ||
} | ||
|
||
return files; | ||
} | ||
|
||
|
||
async function fetchOCITemplateManifestIfExistsFromUserIdentifier(output: Log, env: NodeJS.ProcessEnv, identifier: string, manifestDigest?: string, authToken?: string): Promise<OCIManifest | undefined> { | ||
const templateRef = getRef(output, identifier); | ||
return await fetchOCIManifestIfExists(output, env, templateRef, manifestDigest, authToken); | ||
} | ||
|
||
function replaceTemplatedValues(output: Log, template: string, options: TemplateOptions) { | ||
const pattern = /\${templateOption:\s*(\w+?)\s*}/g; // ${templateOption:XXXX} | ||
return template.replace(pattern, (_, token) => { | ||
output.write(`Replacing ${token} with ${options[token]}`); | ||
return options[token] || ''; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
src/test/container-templates/containerTemplatesOCI.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; | ||
import * as assert from 'assert'; | ||
export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); | ||
import { fetchTemplate, SelectedTemplate } from '../../spec-configuration/containerTemplatesOCI'; | ||
import * as path from 'path'; | ||
import { readLocalFile } from '../../spec-utils/pfs'; | ||
|
||
describe('fetchTemplate', async function () { | ||
this.timeout('120s'); | ||
|
||
it('succeeds on docker-from-docker template', async () => { | ||
|
||
// https://github.com/devcontainers/templates/tree/main/src/docker-from-docker | ||
const selectedTemplate: SelectedTemplate = { | ||
id: 'ghcr.io/devcontainers/templates/docker-from-docker:latest', | ||
options: {'installZsh': 'false', 'upgradePackages': 'true', 'dockerVersion': '20.10', 'moby': 'true', 'enableNonRootDocker': 'true' } | ||
}; | ||
|
||
const dest = path.relative(process.cwd(), path.join(__dirname, 'tmp1')); | ||
const files = await fetchTemplate(output, selectedTemplate, dest); | ||
assert.ok(files); | ||
// Should only container 1 file '.devcontainer.json'. The other 3 in this repo should be ignored. | ||
assert.strictEqual(files.length, 1); | ||
|
||
// Read File | ||
const file = (await readLocalFile(path.join(dest, files[0]))).toString(); | ||
assert.match(file, /"name": "Docker from Docker"/); | ||
assert.match(file, /"installZsh": "false"/); | ||
assert.match(file, /"upgradePackages": "true"/); | ||
assert.match(file, /"version": "20.10"/); | ||
assert.match(file, /"moby": "true"/); | ||
assert.match(file, /"enableNonRootDocker": "true"/); | ||
}); | ||
|
||
it('succeeds on anaconda-postgres template', async () => { | ||
|
||
// https://github.com/devcontainers/templates/tree/main/src/anaconda-postgres | ||
const selectedTemplate: SelectedTemplate = { | ||
id: 'ghcr.io/devcontainers/templates/anaconda-postgres:latest', | ||
options: { 'nodeVersion': 'lts/*' } | ||
}; | ||
|
||
const dest = path.relative(process.cwd(), path.join(__dirname, 'tmp2')); | ||
const files = await fetchTemplate(output, selectedTemplate, dest); | ||
assert.ok(files); | ||
// Expected: | ||
// ./environment.yml, ./.devcontainer/.env, ./.devcontainer/Dockerfile, ./.devcontainer/devcontainer.json, ./.devcontainer/docker-compose.yml, ./.devcontainer/noop.txt | ||
assert.strictEqual(files.length, 6); | ||
|
||
// Read File | ||
const file = (await readLocalFile(path.join(dest, '.devcontainer', 'Dockerfile'))).toString(); | ||
assert.match(file, /ARG NODE_VERSION="lts\/\*"/); | ||
|
||
}); | ||
}); |