Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 4 additions & 3 deletions src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
type ConnectionOptions,
CryptoConnection
} from './connection';
import type { ClientMetadata } from './handshake/client_metadata';
import { addContainerMetadata } from './handshake/client_metadata';
import {
MAX_SUPPORTED_SERVER_VERSION,
MAX_SUPPORTED_WIRE_VERSION,
Expand Down Expand Up @@ -183,7 +183,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 +200,12 @@ export async function prepareHandshakeDocument(
const options = authContext.options;
const compressors = options.compressors ? options.compressors : [];
const { serverApi } = authContext.connection;
const clientMetadata = await addContainerMetadata(options.metadata);

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

Expand Down
63 changes: 59 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,62 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
}
}
}
return metadataDocument.toObject() as ClientMetadata;
}

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

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
103 changes: 97 additions & 6 deletions test/unit/cmap/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,100 @@ describe('Connect Tests', function () {
expect(error).to.be.instanceOf(MongoNetworkError);
});

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

context('when only kubernetes is present', () => {
const authContext = {
connection: {},
options: { ...CONNECT_DEFAULTS }
};

beforeEach(() => {
process.env.KUBERNETES_SERVICE_HOST = 'I exist';
});

afterEach(() => {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
});

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 kubernetes and FAAS are both present', () => {
const authContext = {
connection: {},
options: { ...CONNECT_DEFAULTS, metadata: { env: { name: 'aws.lambda' } } }
};

beforeEach(() => {
process.env.KUBERNETES_SERVICE_HOST = 'I exist';
});

afterEach(() => {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.KUBERNETES_SERVICE_HOST;
});

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 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(() => {
process.env.KUBERNETES_SERVICE_HOST = '';
});

afterEach(() => {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.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(() => {
process.env.KUBERNETES_SERVICE_HOST = cachedEnv.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 = {};
const options = { ...CONNECT_DEFAULTS };
const authContext = {
connection: { serverApi: { version: '1' } },
options
Expand All @@ -200,7 +291,7 @@ describe('Connect Tests', function () {
});

context('when serverApi is not present', () => {
const options = {};
const options = { ...CONNECT_DEFAULTS };
const authContext = {
connection: {},
options
Expand All @@ -216,7 +307,7 @@ describe('Connect Tests', function () {
context('when loadBalanced is not set as an option', () => {
const authContext = {
connection: {},
options: {}
options: { ...CONNECT_DEFAULTS }
};

it('does not set loadBalanced on the handshake document', async () => {
Expand All @@ -238,7 +329,7 @@ describe('Connect Tests', function () {
context('when loadBalanced is set to false', () => {
const authContext = {
connection: {},
options: { loadBalanced: false }
options: { ...CONNECT_DEFAULTS, loadBalanced: false }
};

it('does not set loadBalanced on the handshake document', async () => {
Expand All @@ -260,7 +351,7 @@ describe('Connect Tests', function () {
context('when loadBalanced is set to true', () => {
const authContext = {
connection: {},
options: { loadBalanced: true }
options: { ...CONNECT_DEFAULTS, loadBalanced: true }
};

it('sets loadBalanced on the handshake document', async () => {
Expand Down