Skip to content

Commit

Permalink
SERVER-49380 Add API params to mongo shell
Browse files Browse the repository at this point in the history
  • Loading branch information
ajdavis authored and Evergreen Agent committed Aug 20, 2020
1 parent 526230b commit d3064df
Show file tree
Hide file tree
Showing 29 changed files with 489 additions and 51 deletions.
187 changes: 187 additions & 0 deletions jstests/noPassthrough/api_version_parameters_shell.js
@@ -0,0 +1,187 @@
/**
* Test the shell's --apiVersion and other options related to the MongoDB Versioned API, and
* test passing API parameters to the Mongo() constructor.
*/

(function() {
'use strict';

const testCases = [
// [requireApiVersion server parameter, expect success, command, API parameters]
[false, true, {ping: 1}, {}],
[false, true, {ping: 1}, {version: '1'}],
[false, true, {count: 'collection'}, {version: '1'}],
[false, true, {getLog: 'global'}, {version: '1'}],
[false, true, {getLog: 'global'}, {version: '1', deprecationErrors: true}],
// getLog isn't in API Version 1, so it's banned with strict: true.
[false, false, {getLog: 'global'}, {version: '1', strict: true}],
[false, true, {ping: 1}, {version: '1', strict: true}],
[false, true, {testDeprecation: 1}, {version: '1', strict: true}],
[false, false, {testDeprecation: 1}, {version: '1', deprecationErrors: true}],
[false, false, {testDeprecation: 1}, {version: '1', strict: true, deprecationErrors: true}],
// tests with setParameter requireApiVersion: true.
[true, true, {count: 'collection'}, {version: '1'}],
[true, true, {count: 'collection'}, {version: '1', strict: true}],
[true, false, {ping: 1}, {}],
[true, true, {ping: 1}, {version: '1'}],
];

function runShell(requireApiVersion, expectSuccess, command, api) {
let shellArgs = [];
if (api.version) {
shellArgs.push('--apiVersion', api.version);
}

if (api.strict) {
shellArgs.push('--apiStrict');
}

if (api.deprecationErrors) {
shellArgs.push('--apiDeprecationErrors');
}

// Test runCommand and runReadCommand.
const scripts = [
`assert.commandWorked(db.getSiblingDB("admin").runCommand(${tojson(command)}))`,
`assert.commandWorked(db.getSiblingDB("admin").runReadCommand(${tojson(command)}))`,
];

for (const script of scripts) {
jsTestLog(`Run shell with script ${script} and args ${tojson(shellArgs)},` +
` requireApiVersion = ${requireApiVersion}, expectSuccess = ${expectSuccess}`);

const result = runMongoProgram.apply(
null, ['mongo', '--port', mongod.port, '--eval', script].concat(shellArgs || []));

if (expectSuccess) {
assert.eq(result,
0,
`Error running shell with command ${tojson(command)} and args ${
tojson(shellArgs)}`);
} else {
assert.neq(result,
0,
`Unexpected success running shell with` +
` command ${tojson(command)} and args ${tojson(shellArgs)}`);
}
}
}

function newMongo(requireApiVersion, expectSuccess, command, api) {
jsTestLog(`Construct Mongo object with command ${tojson(command)} and args ${tojson(api)},` +
` requireApiVersion = ${requireApiVersion}, expectSuccess = ${expectSuccess}`);
if (expectSuccess) {
const m = new Mongo(`mongodb://localhost:${mongod.port}`,
undefined /* encryptedDBClientCallback */,
{api: api});
const reply = m.adminCommand(command);
assert.commandWorked(reply, command);
} else {
let m;
try {
m = new Mongo(`mongodb://localhost:${mongod.port}`,
undefined /* encryptedDBClientCallback */,
{api: api});
} catch (e) {
// The constructor threw, but we expected failure.
print(e);
return;
}
const reply = m.adminCommand(command);
assert.commandFailed(reply, command);
}
}

const mongod = MongoRunner.runMongod({verbose: 2});

for (let [requireApiVersion, successExpected, command, api] of testCases) {
const m = new Mongo(`localhost:${mongod.port}`, undefined, {api: {version: '1'}});
assert.commandWorked(
m.getDB("admin").runCommand({setParameter: 1, requireApiVersion: requireApiVersion}));

runShell(requireApiVersion, successExpected, command, api);
newMongo(requireApiVersion, successExpected, command, api);
}

// Reset.
const m = new Mongo(`localhost:${mongod.port}`, undefined, {api: {version: '1'}});
assert.commandWorked(m.getDB("admin").runCommand({setParameter: 1, requireApiVersion: false}));

/*
* Shell-specific tests.
*/

// Version 2 is not supported.
runShell(false, false, {ping: 1}, {version: '2'});
// apiVersion is required if strict or deprecationErrors is included
runShell(false, false, {ping: 1}, {strict: true});
runShell(false, false, {ping: 1}, {deprecationErrors: true});
runShell(false, false, {ping: 1}, {strict: true, deprecationErrors: true});

/*
* Mongo-specific tests.
*/
assert.throws(() => {
new Mongo(mongod.port, null, "not an object");
}, [], "Mongo() constructor should check that options argument is an object");

assert.throws(() => {
new Mongo(mongod.port, null, {api: "not an object"});
}, [], "Mongo() constructor should check that 'api' is an object");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: 1}});
}, [], "Mongo() constructor should reject API version 1 (as a number)");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: '2'}});
}, [], "Mongo() constructor should reject API version 2");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: '1', strict: 1}});
}, [], "Mongo() constructor should reject strict: 1");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: '1', strict: 'asdf'}});
}, [], "Mongo() constructor should reject strict: 'asdf");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: '1', deprecationErrors: 1}});
}, [], "Mongo() constructor should reject deprecationErrors: 1");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {version: '1', deprecationErrors: 'asdf'}});
}, [], "Mongo() constructor should reject deprecationErrors: 'asdf'");

