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
21 changes: 16 additions & 5 deletions packages/mongodb-runner/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ import type { MongoClientOptions } from 'mongodb';
.demandCommand(1, 'A command needs to be provided')
.help().argv;
const [command, ...args] = argv._.map(String);
// Allow args to be provided by the config file.
if (Array.isArray(argv.args)) {
args.push(...argv.args.map(String));
}
if (argv.debug || argv.verbose) {
createDebug.enable('mongodb-runner');
}
Expand All @@ -111,22 +115,29 @@ import type { MongoClientOptions } from 'mongodb';
async function start() {
const { cluster, id } = await utilities.start(argv, args);
const cs = new ConnectionString(cluster.connectionString);
console.log(`Server started and running at ${cs.toString()}`);
// Only the connection string should print to stdout so it can be captured
// by a calling process.
console.error(`Server started and running at ${cs.toString()}`);
if (cluster.oidcIssuer) {
cs.typedSearchParams<MongoClientOptions>().set(
'authMechanism',
'MONGODB-OIDC',
);
console.log(`OIDC provider started and running at ${cluster.oidcIssuer}`);
console.log(`Server connection string with OIDC auth: ${cs.toString()}`);
console.error(
`OIDC provider started and running at ${cluster.oidcIssuer}`,
);
console.error(
`Server connection string with OIDC auth: ${cs.toString()}`,
);
}
console.log('Run the following command to stop the instance:');
console.log(
console.error('Run the following command to stop the instance:');
console.error(
`${argv.$0} stop --id=${id}` +
(argv.runnerDir !== defaultRunnerDir
? `--runnerDir=${argv.runnerDir}`
: ''),
);
console.log(cs.toString());
cluster.unref();
}

Expand Down
33 changes: 33 additions & 0 deletions packages/mongodb-runner/src/mongocluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export interface CommonOptions {
*/
tlsAddClientKey?: boolean;

/**
* Whether to require an API version for commands.
*/
requireApiVersion?: number;

/**
* Topology of the cluster.
*/
Expand Down Expand Up @@ -488,6 +493,7 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
...options,
...s,
topology: 'replset',
requireApiVersion: undefined,
users: isConfig ? undefined : options.users, // users go on the mongos/config server only for the config set
});
return [cluster, isConfig] as const;
Expand Down Expand Up @@ -528,6 +534,7 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
}

await cluster.addAuthIfNeeded();
await cluster.addRequireApiVersionIfNeeded(options);
return cluster;
}

Expand All @@ -536,6 +543,32 @@ export class MongoCluster extends EventEmitter<MongoClusterEvents> {
yield* this.shards;
}

async addRequireApiVersionIfNeeded({
...options
}: MongoClusterOptions): Promise<void> {
// Set up requireApiVersion if requested.
if (options.requireApiVersion === undefined) {
return;
}
if (options.topology === 'replset') {
throw new Error(
'requireApiVersion is not supported for replica sets, see SERVER-97010',
);
}
await Promise.all(
[...this.servers].map(
async (child) =>
await child.withClient(async (client) => {
const admin = client.db('admin');
await admin.command({ setParameter: 1, requireApiVersion: true });
}),
),
);
await this.updateDefaultConnectionOptions({
serverApi: String(options.requireApiVersion) as '1',
});
}

async addAuthIfNeeded(): Promise<void> {
if (!this.users?.length) return;
// Sleep to give time for a possible replset election to settle.
Expand Down
86 changes: 72 additions & 14 deletions packages/mongodb-runner/src/mongoserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
debugVerbose,
jsonClone,
makeConnectionString,
sleep,
} from './util';

/**
Expand Down Expand Up @@ -286,9 +287,13 @@ export class MongoServer extends EventEmitter<MongoServerEvents> {
logEntryStream.resume();

srv.port = port;
const buildInfoError = await srv._populateBuildInfo('insert-new');
if (buildInfoError) {
debug('failed to get buildInfo', buildInfoError);
// If a keyFile is present, we cannot read or write on the server until
// a user is added to the primary.
if (!options.args?.includes('--keyFile')) {
const buildInfoError = await srv._populateBuildInfo('insert-new');
if (buildInfoError) {
debug('failed to get buildInfo', buildInfoError);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a keyfile is being used, wouldn't we want to use it for authentication?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, when a keyFile is present you can't do anything on the server until a user has been added to the primary.

} catch (err) {
await srv.close();
Expand All @@ -301,24 +306,77 @@ export class MongoServer extends EventEmitter<MongoServerEvents> {
async updateDefaultConnectionOptions(
options: Partial<MongoClientOptions>,
): Promise<void> {
// Assume we need these new options to connect.
this.defaultConnectionOptions = {
...this.defaultConnectionOptions,
...options,
};

// If there is no auth in the connection options, do an immediate metadata refresh and return.
let buildInfoError: Error | null = null;
if (!options.auth) {
buildInfoError = await this._populateBuildInfo('restore-check');
if (buildInfoError) {
debug(
'failed to refresh buildInfo when updating connection options',
buildInfoError,
options,
);
throw buildInfoError;
}
return;
}

debug('Waiting for authorization on', this.port);

// Wait until we can get connectionStatus.
let supportsAuth = false;
let error: unknown = null;
for (let attempts = 0; attempts < 10; attempts++) {
buildInfoError = await this._populateBuildInfo('restore-check', {
...options,
});
if (!buildInfoError) break;
error = null;
try {
supportsAuth = await this.withClient(async (client) => {
const status = await client
.db('admin')
.command({ connectionStatus: 1 });
if (status.authInfo.authenticatedUsers.length > 0) {
return true;
}
// The server is most likely an arbiter, which does not support
// authenticated users but does support getting the buildInfo.
debug('Server does not support authorization', this.port);
this.buildInfo = await client.db('admin').command({ buildInfo: 1 });
return false;
});
} catch (e) {
error = e;
await sleep(2 ** attempts * 10);
}
if (error === null) {
break;
}
}

if (error !== null) {
throw error;
}

if (!supportsAuth) {
return;
}

const mode = this.hasInsertedMetadataCollEntry
? 'restore-check'
: 'insert-new';
buildInfoError = await this._populateBuildInfo(mode);
if (buildInfoError) {
debug(
'failed to get buildInfo when setting new options',
'failed to refresh buildInfo when updating connection options',
buildInfoError,
options,
this.connectionString,
);
throw buildInfoError;
}
if (buildInfoError) throw buildInfoError;
this.defaultConnectionOptions = {
...this.defaultConnectionOptions,
...options,
};
}

async close(): Promise<void> {
Expand Down
Loading