Skip to content

Commit 28b7040

Browse files
feat(NODE-5968): container and Kubernetes awareness in client metadata (#4005)
1 parent e30c6d3 commit 28b7040

File tree

11 files changed

+247
-19
lines changed

11 files changed

+247
-19
lines changed

src/cmap/connect.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
type ConnectionOptions,
2626
CryptoConnection
2727
} from './connection';
28-
import type { ClientMetadata } from './handshake/client_metadata';
2928
import {
3029
MAX_SUPPORTED_SERVER_VERSION,
3130
MAX_SUPPORTED_WIRE_VERSION,
@@ -183,7 +182,7 @@ export interface HandshakeDocument extends Document {
183182
ismaster?: boolean;
184183
hello?: boolean;
185184
helloOk?: boolean;
186-
client: ClientMetadata;
185+
client: Document;
187186
compression: string[];
188187
saslSupportedMechs?: string;
189188
loadBalanced?: boolean;
@@ -200,11 +199,12 @@ export async function prepareHandshakeDocument(
200199
const options = authContext.options;
201200
const compressors = options.compressors ? options.compressors : [];
202201
const { serverApi } = authContext.connection;
202+
const clientMetadata: Document = await options.extendedMetadata;
203203

204204
const handshakeDoc: HandshakeDocument = {
205205
[serverApi?.version || options.loadBalanced === true ? 'hello' : LEGACY_HELLO_COMMAND]: 1,
206206
helloOk: true,
207-
client: options.metadata,
207+
client: clientMetadata,
208208
compression: compressors
209209
};
210210

src/cmap/connection.ts

+2
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export interface ConnectionOptions
119119
cancellationToken?: CancellationToken;
120120
metadata: ClientMetadata;
121121
/** @internal */
122+
extendedMetadata: Promise<Document>;
123+
/** @internal */
122124
mongoLogger?: MongoLogger | undefined;
123125
}
124126

src/cmap/connection_pool.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
233233
maxIdleTimeMS: options.maxIdleTimeMS ?? 0,
234234
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
235235
minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100,
236-
autoEncrypter: options.autoEncrypter,
237-
metadata: options.metadata
236+
autoEncrypter: options.autoEncrypter
238237
});
239238

240239
if (this.options.minPoolSize > this.options.maxPoolSize) {

src/cmap/handshake/client_metadata.ts

+54-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { promises as fs } from 'fs';
12
import * as os from 'os';
23
import * as process from 'process';
34

4-
import { BSON, Int32 } from '../../bson';
5+
import { BSON, type Document, Int32 } from '../../bson';
56
import { MongoInvalidArgumentError } from '../../error';
67
import type { MongoOptions } from '../../mongo_client';
78

@@ -71,13 +72,13 @@ export class LimitedSizeDocument {
7172
return true;
7273
}
7374

74-
toObject(): ClientMetadata {
75+
toObject(): Document {
7576
return BSON.deserialize(BSON.serialize(this.document), {
7677
promoteLongs: false,
7778
promoteBuffers: false,
7879
promoteValues: false,
7980
useBigInt64: false
80-
}) as ClientMetadata;
81+
});
8182
}
8283
}
8384

@@ -152,8 +153,57 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
152153
}
153154
}
154155
}
156+
return metadataDocument.toObject() as ClientMetadata;
157+
}
158+
159+
let dockerPromise: Promise<boolean>;
160+
/** @internal */
161+
async function getContainerMetadata() {
162+
const containerMetadata: Record<string, any> = {};
163+
dockerPromise ??= fs.access('/.dockerenv').then(
164+
() => true,
165+
() => false
166+
);
167+
const isDocker = await dockerPromise;
168+
169+
const { KUBERNETES_SERVICE_HOST = '' } = process.env;
170+
const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false;
171+
172+
if (isDocker) containerMetadata.runtime = 'docker';
173+
if (isKubernetes) containerMetadata.orchestrator = 'kubernetes';
174+
175+
return containerMetadata;
176+
}
177+
178+
/**
179+
* @internal
180+
* Re-add each metadata value.
181+
* Attempt to add new env container metadata, but keep old data if it does not fit.
182+
*/
183+
export async function addContainerMetadata(originalMetadata: ClientMetadata) {
184+
const containerMetadata = await getContainerMetadata();
185+
if (Object.keys(containerMetadata).length === 0) return originalMetadata;
186+
187+
const extendedMetadata = new LimitedSizeDocument(512);
188+
189+
const extendedEnvMetadata = { ...originalMetadata?.env, container: containerMetadata };
190+
191+
for (const [key, val] of Object.entries(originalMetadata)) {
192+
if (key !== 'env') {
193+
extendedMetadata.ifItFitsItSits(key, val);
194+
} else {
195+
if (!extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata)) {
196+
// add in old data if newer / extended metadata does not fit
197+
extendedMetadata.ifItFitsItSits('env', val);
198+
}
199+
}
200+
}
201+
202+
if (!('env' in originalMetadata)) {
203+
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
204+
}
155205

