Skip to content

Commit

Permalink
feat(NODE-5968): container and Kubernetes awareness in client metadata (
Browse files Browse the repository at this point in the history
  • Loading branch information
aditi-khare-mongoDB authored Mar 6, 2024
1 parent e30c6d3 commit 28b7040
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 19 deletions.
6 changes: 3 additions & 3 deletions src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
type ConnectionOptions,
CryptoConnection
} from './connection';
import type { ClientMetadata } from './handshake/client_metadata';
import {
MAX_SUPPORTED_SERVER_VERSION,
MAX_SUPPORTED_WIRE_VERSION,
Expand Down Expand Up @@ -183,7 +182,7 @@ export interface HandshakeDocument extends Document {
ismaster?: boolean;
hello?: boolean;
helloOk?: boolean;
client: ClientMetadata;
client: Document;
compression: string[];
saslSupportedMechs?: string;
loadBalanced?: boolean;
Expand All @@ -200,11 +199,12 @@ export async function prepareHandshakeDocument(
const options = authContext.options;
const compressors = options.compressors ? options.compressors : [];
const { serverApi } = authContext.connection;
const clientMetadata: Document = await options.extendedMetadata;

const handshakeDoc: HandshakeDocument = {
[serverApi?.version || options.loadBalanced === true ? 'hello' : LEGACY_HELLO_COMMAND]: 1,
helloOk: true,
client: options.metadata,
client: clientMetadata,
compression: compressors
};

Expand Down
2 changes: 2 additions & 0 deletions src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export interface ConnectionOptions
cancellationToken?: CancellationToken;
metadata: ClientMetadata;
/** @internal */
extendedMetadata: Promise<Document>;
/** @internal */
mongoLogger?: MongoLogger | undefined;
}

Expand Down
3 changes: 1 addition & 2 deletions src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
maxIdleTimeMS: options.maxIdleTimeMS ?? 0,
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100,
autoEncrypter: options.autoEncrypter,
metadata: options.metadata
autoEncrypter: options.autoEncrypter
});

if (this.options.minPoolSize > this.options.maxPoolSize) {
Expand Down
58 changes: 54 additions & 4 deletions src/cmap/handshake/client_metadata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as process from 'process';

import { BSON, Int32 } from '../../bson';
import { BSON, type Document, Int32 } from '../../bson';
import { MongoInvalidArgumentError } from '../../error';
import type { MongoOptions } from '../../mongo_client';

Expand Down Expand Up @@ -71,13 +72,13 @@ export class LimitedSizeDocument {
return true;
}

toObject(): ClientMetadata {
toObject(): Document {
return BSON.deserialize(BSON.serialize(this.document), {
promoteLongs: false,
promoteBuffers: false,
promoteValues: false,
useBigInt64: false
}) as ClientMetadata;
});
}
}

Expand Down Expand Up @@ -152,8 +153,57 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
}
}
}
return metadataDocument.toObject() as ClientMetadata;
}

let dockerPromise: Promise<boolean>;
/** @internal */
async function getContainerMetadata() {
const containerMetadata: Record<string, any> = {};
dockerPromise ??= fs.access('/.dockerenv').then(
() => true,
() => false
);
const isDocker = await dockerPromise;

const { KUBERNETES_SERVICE_HOST = '' } = process.env;
const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false;

if (isDocker) containerMetadata.runtime = 'docker';
if (isKubernetes) containerMetadata.orchestrator = 'kubernetes';

return containerMetadata;
}

/**
* @internal
* Re-add each metadata value.
* Attempt to add new env container metadata, but keep old data if it does not fit.
*/
export async function addContainerMetadata(originalMetadata: ClientMetadata) {
const containerMetadata = await getContainerMetadata();
if (Object.keys(containerMetadata).length === 0) return originalMetadata;

const extendedMetadata = new LimitedSizeDocument(512);

const extendedEnvMetadata = { ...originalMetadata?.env, container: containerMetadata };

for (const [key, val] of Object.entries(originalMetadata)) {
if (key !== 'env') {
extendedMetadata.ifItFitsItSits(key, val);
} else {
if (!extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata)) {
// add in old data if newer / extended metadata does not fit
extendedMetadata.ifItFitsItSits('env', val);
}
}
}

if (!('env' in originalMetadata)) {
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
}

return metadataDocument.toObject();
return extendedMetadata.toObject();
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { URLSearchParams } from 'url';
import type { Document } from './bson';
import { MongoCredentials } from './cmap/auth/mongo_credentials';
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';
import { makeClientMetadata } from './cmap/handshake/client_metadata';
import { addContainerMetadata, makeClientMetadata } from './cmap/handshake/client_metadata';
import { Compressor, type CompressorName } from './cmap/wire_protocol/compression';
import { Encrypter } from './encrypter';
import {
Expand Down Expand Up @@ -552,6 +552,10 @@ export function parseOptions(

mongoOptions.metadata = makeClientMetadata(mongoOptions);

mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).catch(() => {
/* rejections will be handled later */
});

return mongoOptions;
}

Expand Down
2 changes: 2 additions & 0 deletions src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,8 @@ export interface MongoOptions
dbName: string;
metadata: ClientMetadata;
/** @internal */
extendedMetadata: Promise<Document>;
/** @internal */
autoEncrypter?: AutoEncrypter;
proxyHost?: string;
proxyPort?: number;
Expand Down
1 change: 1 addition & 0 deletions src/sdam/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
directConnection: boolean;
loadBalanced: boolean;
metadata: ClientMetadata;
extendedMetadata: Promise<Document>;
serverMonitoringMode: ServerMonitoringMode;
/** MongoDB server API version */
serverApi?: ServerApi;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai';

import {
addContainerMetadata,
connect,
Connection,
type ConnectionOptions,
Expand Down Expand Up @@ -50,7 +51,8 @@ describe('Connection', function () {
...commonConnectOptions,
connectionType: Connection,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
metadata: makeClientMetadata({ driverInfo: {} }),
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
};

let conn;
Expand All @@ -72,7 +74,8 @@ describe('Connection', function () {
connectionType: Connection,
...this.configuration.options,
monitorCommands: true,
metadata: makeClientMetadata({ driverInfo: {} })
metadata: makeClientMetadata({ driverInfo: {} }),
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
};

let conn;
Expand Down
9 changes: 8 additions & 1 deletion test/tools/cmap_spec_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { clearTimeout, setTimeout } from 'timers';
import { promisify } from 'util';

import {
addContainerMetadata,
CMAP_EVENTS,
type Connection,
ConnectionPool,
Expand Down Expand Up @@ -369,6 +370,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
}

const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} });
const extendedMetadata = addContainerMetadata(metadata);
delete poolOptions.appName;

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

threadContext.createPool({ ...poolOptions, metadata, minPoolSizeCheckFrequencyMS });
threadContext.createPool({
...poolOptions,
metadata,
extendedMetadata,
minPoolSizeCheckFrequencyMS
});
// yield control back to the event loop so that the ConnectionPoolCreatedEvent
// has a chance to be fired before any synchronously-emitted events from
// the queued operations
Expand Down
Loading

0 comments on commit 28b7040

Please sign in to comment.