From 47a6565b1d51db7e5327cc7119eb811615f800d1 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Wed, 2 Sep 2026 16:07:01 -0500 Subject: [PATCH] feat(mongodb-runner): add --json output to start and ls --- packages/mongodb-runner/README.md | 39 +++++++++++++ .../docs/disaggregated-storage.md | 20 +++++-- packages/mongodb-runner/src/cli.spec.ts | 57 +++++++++++++++++++ packages/mongodb-runner/src/cli.ts | 37 +++++++++++- 4 files changed, 147 insertions(+), 6 deletions(-) diff --git a/packages/mongodb-runner/README.md b/packages/mongodb-runner/README.md index 6740f4e9e..1ce23c5de 100644 --- a/packages/mongodb-runner/README.md +++ b/packages/mongodb-runner/README.md @@ -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] ``` @@ -109,6 +111,43 @@ You can specify a config file for the CLI using `--config`: $ npx @mongodb-js/mongodb-runner start --config ``` +## 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 diff --git a/packages/mongodb-runner/docs/disaggregated-storage.md b/packages/mongodb-runner/docs/disaggregated-storage.md index d1da307f1..eacd603b0 100644 --- a/packages/mongodb-runner/docs/disaggregated-storage.md +++ b/packages/mongodb-runner/docs/disaggregated-storage.md @@ -93,10 +93,22 @@ Full sequence, assuming a mongodb server checkout at `$MONGO_REPO`: # --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 diff --git a/packages/mongodb-runner/src/cli.spec.ts b/packages/mongodb-runner/src/cli.spec.ts index be2b3ff1a..90917cc5d 100644 --- a/packages/mongodb-runner/src/cli.spec.ts +++ b/packages/mongodb-runner/src/cli.spec.ts @@ -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']); + }); }); diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index ce9f73738..02067dbe8 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -99,6 +99,12 @@ import type { MongoClientOptions } from 'mongodb'; 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' }) .command('start', 'Start a MongoDB instance') @@ -177,7 +183,26 @@ import type { MongoClientOptions } from 'mongodb'; ? `--runnerDir=${argv.runnerDir}` : ''), ); - console.log(cs.toString()); + if (argv.json) { + const result: Record = { + __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(); } @@ -189,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)); } }