Skip to content

Commit

Permalink
feat(NODE-5967): 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 8, 2024
1 parent 5732b52 commit 55c290a
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 13 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 @@ -180,7 +179,7 @@ export interface HandshakeDocument extends Document {
ismaster?: boolean;
hello?: boolean;
helloOk?: boolean;
client: ClientMetadata;
client: Document;
compression: string[];
saslSupportedMechs?: string;
loadBalanced?: boolean;
Expand All @@ -197,11 +196,12 @@ export async function prepareHandshakeDocument(
const options = authContext.options;
const compressors = options.compressors ? options.compressors : [];
const { serverApi } = authContext.connection;
const clientMetadata = await options.extendedMetadata;

const handshakeDoc: HandshakeDocument = {
[serverApi?.version ? '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 @@ -133,6 +133,8 @@ export interface ConnectionOptions
socketTimeoutMS?: number;
cancellationToken?: CancellationToken;
metadata: ClientMetadata;
/** @internal */
extendedMetadata: Promise<Document>;
}

/** @internal */
Expand Down
59 changes: 55 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 @@ -153,7 +154,57 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
}
}

return metadataDocument.toObject();
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 extendedMetadata.toObject();
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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 @@ -550,6 +550,9 @@ 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 @@ -757,6 +757,8 @@ export interface MongoOptions
writeConcern: WriteConcern;
dbName: string;
metadata: ClientMetadata;
/** @internal */
extendedMetadata: Promise<Document>;
/**
* @deprecated This option will be removed in the next major version.
*/
Expand Down
1 change: 1 addition & 0 deletions src/sdam/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
directConnection: boolean;
loadBalanced: boolean;
metadata: ClientMetadata;
extendedMetadata: Promise<Document>;
/** MongoDB server API version */
serverApi?: ServerApi;
[featureFlag: symbol]: any;
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 @@ -36,7 +37,8 @@ describe('Connection', function () {
const connectOptions: Partial<ConnectionOptions> = {
connectionType: Connection,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
metadata: makeClientMetadata({ driverInfo: {} }),
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
};

connect(connectOptions as any as ConnectionOptions, (err, conn) => {
Expand All @@ -60,7 +62,8 @@ describe('Connection', function () {
connectionType: Connection,
monitorCommands: true,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
metadata: makeClientMetadata({ driverInfo: {} }),
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
};

connect(connectOptions as any as ConnectionOptions, (err, conn) => {
Expand Down Expand Up @@ -92,7 +95,8 @@ describe('Connection', function () {
const connectOptions: Partial<ConnectionOptions> = {
connectionType: Connection,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
metadata: makeClientMetadata({ driverInfo: {} }),
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
};

connect(connectOptions as any as ConnectionOptions, (err, 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 @@ -371,6 +372,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 @@ -382,7 +384,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 55c290a

Please sign in to comment.