Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
159 changes: 158 additions & 1 deletion test/unit/cmap/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { promisify } from 'util';

import {
addContainerMetadata,
CancellationToken,
type ClientMetadata,
connect,
Expand All @@ -24,6 +25,7 @@ const CONNECT_DEFAULTS = {
generation: 1,
monitorCommands: false,
metadata: {} as ClientMetadata,
extendedMetadata: addContainerMetadata({} as ClientMetadata),
loadBalanced: false
};

Expand Down Expand Up @@ -207,7 +209,162 @@ describe('Connect Tests', function () {
});
});

context('prepareHandshakeDocument', () => {
describe('prepareHandshakeDocument', () => {
describe('client environment (containers and FAAS)', () => {
const cachedEnv = process.env;

context('when only kubernetes is present', () => {
let authContext;

beforeEach(() => {
process.env.KUBERNETES_SERVICE_HOST = 'I exist';
authContext = {
connection: {},
options: {
...CONNECT_DEFAULTS,
extendedMetadata: addContainerMetadata({} as ClientMetadata)
}
};
});

afterEach(() => {
if (cachedEnv.KUBERNETES_SERVICE_HOST != null) {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
} else {
delete process.env.KUBERNETES_SERVICE_HOST;
}
authContext = {};
});

it(`should include { orchestrator: 'kubernetes'} in client.env.container`, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client.env.container.orchestrator).to.equal('kubernetes');
});

it(`should not have 'name' property in client.env `, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client.env).to.not.have.property('name');
});

context('when 512 byte size limit is exceeded', async () => {
it(`should not 'env' property in client`, async () => {
// make metadata = 507 bytes, so it takes up entire LimitedSizeDocument
const longAppName = 's'.repeat(493);
const longAuthContext = {
connection: {},
options: {
...CONNECT_DEFAULTS,
extendedMetadata: addContainerMetadata({ appName: longAppName })
}
};
const handshakeDocument = await prepareHandshakeDocument(longAuthContext);
expect(handshakeDocument.client).to.not.have.property('env');
});
});
});

context('when kubernetes and FAAS are both present', () => {
let authContext;

beforeEach(() => {
process.env.KUBERNETES_SERVICE_HOST = 'I exist';
authContext = {
connection: {},
options: {
...CONNECT_DEFAULTS,
extendedMetadata: addContainerMetadata({ env: { name: 'aws.lambda' } })
}
};
});

afterEach(() => {
if (cachedEnv.KUBERNETES_SERVICE_HOST != null) {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
} else {
delete process.env.KUBERNETES_SERVICE_HOST;
}
authContext = {};
});

it(`should include { orchestrator: 'kubernetes'} in client.env.container`, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client.env.container.orchestrator).to.equal('kubernetes');
});

it(`should still have properly set 'name' property in client.env `, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client.env.name).to.equal('aws.lambda');
});

context('when 512 byte size limit is exceeded', async () => {
it(`should not have 'container' property in client.env`, async () => {
// make metadata = 507 bytes, so it takes up entire LimitedSizeDocument
const longAppName = 's'.repeat(447);
const longAuthContext = {
connection: {},
options: {
...CONNECT_DEFAULTS,
extendedMetadata: {
appName: longAppName,
env: { name: 'aws.lambda' }
} as unknown as Promise<Document>
}
};
const handshakeDocument = await prepareHandshakeDocument(longAuthContext);
expect(handshakeDocument.client.env.name).to.equal('aws.lambda');
expect(handshakeDocument.client.env).to.not.have.property('container');
});
});
});

context('when container nor FAAS env is not present (empty string case)', () => {
const authContext = {
connection: {},
options: { ...CONNECT_DEFAULTS }
};

context('when process.env.KUBERNETES_SERVICE_HOST = undefined', () => {
beforeEach(() => {
delete process.env.KUBERNETES_SERVICE_HOST;
});

afterEach(() => {
afterEach(() => {
if (cachedEnv.KUBERNETES_SERVICE_HOST != null) {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
} else {
delete process.env.KUBERNETES_SERVICE_HOST;
}
});
});

it(`should not have 'env' property in client`, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client).to.not.have.property('env');
});
});

context('when process.env.KUBERNETES_SERVICE_HOST is an empty string', () => {
beforeEach(() => {
process.env.KUBERNETES_SERVICE_HOST = '';
});

afterEach(() => {
if (cachedEnv.KUBERNETES_SERVICE_HOST != null) {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
} else {
delete process.env.KUBERNETES_SERVICE_HOST;
}
});

it(`should not have 'env' property in client`, async () => {
const handshakeDocument = await prepareHandshakeDocument(authContext);
expect(handshakeDocument.client).to.not.have.property('env');
});
});
});
});

context('when serverApi.version is present', () => {
const options = {
authProviders: new MongoClientAuthProviders()
Expand Down
Loading