Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 85 additions & 40 deletions packages/mongodb-downloader/src/index.ts
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,
Expand All @@ -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;
Expand All @@ -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;
Comment on lines +45 to +52

Copy link
Copy Markdown
Collaborator

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

};

export class MongoDBDownloader {
Expand All @@ -39,7 +58,8 @@ export class MongoDBDownloader {
version = '*',
directory,
useLockfile,
}: MongoDBDownloaderOptions): Promise<DownloadResult> {
downloadUrl,
}: MongoDBDownloaderOptions): Promise<DownloadResult | DownloadUrlResult> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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,
Expand All @@ -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',
Expand Down Expand Up @@ -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}...`);

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 {
Expand All @@ -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...`);
Expand Down Expand Up @@ -206,7 +262,7 @@ export class MongoDBDownloader {
}: {
bindir: string;
artifactInfoFile: string;
}): Promise<DownloadResult | undefined> {
}): Promise<DownloadResult | DownloadUrlResult | undefined> {
try {
await fs.stat(artifactInfoFile);
return {
Expand All @@ -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;
}
7 changes: 7 additions & 0 deletions packages/mongodb-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ You can specify a config file for the CLI using `--config`:
$ npx mongodb-runner start --config <path/to/config.json>
```

## DSC clusters

mongodb-runner can launch clusters that use DSC, backed by an SLS storage
layer, managing the storage backend's docker compose project alongside the
mongod processes. See
[docs/disaggregated-storage.md](./docs/disaggregated-storage.md).

## License

Apache 2.0
Loading
Loading