Skip to content

Commit be43714

Browse files
authored
feat(build-info): identify server name COMPASS-9930 (#585)
* Add a new identifyServerName function * Add firestore regexp * Add pg_documentdb * Deprecate `getGenuineMongoDB` * Add tests * Add debug calls * Fail gracefully if the buildInfo command fails * Update signature of identifyServerName to take and options object and only adminCommand * Refactored to call commands in parallel
1 parent fb0eb51 commit be43714

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mongodb-build-info/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"typescript": "^5.0.4"
7878
},
7979
"dependencies": {
80+
"debug": "^4.4.0",
8081
"mongodb-connection-string-url": "^3.0.0"
8182
}
8283
}

packages/mongodb-build-info/src/index.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import ConnectionString from 'mongodb-connection-string-url';
2+
import { debug as createDebug } from 'debug';
3+
4+
const debug = createDebug('mongodb-build-info');
5+
6+
type Document = Record<string, unknown>;
27

38
const ATLAS_REGEX = /\.mongodb(-dev|-qa|-stage)?\.net$/i;
49
const ATLAS_STREAM_REGEX = /^atlas-stream-.+/i;
@@ -7,6 +12,7 @@ const LOCALHOST_REGEX =
712
const DIGITAL_OCEAN_REGEX = /\.mongo\.ondigitalocean\.com$/i;
813
const COSMOS_DB_REGEX = /\.cosmos\.azure\.com$/i;
914
const DOCUMENT_DB_REGEX = /docdb(-elastic)?\.amazonaws\.com$/i;
15+
const FIRESTORE_REGEX = /\.firestore.goog$/i;
1016

