Created
October 12, 2021 14:24
-
-
Save mcasimir/7f089a2e66018cf12c4cc1e516b08526 to your computer and use it in GitHub Desktop.
instance-detail.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { union, unionBy } from 'lodash'; | |
import { AnyError, MongoClient, ReadPreference } from 'mongodb'; | |
import { | |
isEnterprise, | |
getGenuineMongoDB, | |
getDataLake, | |
} from 'mongodb-build-info'; | |
import createLogger from '@mongodb-js/compass-logging'; | |
const { debug } = createLogger('COMPASS-CONNECT'); | |
// eslint-disable-next-line @typescript-eslint/no-var-requires | |
const toNS = require('mongodb-ns'); | |
interface HostInfoDetails { | |
system_time?: any; // ISODate? | |
hostname?: string; | |
os?: string; | |
os_family?: string; | |
kernel_version?: string; | |
kernel_version_string?: string; | |
memory_bits?: number; | |
memory_page_size?: number; | |
arch?: string; | |
cpu_cores?: number; | |
cpu_cores_physical?: number; | |
cpu_scheduler?: string; | |
cpu_frequency?: number; | |
cpu_string?: string; | |
cpu_bits?: number; | |
machine_model?: string; | |
feature_numa?: boolean; | |
feature_always_full_sync?: number; | |
feature_nfs_async?: number; | |
} | |
interface GenuineMongoDBDetails { | |
isGenuine: boolean; | |
dbType: string; | |
} | |
interface BuildInfoDetails { | |
version: string; | |
commit: string; | |
commit_url: string; | |
flags_loader: any; | |
flags_compiler: any; | |
allocator: string; | |
javascript_engine: string; | |
debug: boolean; | |
for_bits: number; | |
max_bson_object_size: number; | |
enterprise_module: boolean; | |
query_engine: any; | |
} | |
interface DataLakeDetails { | |
isDataLake: boolean; | |
version: string; | |
} | |
interface InstanceDetails { | |
build: BuildInfoDetails; | |
dataLake: DataLakeDetails; | |
genuineMongoDB: GenuineMongoDBDetails; | |
host: HostInfoDetails; | |
featureCompatibilityVersion: string; | |
databases: any[]; | |
} | |
export async function getInstance( | |
client: MongoClient | |
): Promise<InstanceDetails> { | |
const [ | |
connectionStatus, | |
getCmdLineOptsResult, | |
hostInfoResult, | |
buildInfoResult, | |
listDatabasesResult, | |
getParameterResult, | |
] = await Promise.all([ | |
runAdminCommand(client, { connectionStatus: 1, showPrivileges: true }), | |
runAdminCommand(client, { getCmdLineOpts: 1 }, null), | |
runAdminCommand(client, { hostInfo: 1 }), | |
runAdminCommand(client, { buildInfo: 1 }), | |
runAdminCommand(client, { | |
listDatabases: 1, | |
nameOnly: true, | |
authorizedDatabases: true, | |
}), | |
runAdminCommand(client, { | |
getParameter: 1, | |
featureCompatibilityVersion: 1, | |
}), | |
]); | |
const databases = await fetchDatabases( | |
client, | |
connectionStatus, | |
listDatabasesResult | |
); | |
return { | |
build: adaptBuildInfo(buildInfoResult), | |
dataLake: buildDataLakeInfo(buildInfoResult), | |
genuineMongoDB: buildGenuineMongoDBInfo( | |
buildInfoResult, | |
getCmdLineOptsResult | |
), | |
host: adaptHostInfo(hostInfoResult), | |
databases: databases, | |
featureCompatibilityVersion: | |
getParameterResult?.featureCompatibilityVersion?.version, | |
}; | |
} | |
function runAdminCommand( | |
client: MongoClient, | |
spec: any, | |
fallback: any = {} | |
): Promise<any> { | |
const adminDb = client.db('admin'); | |
const readPreference = client.readPreference || ReadPreference.PRIMARY; | |
return adminDb | |
.command(spec, { readPreference }) | |
.catch(ignoreNotAuthorized(fallback)); | |
} | |
function buildGenuineMongoDBInfo( | |
buildInfo: any, | |
cmdLineOpts: any | |
): GenuineMongoDBDetails { | |
const { isGenuine, serverName } = getGenuineMongoDB(buildInfo, cmdLineOpts); | |
return { | |
isGenuine, | |
dbType: serverName, | |
}; | |
} | |
function buildDataLakeInfo(buildInfo: any): DataLakeDetails { | |
const { isDataLake, dlVersion } = getDataLake(buildInfo); | |
return { | |
isDataLake, | |
version: dlVersion, | |
}; | |
} | |
async function fetchDatabases( | |
client: MongoClient, | |
connectionStatus: any, | |
listDatabaseCommandResult: any | |
) { | |
const privileges = extractPrivilegesByDatabaseAndCollection(connectionStatus); | |
const listedDatabaseNames = ( | |
(listDatabaseCommandResult || {}).databases || [] | |
).map(({ name }: { name: string }) => name); | |
// we pull in the database names listed among the user privileges. | |
// this accounts for situations where a user would not have rights to listDatabases | |
// on the cluster but is authorized to perform actions on specific databases. | |
const databasesFromPrivileges = Object.keys(privileges); | |
const databaseNames = union( | |
listedDatabaseNames, | |
databasesFromPrivileges | |
).filter((databaseName) => !!databaseName); | |
const databases = ( | |
await Promise.all( | |
databaseNames.map((name) => | |
fetchDatabaseWithCollections(client, name, privileges) | |
) | |
) | |
) | |
.filter(Boolean) | |
.filter(({ name }) => name); | |
return databases; | |
} | |
function extractPrivilegesByDatabaseAndCollection(connectionStatus: any) { | |
const privileges = | |
connectionStatus?.authInfo?.authenticatedUserPrivileges || []; | |
const databases: Record<string, any> = {}; | |
for (const privilege of privileges) { | |
const { db, collection } = (privilege || {}).resource || {}; | |
databases[db] = { [collection || '']: privilege.actions || [] }; | |
} | |
return databases; | |
} | |
async function fetchDatabaseWithCollections( | |
client: MongoClient, | |
dbName: string, | |
privileges: Record<string, any> = {} | |
) { | |
const db = client.db(dbName); | |
/** | |
* @note: Durran: For some reason the listCollections call does not take into | |
* account the read preference that was set on the db instance - it only looks | |
* in the passed options: https://github.com/mongodb/node-mongodb-native/blob/2.2/lib/db.js#L671 | |
*/ | |
const readPreference = client.readPreference || ReadPreference.PRIMARY; | |
const [database, rawCollections] = await Promise.all([ | |
db | |
.command({ dbStats: 1 }) | |
.catch( | |
ignoreNotAuthorized({ | |
db: dbName, | |
}) | |
) | |
.then(adaptDatabaseInfo), | |
db | |
.listCollections( | |
{ nameOnly: true, authorizedCollections: true }, | |
{ readPreference } | |
) | |
.toArray() | |
.catch(ignoreNotAuthorized([])) | |
.catch(ignoreMongosLocalException([])), | |
]); | |
const listedCollections = rawCollections.map((rawCollection) => ({ | |
db: dbName, | |
...rawCollection, | |
})); | |
const collectionsFromPrivileges = Object.keys(privileges[dbName] || {}) | |
.filter(Boolean) | |
.filter((name) => !isSystemCollection(name)) | |
.map((name) => ({ | |
db: dbName, | |
name, | |
})); | |
const collections = unionBy( | |
listedCollections, | |
collectionsFromPrivileges, | |
'name' | |
) | |
.filter(Boolean) | |
.filter(({ name, db }) => name && db) | |
.map(adaptCollectionInfo); | |
return { | |
...database, | |
collections, | |
}; | |
} | |
function isSystemCollection(name: string) { | |
return name.startsWith('system.'); | |
} | |
function isNotAuthorized(err: AnyError) { | |
if (!err) { | |
return false; | |
} | |
const msg = err.message || JSON.stringify(err); | |
return new RegExp('not (authorized|allowed)').test(msg); | |
} | |
function isMongosLocalException(err: AnyError) { | |
if (!err) { | |
return false; | |
} | |
const msg = err.message || JSON.stringify(err); | |
return new RegExp('database through mongos').test(msg); | |
} | |
function ignoreNotAuthorized<T>(fallback: T): (err: AnyError) => Promise<T> { | |
return (err: AnyError) => { | |
if (isNotAuthorized(err)) { | |
debug('ignoring not authorized error and returning fallback value:', { | |
err, | |
fallback, | |
}); | |
return Promise.resolve(fallback); | |
} | |
return Promise.reject(err); | |
}; | |
} | |
function ignoreMongosLocalException<T>( | |
fallback: T | |
): (err: AnyError) => Promise<T> { | |
return (err: AnyError) => { | |
if (isMongosLocalException(err)) { | |
debug( | |
'ignoring mongos action on local db error and returning fallback value:', | |
{ | |
err, | |
fallback, | |
} | |
); | |
return Promise.resolve(fallback); | |
} | |
return Promise.reject(err); | |
}; | |
} | |
function adaptHostInfo(rawHostInfo: any): HostInfoDetails { | |
return { | |
system_time: rawHostInfo?.system?.currentTime, | |
hostname: rawHostInfo?.system?.hostname || 'unknown', | |
os: rawHostInfo?.os?.name, | |
os_family: (rawHostInfo?.os?.type || '').toLowerCase(), | |
kernel_version: rawHostInfo?.os?.version, | |
kernel_version_string: rawHostInfo?.extra?.versionString, | |
memory_bits: | |
parseInt(rawHostInfo?.system?.memSizeMB || 0, 10) * 1024 * 1024, | |
memory_page_size: rawHostInfo?.extra?.pageSize, | |
arch: rawHostInfo?.system?.cpuArch, | |
cpu_cores: rawHostInfo?.system?.numCores, | |
cpu_cores_physical: rawHostInfo?.extra?.physicalCores, | |
cpu_scheduler: rawHostInfo?.extra?.scheduler, | |
cpu_frequency: | |
parseInt(rawHostInfo?.extra?.cpuFrequencyMHz || 0, 10) * 1_000_000, | |
cpu_string: rawHostInfo?.extra?.cpuString, | |
cpu_bits: rawHostInfo?.system?.cpuAddrSize, | |
machine_model: rawHostInfo?.extra?.model, | |
feature_numa: rawHostInfo?.system?.numaEnabled, | |
feature_always_full_sync: rawHostInfo?.extra?.alwaysFullSync, | |
feature_nfs_async: rawHostInfo?.extra?.nfsAsync, | |
}; | |
} | |
function adaptBuildInfo(rawBuildInfo: any): BuildInfoDetails { | |
return { | |
version: rawBuildInfo.version, | |
commit: rawBuildInfo.gitVersion, | |
commit_url: rawBuildInfo.gitVersion | |
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
`https://github.com/mongodb/mongo/commit/${rawBuildInfo.gitVersion}` | |
: '', | |
flags_loader: rawBuildInfo.loaderFlags, | |
flags_compiler: rawBuildInfo.compilerFlags, | |
allocator: rawBuildInfo.allocator, | |
javascript_engine: rawBuildInfo.javascriptEngine, | |
debug: rawBuildInfo.debug, | |
for_bits: rawBuildInfo.bits, | |
max_bson_object_size: rawBuildInfo.maxBsonObjectSize, | |
// Cover both cases of detecting enterprise module, see SERVER-18099. | |
enterprise_module: isEnterprise(rawBuildInfo), | |
query_engine: rawBuildInfo.queryEngine ? rawBuildInfo.queryEngine : null, | |
}; | |
} | |
function adaptDatabaseInfo(databaseStats: any = {}) { | |
return { | |
_id: databaseStats.db, | |
name: databaseStats.db, | |
document_count: databaseStats.objects || 0, | |
storage_size: databaseStats.storageSize || 0, | |
index_count: databaseStats.indexes || 0, | |
index_size: databaseStats.indexSize || 0, | |
}; | |
} | |
function adaptCollectionInfo(rawCollectionInfo: any) { | |
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands | |
const ns = toNS(rawCollectionInfo.db + '.' + rawCollectionInfo.name); | |
return { | |
_id: ns.toString(), | |
name: ns.collection, | |
database: ns.database, | |
readonly: rawCollectionInfo?.info?.readOnly || false, | |
collation: rawCollectionInfo?.options?.collation, | |
type: rawCollectionInfo?.type || 'collection', | |
view_on: rawCollectionInfo?.options?.viewOn, | |
pipeline: rawCollectionInfo?.options?.pipeline, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment