-
Notifications
You must be signed in to change notification settings - Fork 18
feat: DSC support for mongodb runner #822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1484845
60e8af4
5908876
89be729
ecef1cf
caf85a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,12 @@ | ||
| /* eslint-disable no-console */ | ||
| import fetch from 'node-fetch'; | ||
| import * as tar from 'tar'; | ||
| import { createHash } from 'crypto'; | ||
| import { promisify } from 'util'; | ||
| import { promises as fs, createWriteStream } from 'fs'; | ||
| import path from 'path'; | ||
| import decompress from 'decompress'; | ||
| import { pipeline } from 'stream'; | ||
| import { pipeline, Transform } from 'stream'; | ||
| import getDownloadURL from 'mongodb-download-url'; | ||
| import type { | ||
| Options as DownloadOptions, | ||
|
|
@@ -22,6 +23,16 @@ export type DownloadResult = DownloadArtifactInfo & { | |
| downloadedBinDir: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Result of a download from a direct URL (`downloadUrl`). Since no download | ||
| * URL lookup is performed, only the URL itself is known; the other artifact | ||
| * metadata fields are unavailable. | ||
| */ | ||
| export type DownloadUrlResult = Partial<DownloadArtifactInfo> & { | ||
| url: string; | ||
| downloadedBinDir: string; | ||
| }; | ||
|
|
||
| export type MongoDBDownloaderOptions = { | ||
| /** The directory to download the artifacts to. */ | ||
| directory: string; | ||
|
|
@@ -31,6 +42,14 @@ export type MongoDBDownloaderOptions = { | |
| useLockfile: boolean; | ||
| /** The options to pass to the download URL lookup. */ | ||
| downloadOptions?: DownloadOptions; | ||
| /** | ||
| * A direct URL to a MongoDB tarball to download. If set, no download URL | ||
| * lookup is performed: `version` is ignored, and of `downloadOptions`, | ||
| * only `platform` and `crypt_shared` are consulted (they determine the | ||
| * archive extraction layout). The result contains no artifact metadata | ||
| * beyond the URL itself (see `DownloadUrlResult`). | ||
| */ | ||
| downloadUrl?: string; | ||
| }; | ||
|
|
||
| export class MongoDBDownloader { | ||
|
|
@@ -39,7 +58,8 @@ export class MongoDBDownloader { | |
| version = '*', | ||
| directory, | ||
| useLockfile, | ||
| }: MongoDBDownloaderOptions): Promise<DownloadResult> { | ||
| downloadUrl, | ||
| }: MongoDBDownloaderOptions): Promise<DownloadResult | DownloadUrlResult> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add the same function overloads as you added below, otherwise it changes the interface here in a way that would require every user to always manually assert whether or not the result contains expected values |
||
| await fs.mkdir(directory, { recursive: true }); | ||
| const isWindows = ['win32', 'windows'].includes( | ||
| downloadOptions.platform ?? process.platform, | ||
|
|
@@ -58,12 +78,20 @@ export class MongoDBDownloader { | |
| versionName = versionName + (isEnterprise ? '-enterprise' : '-community'); | ||
| } | ||
|
|
||
| const downloadTarget = path.resolve( | ||
| directory, | ||
| `mongodb-${process.platform}-${process.env.DISTRO_ID || 'none'}-${ | ||
| process.arch | ||
| }-${versionName}`.replace(/[^a-zA-Z0-9_-]/g, ''), | ||
| ); | ||
| const downloadTarget = downloadUrl | ||
| ? path.resolve( | ||
| directory, | ||
| `mongodb-custom-${createHash('sha256') | ||
| .update(downloadUrl) | ||
| .digest('hex') | ||
| .slice(0, 16)}`, | ||
| ) | ||
| : path.resolve( | ||
| directory, | ||
| `mongodb-${process.platform}-${process.env.DISTRO_ID || 'none'}-${ | ||
| process.arch | ||
| }-${versionName}`.replace(/[^a-zA-Z0-9_-]/g, ''), | ||
| ); | ||
| const bindir = path.resolve( | ||
| downloadTarget, | ||
| isCryptLibrary && !isWindows ? 'lib' : 'bin', | ||
|
|
@@ -99,11 +127,14 @@ export class MongoDBDownloader { | |
| } | ||
|
|
||
| await fs.mkdir(downloadTarget, { recursive: true }); | ||
| const artifactInfo = await this.lookupDownloadUrl({ | ||
| targetVersion: version, | ||
| enterprise: isEnterprise, | ||
| options: downloadOptions, | ||
| }); | ||
| const artifactInfo: DownloadArtifactInfo | { url: string } = | ||
| downloadUrl | ||
| ? { url: downloadUrl } | ||
| : await this.lookupDownloadUrl({ | ||
| targetVersion: version, | ||
| enterprise: isEnterprise, | ||
| options: downloadOptions, | ||
| }); | ||
| const { url } = artifactInfo; | ||
| debug(`Downloading ${url} into ${downloadTarget}...`); | ||
|
|
||
|
|
@@ -139,11 +170,35 @@ export class MongoDBDownloader { | |
| const response = await fetch(url, { | ||
| highWaterMark: MongoDBDownloader.HWM, | ||
| } as Parameters<typeof fetch>[1]); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Failed to download ${url}: ${response.status} ${response.statusText}`, | ||
| ); | ||
| } | ||
| const totalBytes = +(response.headers.get('content-length') ?? ''); | ||
| const totalMB = totalBytes ? (totalBytes / 1048576).toFixed(1) : null; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: can we move the number to a var so that it's very clear immediately what it is and we always consistently use the same number below? |
||
| debug(`Download started`, { url, totalMB }); | ||
| let downloadedBytes = 0; | ||
| let lastProgressLog = Date.now(); | ||
| const progress = new Transform({ | ||
| transform(chunk: Buffer, _encoding, callback) { | ||
| downloadedBytes += chunk.length; | ||
| if (Date.now() - lastProgressLog >= 3000) { | ||
| lastProgressLog = Date.now(); | ||
| const downloadedMB = (downloadedBytes / 1048576).toFixed(1); | ||
| debug( | ||
| `Downloading: ${downloadedMB}MB${totalMB ? ` / ${totalMB}MB` : ''}`, | ||
| ); | ||
| } | ||
| callback(null, chunk); | ||
| }, | ||
| }); | ||
| if (/\.tgz$|\.tar(\.[^.]+)?$/.exec(url)) { | ||
| // the server's tarballs can contain hard links, which the (unmaintained?) | ||
| // `download` package is unable to handle (https://github.com/kevva/decompress/issues/93) | ||
| await promisify(pipeline)( | ||
| response.body, | ||
| progress, | ||
| tar.x({ cwd: downloadTarget, strip: isCryptLibrary ? 0 : 1 }), | ||
| ); | ||
| } else { | ||
|
|
@@ -153,6 +208,7 @@ export class MongoDBDownloader { | |
| ); | ||
| await promisify(pipeline)( | ||
| response.body, | ||
| progress, | ||
| createWriteStream(filename, { highWaterMark: MongoDBDownloader.HWM }), | ||
| ); | ||
| debug(`Written file ${url} to ${filename}, extracting...`); | ||
|
|
@@ -206,7 +262,7 @@ export class MongoDBDownloader { | |
| }: { | ||
| bindir: string; | ||
| artifactInfoFile: string; | ||
| }): Promise<DownloadResult | undefined> { | ||
| }): Promise<DownloadResult | DownloadUrlResult | undefined> { | ||
| try { | ||
| await fs.stat(artifactInfoFile); | ||
| return { | ||
|
|
@@ -230,32 +286,21 @@ async function withoutLock<T>( | |
| const downloader = new MongoDBDownloader(); | ||
|
|
||
| /** Download mongod + mongos with version info and return version info and the path to a directory containing them. */ | ||
| export async function downloadMongoDbWithVersionInfo({ | ||
| downloadOptions = {}, | ||
| version = '*', | ||
| directory, | ||
| useLockfile, | ||
| }: MongoDBDownloaderOptions): Promise<DownloadResult> { | ||
| return await downloader.downloadMongoDbWithVersionInfo({ | ||
| downloadOptions, | ||
| version, | ||
| directory, | ||
| useLockfile, | ||
| }); | ||
| export function downloadMongoDbWithVersionInfo( | ||
| options: MongoDBDownloaderOptions & { downloadUrl: string }, | ||
| ): Promise<DownloadUrlResult>; | ||
| export function downloadMongoDbWithVersionInfo( | ||
| options: MongoDBDownloaderOptions & { downloadUrl?: undefined }, | ||
| ): Promise<DownloadResult>; | ||
| export async function downloadMongoDbWithVersionInfo( | ||
| options: MongoDBDownloaderOptions, | ||
| ): Promise<DownloadResult | DownloadUrlResult> { | ||
| return await downloader.downloadMongoDbWithVersionInfo(options); | ||
| } | ||
| /** Download mongod + mongos and return the path to a directory containing them. */ | ||
| export async function downloadMongoDb({ | ||
| downloadOptions = {}, | ||
| version = '*', | ||
| directory, | ||
| useLockfile, | ||
| }: MongoDBDownloaderOptions): Promise<string> { | ||
| return ( | ||
| await downloader.downloadMongoDbWithVersionInfo({ | ||
| downloadOptions, | ||
| version, | ||
| directory, | ||
| useLockfile, | ||
| }) | ||
| ).downloadedBinDir; | ||
| export async function downloadMongoDb( | ||
| options: MongoDBDownloaderOptions, | ||
| ): Promise<string> { | ||
| return (await downloader.downloadMongoDbWithVersionInfo(options)) | ||
| .downloadedBinDir; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should think about some different approach for adding this functionality in the downloader: the issue I see here is that while downloadOptions will be ignored, we will still consider them somewhat when returning the DownloadResult metadata, which from the consumer perspective might put you in a weird state where what you download doesn't match the returned options at all