// apiVersion is required if strict or deprecationErrors is included
assert.throws(() => {
new Mongo(mongod.port, null, {api: {strict: true}});
}, [], "Mongo() constructor should reject 'strict' without 'version'");

assert.throws(() => {
new Mongo(mongod.port, null, {api: {deprecationErrors: true}});
}, [], "Mongo() constructor should reject 'deprecationErrors' without 'version'");

MongoRunner.stopMongod(mongod);

/*
* Test that we can call replSetGetStatus while assembling the shell prompt, although
* replSetGetStatus is not in API Version 1 and the shell is running with --apiStrict.
*/
const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();

const testPrompt = "assert(RegExp('PRIMARY').test(defaultPrompt()))";
const result = runMongoProgram('mongo',
'--port',
rst.getPrimary().port,
'--apiVersion',
'1',
'--apiStrict',
'--eval',
testPrompt);
assert.eq(result, 0, `Error running shell with script '${testPrompt}'`);

rst.stopSet();
})();
2 changes: 2 additions & 0 deletions src/mongo/client/SConscript
Expand Up @@ -182,6 +182,7 @@ clientDriverEnv.Library(
'dbclient_base.cpp',
'dbclient_cursor.cpp',
'index_spec.cpp',
env.Idlc('client_api_version_parameters.idl')[0],
],
LIBDEPS=[
'$BUILD_DIR/mongo/db/dbmessage',
Expand All @@ -196,6 +197,7 @@ clientDriverEnv.Library(
'connection_string',
],
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/idl/idl_parser',
'$BUILD_DIR/mongo/util/net/ssl_manager',
],
)
Expand Down
56 changes: 56 additions & 0 deletions src/mongo/client/client_api_version_parameters.idl
@@ -0,0 +1,56 @@
# Copyright (C) 2020-present MongoDB, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the Server Side Public License, version 1,
# as published by MongoDB, Inc.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Server Side Public License for more details.
#
# You should have received a copy of the Server Side Public License
# along with this program. If not, see
# <http://www.mongodb.com/licensing/server-side-public-license>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the Server Side Public License in all respects for
# all of the code used other than as permitted herein. If you modify file(s)
# with this exception, you may extend this exception to your version of the
# file(s), but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version. If you delete this
# exception statement from all source files in the program, then also delete
# it in the license file.

global:
cpp_namespace: "mongo"

imports:
- "mongo/idl/basic_types.idl"

structs:

# Follow the MongoDB Drivers API for passing API Version parameters in clients. The drivers API
# is like MongoClient(uri, api={version: "1", strict: true, deprecationErrors: true}).

