Skip to content
Closed
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
resources/cidrs.json
packages/mongodb-schema/examples/fanclub.json
packages/mongodb-runner/test/fixtures/sls/malformed/manifest.json
1 change: 1 addition & 0 deletions packages/mongodb-runner/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.nyc_output
dist
coverage
test/fixtures/sls/malformed
39 changes: 39 additions & 0 deletions packages/mongodb-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Options:
--docker Docker image name to run server instances under [string]
--id ID to save the cluster metadata under [string]
--all for `stop`: stop all clusters [boolean]
--json for `start` and `ls`: print machine-readable JSON
on stdout instead of plain text [boolean]
--debug Enable debug output [boolean]
--help Show help [boolean]
```
Expand Down Expand Up @@ -109,6 +111,43 @@ You can specify a config file for the CLI using `--config`:
$ npx @mongodb-js/mongodb-runner start --config <path/to/config.json>
```

## Machine-readable output

By default `start` prints diagnostics to stderr and the bare connection string
to stdout, and `ls` prints one `id: uri` line per running instance. Pass
`--json` to get structured output on stdout instead (diagnostics still go to
stderr, and are not part of the JSON):

```sh
$ npx mongodb-runner start -t replset --json
{
"id": "2b56a4d3-...",
"connectionString": "mongodb://localhost:27017,localhost:27018/?replicaSet=repl0"
}
```

For OIDC clusters the result also carries `oidcIssuer` and
`connectionStringWithOidc` (the connection string with
`authMechanism=MONGODB-OIDC` appended). For DSC clusters it carries an `sls`
object with the allocated `ports` and `services`. Capture it to a file rather
than scraping stdout:

```sh
$ npx mongodb-runner start -t replset --json > cluster.json
```

`ls --json` prints a JSON array of the running instances:

```sh
$ npx mongodb-runner ls --json
[
{
"id": "2b56a4d3-...",
"connectionString": "mongodb://localhost:27017/"
}
]
```

## DSC clusters

mongodb-runner can launch clusters that use DSC, backed by an SLS storage
Expand Down
76 changes: 53 additions & 23 deletions packages/mongodb-runner/docs/disaggregated-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,35 @@ override.

- Docker with `docker compose` v2.
- Access to the SLS container images. The default repository is a private ECR
registry, so log in first:
registry. mongodb-runner authenticates to it automatically before starting
the compose project, which requires the `aws` CLI on your `PATH` and AWS
credentials in the usual places. Pass `--slsSkipEcrLogin` to disable this if
you have already authenticated another way.

To authenticate by hand instead:

```bash
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 664315256653.dkr.ecr.us-east-1.amazonaws.com
aws ecr get-authorization-token \
--region us-east-1 \
--registry-ids 664315256653 \
--query 'authorizationData[0].authorizationToken' \
--output text |
base64 -d | cut -d: -f2- |
docker login --username AWS --password-stdin 664315256653.dkr.ecr.us-east-1.amazonaws.com
```

Note that `aws ecr get-login-password`, which is the more commonly documented
form, issues a token scoped to _your own_ registry. If your AWS profile lives
outside account `664315256653` that token is for the wrong account and
`docker login` rejects it with a bare `status: 400 Bad Request`. Hence the
explicit `--registry-ids`.

Authenticating successfully is not sufficient to pull: minting a token only
requires `ecr:GetAuthorizationToken` in your own account, while pulling
requires `ecr:BatchGetImage` granted by a resource policy on the repositories
in `664315256653`. If login succeeds but pulls fail, that policy is what you
are missing.

- A `mongod` build that understands the `disaggregatedStorageConfig`
server parameter. Stock community/enterprise release binaries do **not** —
you need a build of the server with the atlas module. Provide it either as:
Expand All @@ -48,38 +70,45 @@ override.
server checkout. Files it references (`slsbackup.proto`,
`flags-state.json`) are resolved relative to it, and the services/ports are
parsed from it, so any version of the file works as-is.
- The SLS image tag to use, typically the `pinned_sls_commit` from
`buildscripts/modules/atlas/manifest.json` in the mongodb server repository.
- The image tag is read automatically from the `pinned_sls_commit` in
`manifest.json` sitting next to the compose file (the server repository's
`buildscripts/modules/atlas/manifest.json`). Pass `--slsImageTag` to
override it.