1117
function isRecord(value: unknown): value is Record<string, unknown> {
1218
return typeof value === 'object' && value !== null;
@@ -109,6 +115,9 @@ export function isDigitalOcean(uri: string): boolean {
109115
return !!getHostnameFromUrl(uri).match(DIGITAL_OCEAN_REGEX);
110116
}
111117

118+
/**
119+
* @deprecated Use `identifyServerName` instead.
120+
*/
112121
export function getGenuineMongoDB(uri: string): {
113122
isGenuine: boolean;
114123
serverName: string;
@@ -134,6 +143,73 @@ export function getGenuineMongoDB(uri: string): {
134143
};
135144
}
136145

146+
type IdentifyServerNameOptions = {
147+
connectionString: string;
148+
adminCommand: (command: Document) => Promise<Document>;
149+
};
150+
151+
/**
152+
* Identify the server name based on connection string and server responses.
153+
* @returns A name of the server, "unknown" if we fail to identify it.
154+
*/
155+
export async function identifyServerName({
156+
connectionString,
157+
adminCommand,
158+
}: IdentifyServerNameOptions): Promise<string> {
159+
try {
160+
const hostname = getHostnameFromUrl(connectionString);
161+
if (hostname.match(COSMOS_DB_REGEX)) {
162+
return 'cosmosdb';
163+
}
164+
165+
if (hostname.match(DOCUMENT_DB_REGEX)) {
166+
return 'documentdb';
167+
}
168+
169+
if (hostname.match(FIRESTORE_REGEX)) {
170+
return 'firestore';
171+
}
172+
173+
const candidates = await Promise.all([
174+
adminCommand({ buildInfo: 1 }).then(
175+
(response) => {
176+
if ('ferretdb' in response) {
177+
return ['ferretdb'];
178+
} else {
179+
return [];
180+
}
181+
},
182+
(error: unknown) => {
183+
debug('buildInfo command failed %O', error);
184+
return [];
185+
},
186+
),
187+
adminCommand({ getParameter: 'foo' }).then(
188+
// A successful response doesn't represent a signal
189+
() => [],
190+
(error: unknown) => {
191+
if (error instanceof Error && /documentdb_api/.test(error.message)) {
192+
return ['pg_documentdb'];
193+
} else {
194+
return [];
195+
}
196+
},
197+
),
198+
]).then((results) => results.flat());
199+
200+
if (candidates.length === 0) {
201+
return 'mongodb';
202+
} else if (candidates.length === 1) {
203+
return candidates[0];
204+
} else {
205+
return 'unknown';
206+
}
207+
} catch (error) {
208+
debug('Failed to identify server name', error);
209+
return 'unknown';
210+
}
211+
}
212+
137213
export function getBuildEnv(buildInfo: unknown): {
138214
serverOs: string | null;
139215
serverArch: string | null;

packages/mongodb-build-info/test/fixtures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export const DOCUMENT_DB_URIS = [
8080
'mongodb://x:y@elastic-docdb-123456789.eu-central-1.docdb-elastic.amazonaws.com:27017',
8181
];
8282

83+
export const FIRESTORE_URIS = [
84+
'mongodb://x:y@bbccdaf5-527a-4be5-9881-b7073e92002b.europe-north2.firestore.goog:443/test-db?loadBalanced=true&tls=true&authMechanism=SCRAM-SHA-256&retryWrites=false',
85+
];
86+
8387
export const COSMOSDB_BUILD_INFO = {
8488
_t: 'BuildInfoResponse',
8589
ok: 1,

packages/mongodb-build-info/test/index.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from 'chai';
2+
23
import * as fixtures from './fixtures';
34
import {
45
isAtlas,
@@ -10,6 +11,7 @@ import {
1011
getBuildEnv,
1112
isEnterprise,
1213
getGenuineMongoDB,
14+
identifyServerName,
1315
} from '../src/index';
1416

1517
describe('mongodb-build-info', function () {
@@ -428,4 +430,74 @@ describe('mongodb-build-info', function () {
428430
expect(isGenuine.serverName).to.equal('mongodb');
429431
});
430432
});
433+
434+
context('identifyServerName', function () {
435+
function fail() {
436+
return Promise.reject(new Error('Should not be called'));
437+
}
438+
439+
it('reports CosmosDB', async function () {
440+
for (const connectionString of fixtures.COSMOS_DB_URI) {
441+
const result = await identifyServerName({
442+
connectionString,
443+
adminCommand: fail,
444+
});
445+
expect(result).to.equal('cosmosdb');
446+
}
447+
});
448+
449+
it('reports DocumentDB', async function () {
450+
for (const connectionString of fixtures.DOCUMENT_DB_URIS) {
451+
const result = await identifyServerName({
452+
connectionString,
453+
adminCommand: fail,
454+
});
455+
expect(result).to.equal('documentdb');
456+
}
457+
});
458+
459+
it('reports Firestore', async function () {
460+
for (const connectionString of fixtures.FIRESTORE_URIS) {
461+
const result = await identifyServerName({
462+
connectionString,
463+
adminCommand: fail,
464+
});
465+
expect(result).to.equal('firestore');
466+
}
467+
});
468+
469+
it('reports FerretDB', async function () {
470+
const result = await identifyServerName({
471+
connectionString: '',
472+
adminCommand(req) {
473+
if ('buildInfo' in req) {
474+
return Promise.resolve({
475+
ferretdb: {},
476+
});
477+
} else {
478+
return Promise.resolve({});
479+
}
480+
},
481+
});
482+
expect(result).to.equal('ferretdb');
483+
});
484+
485+
it('reports PG DocumentDB', async function () {
486+
const result = await identifyServerName({
487+
connectionString: '',
488+
adminCommand(req) {
489+
if ('getParameter' in req) {
490+
return Promise.reject(
491+
new Error(
492+
'function documentdb_api.get_parameter(boolean, boolean, text[]) does not exist',
493+
),
494+
);
495+
} else {
496+
return Promise.resolve({});
497+
}
498+
},
499+
});
500+
expect(result).to.equal('pg_documentdb');
501+
});
502+
});
431503
});

0 commit comments

Comments
 (0)