Files
mongo/jstests/libs/override_methods/simulate_atlas_proxy.js
ben-gawel 424337fddb SERVER-96711 Refactor atlas proxy to remove tenant migration references (#28677)
GitOrigin-RevId: 72b2647eec00b889608778023f95f2ba618e2df2
2024-11-07 00:03:50 +00:00

231 lines
8.0 KiB
JavaScript

/**
* Overrides the runCommand method to prefix all databases and namespaces ("config", "admin",
* "local" excluded) with a tenant prefix.
*/
import {OverrideHelpers} from "jstests/libs/override_methods/override_helpers.js";
import {
createCmdObjWithTenantId,
getTenantIdForDatabase,
isCmdObjWithTenantId,
prependTenantIdToDbNameIfApplicable,
removeTenantIdAndMaybeCheckPrefixes,
} from "jstests/serverless/libs/tenant_prefixing.js";
// Assert that some tenantIds are provided
assert(!!TestData.tenantId || (TestData.tenantIds && TestData.tenantIds.length > 0),
"Missing required tenantId or tenantIds");
// Save references to the original methods in the IIFE's scope.
// This scoping allows the original methods to be called by the overrides below.
const originalRunCommand = Mongo.prototype.runCommand;
// Save a reference to the connection created at shell startup. This will be used as a proxy for
// multiple internal routing connections for the lifetime of the test execution. If there is no
// initial connection, then we will not perform connection routing when using this override.
const initialConn = (typeof db !== 'undefined') ? db.getMongo() : undefined;
/**
* Asserts that the provided connection is an internal routing connection, not the top-level proxy
* connection. The proxy connection also has an internal routing connection, so it is excluded from
* this check.
*/
function assertRoutingConnection(conn) {
if (conn !== initialConn) {
assert.eq(null,
conn._internalRoutingConnection,
"Expected connection to have no internal routing connection.");
}
}
/**
* @returns The internal routing connection for a provided connection
*/
function getRoutingConnection(conn) {
if (conn === initialConn && conn._internalRoutingConnection == null) {
conn._internalRoutingConnection = conn;
}
// Since we are patching the prototype below, there must eventually be a "base case" for
// determining which connection to run a method on. If the provided `conn` has no internal
// routing connection, we assume that it _is_ the internal routing connection, and return
// here.
if (conn._internalRoutingConnection == null) {
return conn;
}
// Sanity check ensuring we have not accidentally created an internal routing connection on an
// internal routing connection.
assertRoutingConnection(conn._internalRoutingConnection);
return conn._internalRoutingConnection;
}
function toIndexSet(indexedDocs) {
let set = new Set();
if (indexedDocs) {
for (let doc of indexedDocs) {
set.add(doc.index);
}
}
return set;
}
/**
* Remove the indices for non-upsert writes that succeeded.
*/
function removeSuccessfulOpIndexesExceptForUpserted(resObj, indexMap, ordered) {
// Optimization to only look through the indices in a set rather than in an array.
let indexSetForUpserted = toIndexSet(resObj.upserted);
let indexSetForWriteErrors = toIndexSet(resObj.writeErrors);
for (let index in Object.keys(indexMap)) {
if ((!indexSetForUpserted.has(parseInt(index)) &&
!(ordered && resObj.writeErrors && (index > resObj.writeErrors[0].index)) &&
!indexSetForWriteErrors.has(parseInt(index)))) {
delete indexMap[index];
}
}
return indexMap;
}
/**
* Rewrites a server connection string (ex: rsName/host,host,host) to a URI that the shell can
* connect to.
*/
function convertServerConnectionStringToURI(input) {
const inputParts = input.split('/');
return `mongodb://${inputParts[1]}/?replicaSet=${inputParts[0]}`;
}
/**
* Executes 'cmdObjWithTenantId'
* 'dbNameWithTenantId' is only used for logging.
*/
function runCommandWithTenantId(
conn, securityToken, dbNameWithTenantId, cmdObjWithTenantId, options) {
// 'indexMap' is a mapping from a write's index in the current cmdObj to its index in the
// original cmdObj.
let indexMap = {};
if (cmdObjWithTenantId.documents) {
for (let i = 0; i < cmdObjWithTenantId.documents.length; i++) {
indexMap[i] = i;
}
}
if (cmdObjWithTenantId.updates) {
for (let i = 0; i < cmdObjWithTenantId.updates.length; i++) {
indexMap[i] = i;
}
}
if (cmdObjWithTenantId.deletes) {
for (let i = 0; i < cmdObjWithTenantId.deletes.length; i++) {
indexMap[i] = i;
}
}
const newConn = getRoutingConnection(conn);
if (securityToken) {
newConn._setSecurityToken(securityToken);
}
return originalRunCommand.apply(newConn, [dbNameWithTenantId, cmdObjWithTenantId, options]);
}
Mongo.prototype.runCommand = function(dbName, cmdObj, options) {
const useSecurityToken = !!TestData.useSecurityToken;
const useResponsePrefixChecking = !!TestData.useResponsePrefixChecking;
const tenantId = getTenantIdForDatabase(dbName);
const dbNameWithTenantId = prependTenantIdToDbNameIfApplicable(dbName, tenantId);
const securityToken = useSecurityToken
? _createTenantToken({tenant: ObjectId(tenantId), expectPrefix: true})
: undefined;
// If the command is already prefixed, just run it
if (isCmdObjWithTenantId(cmdObj)) {
return runCommandWithTenantId(this, securityToken, dbNameWithTenantId, cmdObj, options);
}
// Prepend a tenant prefix to all database names and namespaces, where applicable.
const cmdObjWithTenantId = createCmdObjWithTenantId(cmdObj, tenantId);
const resObj = runCommandWithTenantId(
this, securityToken, dbNameWithTenantId, cmdObjWithTenantId, options);
// Remove the tenant prefix from all database names and namespaces in the result since tests
// assume the command was run against the original database.
const cmdName = Object.keys(cmdObj)[0];
let checkPrefixOptions = !useResponsePrefixChecking ? {} : {
checkPrefix: true,
expectPrefix: true,
tenantId,
dbName,
cmdName,
debugLog: "Failed to check tenant prefix in response : " + tojsononeline(resObj) +
". The request command obj is " + tojsononeline(cmdObjWithTenantId)
};
removeTenantIdAndMaybeCheckPrefixes(resObj, checkPrefixOptions);
return resObj;
};
Mongo.prototype.getDbNameWithTenantPrefix = function(dbName) {
const tenantId = getTenantIdForDatabase(dbName);
return prependTenantIdToDbNameIfApplicable(dbName, tenantId);
};
// Override base methods on the Mongo prototype to try to proxy the call to the underlying
// internal routing connection, if one exists.
// NOTE: This list is derived from scripting/mozjs/mongo.cpp:62.
['auth',
'cleanup',
'close',
'compact',
'getAutoEncryptionOptions',
'isAutoEncryptionEnabled',
'cursorHandleFromId',
'find',
'generateDataKey',
'getDataKeyCollection',
'logout',
'encrypt',
'decrypt',
'isReplicaSetConnection',
'_markNodeAsFailed',
'getMinWireVersion',
'getMaxWireVersion',
'isReplicaSetMember',
'isMongos',
'isTLS',
'getApiParameters',
'_startSession',
'_refreshAccessToken',
// Don't override this method, since it is never called directly in jstests. The expectation of is
// that it will be run on the connection `Mongo.prototype.runCommand` chose.
// '_runCommandImpl',
].forEach(methodName => {
const $method = Mongo.prototype[methodName];
Mongo.prototype[methodName] = function() {
return $method.apply(getRoutingConnection(this), arguments);
};
});
// The following methods are overridden so that the method applies to both
// the proxy connection and the underlying internal routing connection, if one exists.
['toggleAutoEncryption',
'unsetAutoEncryption',
'setAutoEncryption',
].forEach(methodName => {
const $method = Mongo.prototype[methodName];
Mongo.prototype[methodName] = function() {
let rc = getRoutingConnection(this);
if (rc !== this) {
$method.apply(rc, arguments);
}
return $method.apply(this, arguments);
};
});
OverrideHelpers.prependOverrideInParallelShell(
"jstests/libs/override_methods/simulate_atlas_proxy.js");