ClientAPIVersionParameters:
description: "Parser for Versioned API parameters passed to 'new Mongo()' in the mongo shell"
strict: true
fields:
version:
description: "The requested API version"
type: string
# The server requires the apiVersion parameter in commands, leave enforcement to the server.
optional: true
strict:
description: "Whether to restrict the connection to behaviors in the requested API version"
type: bool
optional: true
deprecationErrors:
description: "Whether to restrict the connection to non-deprecated behaviors in the
requested API version"
type: bool
optional: true
19 changes: 12 additions & 7 deletions src/mongo/client/connection_string.h
Expand Up @@ -43,6 +43,7 @@

namespace mongo {

class ClientAPIVersionParameters;
class DBClientBase;
class MongoURI;

Expand Down Expand Up @@ -121,10 +122,12 @@ class ConnectionString {
bool operator==(const ConnectionString& other) const;
bool operator!=(const ConnectionString& other) const;

std::unique_ptr<DBClientBase> connect(StringData applicationName,
std::string& errmsg,
double socketTimeout = 0,
const MongoURI* uri = nullptr) const;
std::unique_ptr<DBClientBase> connect(
StringData applicationName,
std::string& errmsg,
double socketTimeout = 0,
const MongoURI* uri = nullptr,
const ClientAPIVersionParameters* apiParameters = nullptr) const;

static StatusWith<ConnectionString> parse(const std::string& url);

Expand All @@ -147,9 +150,11 @@ class ConnectionString {
virtual ~ConnectionHook() {}

// Returns an alternative connection object for a string
virtual std::unique_ptr<DBClientBase> connect(const ConnectionString& c,
std::string& errmsg,
double socketTimeout) = 0;
virtual std::unique_ptr<DBClientBase> connect(
const ConnectionString& c,
std::string& errmsg,
double socketTimeout,
const ClientAPIVersionParameters* apiParameters = nullptr) = 0;
};

static void setConnectionHook(ConnectionHook* hook) {
Expand Down
24 changes: 16 additions & 8 deletions src/mongo/client/connection_string_connect.cpp
Expand Up @@ -46,10 +46,12 @@ namespace mongo {
Mutex ConnectionString::_connectHookMutex = MONGO_MAKE_LATCH();
ConnectionString::ConnectionHook* ConnectionString::_connectHook = nullptr;

std::unique_ptr<DBClientBase> ConnectionString::connect(StringData applicationName,
std::string& errmsg,
double socketTimeout,
const MongoURI* uri) const {
std::unique_ptr<DBClientBase> ConnectionString::connect(
StringData applicationName,
std::string& errmsg,
double socketTimeout,
const MongoURI* uri,
const ClientAPIVersionParameters* apiParameters) const {
MongoURI newURI{};
if (uri) {
newURI = *uri;
Expand All @@ -58,7 +60,8 @@ std::unique_ptr<DBClientBase> ConnectionString::connect(StringData applicationNa
switch (_type) {
case MASTER: {
for (const auto& server : _servers) {
auto c = std::make_unique<DBClientConnection>(true, 0, newURI);
auto c = std::make_unique<DBClientConnection>(
true, 0, newURI, DBClientConnection::HandshakeValidationHook(), apiParameters);

c->setSoTimeout(socketTimeout);
LOGV2_DEBUG(20109,
Expand All @@ -76,8 +79,12 @@ std::unique_ptr<DBClientBase> ConnectionString::connect(StringData applicationNa
}

case SET: {
auto set = std::make_unique<DBClientReplicaSet>(
_setName, _servers, applicationName, socketTimeout, std::move(newURI));
auto set = std::make_unique<DBClientReplicaSet>(_setName,
_servers,
applicationName,
socketTimeout,
std::move(newURI),
apiParameters);
if (!set->connect()) {
errmsg = "connect failed to replica set ";
errmsg += toString();
Expand All @@ -98,7 +105,8 @@ std::unique_ptr<DBClientBase> ConnectionString::connect(StringData applicationNa
_connectHook);

// Double-checked lock, since this will never be active during normal operation
auto replacementConn = _connectHook->connect(*this, errmsg, socketTimeout);
auto replacementConn =
_connectHook->connect(*this, errmsg, socketTimeout, apiParameters);

LOGV2(20111,
"Replacing connection to {oldConnString} with {newConnString}",
Expand Down

0 comments on commit d3064df

Please sign in to comment.