## Quick start

Given the compose file and image tag, everything else (compose environment
Given the compose file, everything else (image tag, compose environment
variables, readiness polling, per-shard log creation, and the
`disaggregatedStorageConfig` server parameter) is generated automatically.
Full sequence, assuming a mongodb server checkout at `$MONGO_REPO`:

```bash
# 1. Log in to the SLS image registry
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 664315256653.dkr.ecr.us-east-1.amazonaws.com

# 2. Look up the pinned SLS image tag
SLS_IMAGE_TAG=$(python3 -c "import json; print(json.load(open('$MONGO_REPO/buildscripts/modules/atlas/manifest.json'))['pinned_sls_commit'])")

# 3. Start a 2-node replica set backed by SLS
# The image tag is read from the manifest.json next to the compose file; pass
# --slsImageTag to override it. Logs in to ECR automatically.
@mongodb-js/mongodb-runner start -t replset \
--slsCompose=$MONGO_REPO/buildscripts/modules/atlas/sls-multicell-docker-compose.yml \
--slsImageTag=$SLS_IMAGE_TAG \
--binDir=/path/to/dsc-mongod/bin \
--debug
# or, instead of --binDir:
# --downloadUrl=https://.../dsc-mongod.tgz
```

This prints the connection string once the cluster is up. The first run pulls
all SLS images, which can take several minutes (`--debug` shows the
progress). `mongodb-runner stop --id=...` (printed by `start`) tears down the
mongod processes and the compose project together.
This prints the connection string once the cluster is up. To get the allocated
SLS service ports/URIs as structured output, add `--json` and capture stdout to
a file instead of scraping it:

```bash
mongodb-runner start -t replset \
--slsCompose=$MONGO_REPO/buildscripts/modules/atlas/sls-multicell-docker-compose.yml \
--binDir=/path/to/dsc-mongod/bin \
--json > cluster.json
```

The JSON object carries `id` and `connectionString` plus an `sls` field with
the `ports` and `services` (the same info printed to stderr without `--json`).
The first run pulls all SLS images, which can take several minutes (`--debug`
shows the progress). `mongodb-runner stop --id=...` (printed by `start`) tears
down the mongod processes and the compose project together.

The same works for `-t standalone` and `-t sharded`. A DSC-capable
`mongod` is required (see Prerequisites) — with a stock binary, startup fails
Expand Down Expand Up @@ -209,7 +238,8 @@ from the server codebase and returns:
| `ports` | The allocated host port per service, e.g. `ports['crs-cell1-0']` |
| `services` | Host `addr`/`uri` per service, e.g. `services['cms-cell1-0'].uri` |

Required options: `composeFile`, `imageTag`. Optional: `imageRepo`,
Required options: `composeFile`. Optional: `imageTag` (defaults to the
`pinned_sls_commit` from the manifest next to the compose file), `imageRepo`,
`thirdPartyImageRepo`, `testDataId` (container label for test attribution),
`hostInternalIP`. The service list is parsed from the compose file's
`ports:` mappings (`parseSLSComposeServices`), so it adapts to whatever
Expand All @@ -223,13 +253,13 @@ server parameter for one shard; options: `logId`, `cellMetadataService`,

## CLI use

For an SLS project, `--slsCompose` + `--slsImageTag` handle everything (see
Quick start):
For an SLS project, `--slsCompose` handles everything (see Quick start);
`--slsImageTag` is optional and overrides the tag read from the manifest:

```bash
@mongodb-js/mongodb-runner start -t replset \
--slsCompose=/path/to/sls-multicell-docker-compose.yml \
--slsImageTag=<tag> --binDir=...
--binDir=...
```

Custom (non-SLS) storage backends are only supported through the programmatic
Expand Down
57 changes: 57 additions & 0 deletions packages/mongodb-runner/src/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,61 @@ describe('cli', function () {

await runCli(['stop', '--all']);
});

it('emits structured output for start with --json', async function () {
const stdout = await runCli([
'start',
'--topology',
'standalone',
'--json',
]);

let parsed: any;
expect(() => {
parsed = JSON.parse(stdout);
}, 'stdout must be parseable as JSON, with diagnostics on stderr').to.not.throw();

expect(parsed.id, 'result should carry the cluster id').to.be.a('string');
expect(
parsed.connectionString,
'result should carry the connection string',
).to.match(/^mongodb:\/\//);

const client = new MongoClient(parsed.connectionString);
const result = await client.db('admin').command({ ping: 1 });
await client.close();
expect(result.ok, 'reported connection string should be usable').to.eq(1);

await runCli(['stop', '--all']);
await runCli(['prune']);
});

it('emits structured output for ls with --json', async function () {
const startStdout = await runCli([
'start',
'--topology',
'standalone',
'--json',
]);
const started = JSON.parse(startStdout);

const lsStdout = await runCli(['ls', '--json']);
let parsed: any;
expect(() => {
parsed = JSON.parse(lsStdout);
}, 'ls --json stdout must be parseable as JSON').to.not.throw();

expect(parsed, 'ls --json should yield an array').to.be.an('array');
const entry = parsed.find((e: any) => e.id === started.id);
expect(entry, 'the started cluster should be listed').to.not.equal(
undefined,
);
expect(
entry.connectionString,
'listed connection string should match the one start reported',
).to.equal(started.connectionString);

await runCli(['stop', '--all']);
await runCli(['prune']);
});
});
49 changes: 44 additions & 5 deletions packages/mongodb-runner/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,23 @@ import type { MongoClientOptions } from 'mongodb';
type: 'string',
describe:
'Path to an SLS multi-cell docker-compose.yml; launches the SLS DSC project and configures mongod to use it (requires a DSC-capable mongod via --binDir or --downloadUrl)',
implies: 'slsImageTag',
})
.option('slsImageTag', {
type: 'string',
describe:
'SLS docker image tag to use with --slsCompose (e.g. the pinned_sls_commit from the server repo manifest)',
'SLS docker image tag to use with --slsCompose (defaults to the pinned_sls_commit from the manifest.json next to the compose file)',
})
.option('slsSkipEcrLogin', {
type: 'boolean',
default: false,
describe:
'Skip authenticating to the SLS image registry (use if you have already run docker login)',
})
.option('json', {
type: 'boolean',
default: false,
describe:
'For `start` and `ls`: print machine-readable JSON on stdout instead of plain text',
})
.option('debug', { type: 'boolean', describe: 'Enable debug output' })
.option('verbose', { type: 'boolean', describe: 'Enable verbose output' })
Expand Down Expand Up @@ -133,7 +144,8 @@ import type { MongoClientOptions } from 'mongodb';
const disaggregatedStorage = argv.slsCompose
? await utilities.createSLSDisaggregatedStorageOptions({
composeFile: argv.slsCompose,
imageTag: argv.slsImageTag!,
imageTag: argv.slsImageTag,
ecrLogin: !argv.slsSkipEcrLogin,
})
: undefined;
if (disaggregatedStorage && 'sls' in disaggregatedStorage) {
Expand Down Expand Up @@ -171,7 +183,26 @@ import type { MongoClientOptions } from 'mongodb';
? `--runnerDir=${argv.runnerDir}`
: ''),
);
console.log(cs.toString());
if (argv.json) {
const result: Record<string, unknown> = {
__proto__: null,
id,
connectionString: cluster.connectionString,
};
if (cluster.oidcIssuer) {
result.oidcIssuer = cluster.oidcIssuer;
result.connectionStringWithOidc = cs.toString();
}
if (disaggregatedStorage && 'sls' in disaggregatedStorage) {
result.sls = {
ports: disaggregatedStorage.sls.ports,
services: disaggregatedStorage.sls.services,
};
}
console.log(JSON.stringify(result, null, 2));
} else {
console.log(cs.toString());
}
cluster.unref();
}

Expand All @@ -183,8 +214,16 @@ import type { MongoClientOptions } from 'mongodb';
}

async function ls() {
const entries: { id: string; connectionString: string }[] = [];
for await (const { id, connectionString } of utilities.instances(argv)) {
console.log(`${id}: ${connectionString}`);
if (argv.json) {
entries.push({ id, connectionString });
} else {
console.log(`${id}: ${connectionString}`);
}
}
if (argv.json) {
console.log(JSON.stringify(entries, null, 2));
}
}

Expand Down
Loading