156-
return metadataDocument.toObject();
206+
return extendedMetadata.toObject();
157207
}
158208

159209
/**

src/connection_string.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { URLSearchParams } from 'url';
55
import type { Document } from './bson';
66
import { MongoCredentials } from './cmap/auth/mongo_credentials';
77
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';
8-
import { makeClientMetadata } from './cmap/handshake/client_metadata';
8+
import { addContainerMetadata, makeClientMetadata } from './cmap/handshake/client_metadata';
99
import { Compressor, type CompressorName } from './cmap/wire_protocol/compression';
1010
import { Encrypter } from './encrypter';
1111
import {
@@ -552,6 +552,10 @@ export function parseOptions(
552552

553553
mongoOptions.metadata = makeClientMetadata(mongoOptions);
554554

555+
mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).catch(() => {
556+
/* rejections will be handled later */
557+
});
558+
555559
return mongoOptions;
556560
}
557561

src/mongo_client.ts

+2
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,8 @@ export interface MongoOptions
827827
dbName: string;
828828
metadata: ClientMetadata;
829829
/** @internal */
830+
extendedMetadata: Promise<Document>;
831+
/** @internal */
830832
autoEncrypter?: AutoEncrypter;
831833
proxyHost?: string;
832834
proxyPort?: number;

src/sdam/topology.ts

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
158158
directConnection: boolean;
159159
loadBalanced: boolean;
160160
metadata: ClientMetadata;
161+
extendedMetadata: Promise<Document>;
161162
serverMonitoringMode: ServerMonitoringMode;
162163
/** MongoDB server API version */
163164
serverApi?: ServerApi;

test/integration/connection-monitoring-and-pooling/connection.test.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from 'chai';
22

33
import {
4+
addContainerMetadata,
45
connect,
56
Connection,
67
type ConnectionOptions,
@@ -50,7 +51,8 @@ describe('Connection', function () {
5051
...commonConnectOptions,
5152
connectionType: Connection,
5253
...this.configuration.options,
53-
metadata: makeClientMetadata({ driverInfo: {} })
54+
metadata: makeClientMetadata({ driverInfo: {} }),
55+
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
5456
};
5557

5658
let conn;
@@ -72,7 +74,8 @@ describe('Connection', function () {
7274
connectionType: Connection,
7375
...this.configuration.options,
7476
monitorCommands: true,
75-
metadata: makeClientMetadata({ driverInfo: {} })
77+
metadata: makeClientMetadata({ driverInfo: {} }),
78+
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
7679
};
7780

7881
let conn;

test/tools/cmap_spec_runner.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { clearTimeout, setTimeout } from 'timers';
44
import { promisify } from 'util';
55

66
import {
7+
addContainerMetadata,
78
CMAP_EVENTS,
89
type Connection,
910
ConnectionPool,
@@ -369,6 +370,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
369370
}
370371

371372
const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} });
373+
const extendedMetadata = addContainerMetadata(metadata);
372374
delete poolOptions.appName;
373375

374376
const operations = test.operations;
@@ -380,7 +382,12 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
380382
const mainThread = threadContext.getThread(MAIN_THREAD_KEY);
381383
mainThread.start();
382384

383-
threadContext.createPool({ ...poolOptions, metadata, minPoolSizeCheckFrequencyMS });
385+
threadContext.createPool({
386+
...poolOptions,
387+
metadata,
388+
extendedMetadata,
389+
minPoolSizeCheckFrequencyMS
390+
});
384391
// yield control back to the event loop so that the ConnectionPoolCreatedEvent
385392
// has a chance to be fired before any synchronously-emitted events from
386393
// the queued operations

0 commit comments

Comments
 (0)