diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts index a0369f9d71..fdf7d626db 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts @@ -16,21 +16,11 @@ import { Detector, - Resource, + IResource, ResourceDetectionConfig, } from '@opentelemetry/resources'; -import { - SEMRESATTRS_CLOUD_PROVIDER, - SEMRESATTRS_CLOUD_PLATFORM, - SEMRESATTRS_K8S_CLUSTER_NAME, - SEMRESATTRS_CONTAINER_ID, - CLOUDPROVIDERVALUES_AWS, - CLOUDPLATFORMVALUES_AWS_EKS, -} from '@opentelemetry/semantic-conventions'; -import * as https from 'https'; -import * as fs from 'fs'; -import * as util from 'util'; -import { diag } from '@opentelemetry/api'; + +import { awsEksDetectorSync } from './AwsEksDetectorSync'; /** * The AwsEksDetector can be used to detect if a process is running in AWS Elastic @@ -39,193 +29,20 @@ import { diag } from '@opentelemetry/api'; * * See https://docs.amazonaws.cn/en_us/xray/latest/devguide/xray-guide.pdf * for more details about detecting information for Elastic Kubernetes plugins + * + * @deprecated Use the new {@link AwsEksDetectorSync} class instead. */ - export class AwsEksDetector implements Detector { + // NOTE: these readonly props are kept for testing purposes readonly K8S_SVC_URL = 'kubernetes.default.svc'; - readonly K8S_TOKEN_PATH = - '/var/run/secrets/kubernetes.io/serviceaccount/token'; - readonly K8S_CERT_PATH = - '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; readonly AUTH_CONFIGMAP_PATH = '/api/v1/namespaces/kube-system/configmaps/aws-auth'; readonly CW_CONFIGMAP_PATH = '/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info'; - readonly CONTAINER_ID_LENGTH = 64; - readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup'; readonly TIMEOUT_MS = 2000; - readonly UTF8_UNICODE = 'utf8'; - - private static readFileAsync = util.promisify(fs.readFile); - private static fileAccessAsync = util.promisify(fs.access); - - /** - * The AwsEksDetector can be used to detect if a process is running on Amazon - * Elastic Kubernetes and returns a promise containing a {@link Resource} - * populated with instance metadata. Returns a promise containing an - * empty {@link Resource} if the connection to kubernetes process - * or aws config maps fails - * @param config The resource detection config - */ - async detect(_config?: ResourceDetectionConfig): Promise { - try { - await AwsEksDetector.fileAccessAsync(this.K8S_TOKEN_PATH); - const k8scert = await AwsEksDetector.readFileAsync(this.K8S_CERT_PATH); - - if (!(await this._isEks(k8scert))) { - return Resource.empty(); - } - - const containerId = await this._getContainerId(); - const clusterName = await this._getClusterName(k8scert); - - return !containerId && !clusterName - ? Resource.empty() - : new Resource({ - [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, - [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EKS, - [SEMRESATTRS_K8S_CLUSTER_NAME]: clusterName || '', - [SEMRESATTRS_CONTAINER_ID]: containerId || '', - }); - } catch (e) { - diag.warn('Process is not running on K8S', e); - return Resource.empty(); - } - } - - /** - * Attempts to make a connection to AWS Config map which will - * determine whether the process is running on an EKS - * process if the config map is empty or not - */ - private async _isEks(cert: Buffer): Promise { - const options = { - ca: cert, - headers: { - Authorization: await this._getK8sCredHeader(), - }, - hostname: this.K8S_SVC_URL, - method: 'GET', - path: this.AUTH_CONFIGMAP_PATH, - timeout: this.TIMEOUT_MS, - }; - return !!(await this._fetchString(options)); - } - - /** - * Attempts to make a connection to Amazon Cloudwatch - * Config Maps to grab cluster name - */ - private async _getClusterName(cert: Buffer): Promise { - const options = { - ca: cert, - headers: { - Authorization: await this._getK8sCredHeader(), - }, - host: this.K8S_SVC_URL, - method: 'GET', - path: this.CW_CONFIGMAP_PATH, - timeout: this.TIMEOUT_MS, - }; - const response = await this._fetchString(options); - try { - return JSON.parse(response).data['cluster.name']; - } catch (e) { - diag.warn('Cannot get cluster name on EKS', e); - } - return ''; - } - /** - * Reads the Kubernetes token path and returns kubernetes - * credential header - */ - private async _getK8sCredHeader(): Promise { - try { - const content = await AwsEksDetector.readFileAsync( - this.K8S_TOKEN_PATH, - this.UTF8_UNICODE - ); - return 'Bearer ' + content; - } catch (e) { - diag.warn('Unable to read Kubernetes client token.', e); - } - return ''; - } - - /** - * Read container ID from cgroup file generated from docker which lists the full - * untruncated docker container ID at the end of each line. - * - * The predefined structure of calling /proc/self/cgroup when in a docker container has the structure: - * - * #:xxxxxx:/ - * - * or - * - * #:xxxxxx:/docker/64characterID - * - * This function takes advantage of that fact by just reading the 64-character ID from the end of the - * first line. In EKS, even if we fail to find target file or target file does - * not contain container ID we do not throw an error but throw warning message - * and then return null string - */ - private async _getContainerId(): Promise { - try { - const rawData = await AwsEksDetector.readFileAsync( - this.DEFAULT_CGROUP_PATH, - this.UTF8_UNICODE - ); - const splitData = rawData.trim().split('\n'); - for (const str of splitData) { - if (str.length > this.CONTAINER_ID_LENGTH) { - return str.substring(str.length - this.CONTAINER_ID_LENGTH); - } - } - } catch (e: any) { - diag.warn(`AwsEksDetector failed to read container ID: ${e.message}`); - } - return undefined; - } - - /** - * Establishes an HTTP connection to AWS instance document url. - * If the application is running on an EKS instance, we should be able - * to get back a valid JSON document. Parses that document and stores - * the identity properties in a local map. - */ - private async _fetchString(options: https.RequestOptions): Promise { - return await new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - req.abort(); - reject(new Error('EKS metadata api request timed out.')); - }, 2000); - const req = https.request(options, res => { - clearTimeout(timeoutId); - const { statusCode } = res; - res.setEncoding(this.UTF8_UNICODE); - let rawData = ''; - res.on('data', chunk => (rawData += chunk)); - res.on('end', () => { - if (statusCode && statusCode >= 200 && statusCode < 300) { - try { - resolve(rawData); - } catch (e) { - reject(e); - } - } else { - reject( - new Error('Failed to load page, status code: ' + statusCode) - ); - } - }); - }); - req.on('error', err => { - clearTimeout(timeoutId); - reject(err); - }); - req.end(); - }); + detect(_config?: ResourceDetectionConfig): Promise { + return Promise.resolve(awsEksDetectorSync.detect()); } } diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts new file mode 100644 index 0000000000..9ac2b4345c --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts @@ -0,0 +1,239 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DetectorSync, + IResource, + Resource, + ResourceAttributes, + ResourceDetectionConfig, +} from '@opentelemetry/resources'; +import { + SEMRESATTRS_CLOUD_PROVIDER, + SEMRESATTRS_CLOUD_PLATFORM, + SEMRESATTRS_K8S_CLUSTER_NAME, + SEMRESATTRS_CONTAINER_ID, + CLOUDPROVIDERVALUES_AWS, + CLOUDPLATFORMVALUES_AWS_EKS, +} from '@opentelemetry/semantic-conventions'; +import * as https from 'https'; +import * as fs from 'fs'; +import * as util from 'util'; +import { diag } from '@opentelemetry/api'; + +/** + * The AwsEksDetectorSync can be used to detect if a process is running in AWS Elastic + * Kubernetes and return a {@link Resource} populated with data about the Kubernetes + * plugins of AWS X-Ray. Returns an empty Resource if detection fails. + * + * See https://docs.amazonaws.cn/en_us/xray/latest/devguide/xray-guide.pdf + * for more details about detecting information for Elastic Kubernetes plugins + */ + +export class AwsEksDetectorSync implements DetectorSync { + readonly K8S_SVC_URL = 'kubernetes.default.svc'; + readonly K8S_TOKEN_PATH = + '/var/run/secrets/kubernetes.io/serviceaccount/token'; + readonly K8S_CERT_PATH = + '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; + readonly AUTH_CONFIGMAP_PATH = + '/api/v1/namespaces/kube-system/configmaps/aws-auth'; + readonly CW_CONFIGMAP_PATH = + '/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info'; + readonly CONTAINER_ID_LENGTH = 64; + readonly DEFAULT_CGROUP_PATH = '/proc/self/cgroup'; + readonly TIMEOUT_MS = 2000; + readonly UTF8_UNICODE = 'utf8'; + + private static readFileAsync = util.promisify(fs.readFile); + private static fileAccessAsync = util.promisify(fs.access); + + detect(_config?: ResourceDetectionConfig): IResource { + return new Resource({}, this._getAttributes()); + } + + /** + * The AwsEksDetector can be used to detect if a process is running on Amazon + * Elastic Kubernetes and returns a promise containing a {@link ResourceAttributes} + * object with instance metadata. Returns a promise containing an + * empty {@link ResourceAttributes} if the connection to kubernetes process + * or aws config maps fails + */ + private async _getAttributes(): Promise { + try { + await AwsEksDetectorSync.fileAccessAsync(this.K8S_TOKEN_PATH); + const k8scert = await AwsEksDetectorSync.readFileAsync( + this.K8S_CERT_PATH + ); + + if (!(await this._isEks(k8scert))) { + return {}; + } + + const containerId = await this._getContainerId(); + const clusterName = await this._getClusterName(k8scert); + + return !containerId && !clusterName + ? {} + : { + [SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_AWS, + [SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_AWS_EKS, + [SEMRESATTRS_K8S_CLUSTER_NAME]: clusterName || '', + [SEMRESATTRS_CONTAINER_ID]: containerId || '', + }; + } catch (e) { + diag.warn('Process is not running on K8S', e); + return {}; + } + } + + /** + * Attempts to make a connection to AWS Config map which will + * determine whether the process is running on an EKS + * process if the config map is empty or not + */ + private async _isEks(cert: Buffer): Promise { + const options = { + ca: cert, + headers: { + Authorization: await this._getK8sCredHeader(), + }, + hostname: this.K8S_SVC_URL, + method: 'GET', + path: this.AUTH_CONFIGMAP_PATH, + timeout: this.TIMEOUT_MS, + }; + return !!(await this._fetchString(options)); + } + + /** + * Attempts to make a connection to Amazon Cloudwatch + * Config Maps to grab cluster name + */ + private async _getClusterName(cert: Buffer): Promise { + const options = { + ca: cert, + headers: { + Authorization: await this._getK8sCredHeader(), + }, + host: this.K8S_SVC_URL, + method: 'GET', + path: this.CW_CONFIGMAP_PATH, + timeout: this.TIMEOUT_MS, + }; + const response = await this._fetchString(options); + try { + return JSON.parse(response).data['cluster.name']; + } catch (e) { + diag.warn('Cannot get cluster name on EKS', e); + } + return ''; + } + /** + * Reads the Kubernetes token path and returns kubernetes + * credential header + */ + private async _getK8sCredHeader(): Promise { + try { + const content = await AwsEksDetectorSync.readFileAsync( + this.K8S_TOKEN_PATH, + this.UTF8_UNICODE + ); + return 'Bearer ' + content; + } catch (e) { + diag.warn('Unable to read Kubernetes client token.', e); + } + return ''; + } + + /** + * Read container ID from cgroup file generated from docker which lists the full + * untruncated docker container ID at the end of each line. + * + * The predefined structure of calling /proc/self/cgroup when in a docker container has the structure: + * + * #:xxxxxx:/ + * + * or + * + * #:xxxxxx:/docker/64characterID + * + * This function takes advantage of that fact by just reading the 64-character ID from the end of the + * first line. In EKS, even if we fail to find target file or target file does + * not contain container ID we do not throw an error but throw warning message + * and then return null string + */ + private async _getContainerId(): Promise { + try { + const rawData = await AwsEksDetectorSync.readFileAsync( + this.DEFAULT_CGROUP_PATH, + this.UTF8_UNICODE + ); + const splitData = rawData.trim().split('\n'); + for (const str of splitData) { + if (str.length > this.CONTAINER_ID_LENGTH) { + return str.substring(str.length - this.CONTAINER_ID_LENGTH); + } + } + } catch (e: any) { + diag.warn(`AwsEksDetector failed to read container ID: ${e.message}`); + } + return undefined; + } + + /** + * Establishes an HTTP connection to AWS instance document url. + * If the application is running on an EKS instance, we should be able + * to get back a valid JSON document. Parses that document and stores + * the identity properties in a local map. + */ + private async _fetchString(options: https.RequestOptions): Promise { + return await new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + req.abort(); + reject(new Error('EKS metadata api request timed out.')); + }, 2000); + + const req = https.request(options, res => { + clearTimeout(timeoutId); + const { statusCode } = res; + res.setEncoding(this.UTF8_UNICODE); + let rawData = ''; + res.on('data', chunk => (rawData += chunk)); + res.on('end', () => { + if (statusCode && statusCode >= 200 && statusCode < 300) { + try { + resolve(rawData); + } catch (e) { + reject(e); + } + } else { + reject( + new Error('Failed to load page, status code: ' + statusCode) + ); + } + }); + }); + req.on('error', err => { + clearTimeout(timeoutId); + reject(err); + }); + req.end(); + }); + } +} + +export const awsEksDetectorSync = new AwsEksDetectorSync(); diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts index b475e4a208..3b6d9cb0ce 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/index.ts @@ -17,5 +17,6 @@ export * from './AwsEc2Detector'; export * from './AwsBeanstalkDetector'; export * from './AwsEcsDetector'; -export * from './AwsEksDetector'; +export { AwsEksDetector, awsEksDetector } from './AwsEksDetector'; +export { AwsEksDetectorSync, awsEksDetectorSync } from './AwsEksDetectorSync'; export * from './AwsLambdaDetector'; diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetector.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetector.test.ts index db9cab4276..ab743dc3b5 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetector.test.ts @@ -18,7 +18,11 @@ import * as nock from 'nock'; import * as sinon from 'sinon'; import * as assert from 'assert'; import { Resource } from '@opentelemetry/resources'; -import { awsEksDetector, AwsEksDetector } from '../../src'; +import { + awsEksDetector, + awsEksDetectorSync, + AwsEksDetectorSync, +} from '../../src'; import { assertK8sResource, assertContainerResource, @@ -54,13 +58,13 @@ describe('awsEksDetector', () => { describe('on successful request', () => { it('should return an aws_eks_instance_resource', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .resolves(correctCgroupData); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -71,7 +75,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -90,14 +95,14 @@ describe('awsEksDetector', () => { it('should return a resource with clusterName attribute without cgroup file', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .onSecondCall() .rejects(errorMsg.fileNotFoundError); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -108,7 +113,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -120,13 +126,13 @@ describe('awsEksDetector', () => { it('should return a resource with container ID attribute without a clusterName', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .resolves(correctCgroupData); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -137,7 +143,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -149,14 +156,14 @@ describe('awsEksDetector', () => { it('should return a resource with clusterName attribute when cgroup file does not contain valid Container ID', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .onSecondCall() .resolves(''); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -167,7 +174,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -180,13 +188,13 @@ describe('awsEksDetector', () => { it('should return an empty resource when not running on Eks', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(''); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .resolves(correctCgroupData); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -194,7 +202,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -207,7 +216,7 @@ describe('awsEksDetector', () => { fileNotFoundError: new Error('cannot file k8s token file'), }; fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .rejects(errorMsg.fileNotFoundError); const resource: Resource = await awsEksDetector.detect(); @@ -218,15 +227,15 @@ describe('awsEksDetector', () => { it('should return an empty resource when containerId and clusterName are invalid', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(''); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .onSecondCall() .rejects(errorMsg.fileNotFoundError); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -237,7 +246,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.isDone(); @@ -249,13 +259,13 @@ describe('awsEksDetector', () => { describe('on unsuccessful request', () => { it('should return an empty resource when timed out', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .resolves(correctCgroupData); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -264,7 +274,9 @@ describe('awsEksDetector', () => { .delayConnection(2500) .reply(200, () => mockedAwsAuth); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); + scope.done(); assert.ok(resource); @@ -273,13 +285,13 @@ describe('awsEksDetector', () => { it('should return an empty resource when receiving error response code', async () => { fileStub = sinon - .stub(AwsEksDetector, 'fileAccessAsync' as any) + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) .resolves(); readStub = sinon - .stub(AwsEksDetector, 'readFileAsync' as any) + .stub(AwsEksDetectorSync, 'readFileAsync' as any) .resolves(correctCgroupData); getCredStub = sinon - .stub(awsEksDetector, '_getK8sCredHeader' as any) + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) .resolves(k8s_token); const scope = nock('https://' + K8S_SVC_URL) .persist() @@ -287,7 +299,9 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(404, () => new Error()); - const resource: Resource = await awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); + scope.done(); assert.ok(resource); diff --git a/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetectorSync.test.ts b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetectorSync.test.ts new file mode 100644 index 0000000000..432e113464 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetectorSync.test.ts @@ -0,0 +1,307 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { Resource } from '@opentelemetry/resources'; +import { awsEksDetectorSync, AwsEksDetectorSync } from '../../src'; +import { + assertK8sResource, + assertContainerResource, + assertEmptyResource, +} from '@opentelemetry/contrib-test-utils'; + +const K8S_SVC_URL = awsEksDetectorSync.K8S_SVC_URL; +const AUTH_CONFIGMAP_PATH = awsEksDetectorSync.AUTH_CONFIGMAP_PATH; +const CW_CONFIGMAP_PATH = awsEksDetectorSync.CW_CONFIGMAP_PATH; + +describe('awsEksDetectorSync', () => { + const errorMsg = { + fileNotFoundError: new Error('cannot find cgroup file'), + }; + + const correctCgroupData = + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'; + const mockedClusterResponse = '{"data":{"cluster.name":"my-cluster"}}'; + const mockedAwsAuth = 'my-auth'; + const k8s_token = 'Bearer 31ada4fd-adec-460c-809a-9e56ceb75269'; + let readStub, fileStub, getCredStub; + + beforeEach(() => { + nock.disableNetConnect(); + nock.cleanAll(); + }); + + afterEach(() => { + sinon.restore(); + nock.enableNetConnect(); + }); + + describe('on successful request', () => { + it('should return an aws_eks_instance_resource', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .resolves(correctCgroupData); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedAwsAuth) + .get(CW_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedClusterResponse); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + sinon.assert.calledOnce(fileStub); + sinon.assert.calledTwice(readStub); + sinon.assert.calledTwice(getCredStub); + + assert.ok(resource); + assertK8sResource(resource, { + clusterName: 'my-cluster', + }); + assertContainerResource(resource, { + id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm', + }); + }); + + it('should return a resource with clusterName attribute without cgroup file', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .onSecondCall() + .rejects(errorMsg.fileNotFoundError); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedAwsAuth) + .get(CW_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedClusterResponse); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assertK8sResource(resource, { + clusterName: 'my-cluster', + }); + }); + + it('should return a resource with container ID attribute without a clusterName', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .resolves(correctCgroupData); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedAwsAuth) + .get(CW_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => ''); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assertContainerResource(resource, { + id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm', + }); + }); + + it('should return a resource with clusterName attribute when cgroup file does not contain valid Container ID', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .onSecondCall() + .resolves(''); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedAwsAuth) + .get(CW_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedClusterResponse); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assert.ok(resource); + assertK8sResource(resource, { + clusterName: 'my-cluster', + }); + }); + + it('should return an empty resource when not running on Eks', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(''); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .resolves(correctCgroupData); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => ''); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assertEmptyResource(resource); + }); + + it('should return an empty resource when k8s token file does not exist', async () => { + const errorMsg = { + fileNotFoundError: new Error('cannot file k8s token file'), + }; + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .rejects(errorMsg.fileNotFoundError); + + const resource: Resource = await awsEksDetectorSync.detect(); + + assert.ok(resource); + assertEmptyResource(resource); + }); + + it('should return an empty resource when containerId and clusterName are invalid', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(''); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .onSecondCall() + .rejects(errorMsg.fileNotFoundError); + + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => mockedAwsAuth) + .get(CW_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(200, () => ''); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.isDone(); + + assert.ok(resource); + assertEmptyResource(resource); + }); + }); + + describe('on unsuccessful request', () => { + it('should return an empty resource when timed out', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .resolves(correctCgroupData); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .delayConnection(2500) + .reply(200, () => mockedAwsAuth); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assertEmptyResource(resource); + }).timeout(awsEksDetectorSync.TIMEOUT_MS + 100); + + it('should return an empty resource when receiving error response code', async () => { + fileStub = sinon + .stub(AwsEksDetectorSync, 'fileAccessAsync' as any) + .resolves(); + readStub = sinon + .stub(AwsEksDetectorSync, 'readFileAsync' as any) + .resolves(correctCgroupData); + getCredStub = sinon + .stub(awsEksDetectorSync, '_getK8sCredHeader' as any) + .resolves(k8s_token); + const scope = nock('https://' + K8S_SVC_URL) + .persist() + .get(AUTH_CONFIGMAP_PATH) + .matchHeader('Authorization', k8s_token) + .reply(404, () => new Error()); + + const resource = awsEksDetectorSync.detect(); + await resource.waitForAsyncAttributes?.(); + + scope.done(); + + assert.ok(resource); + assertEmptyResource(resource); + }); + }); +}); diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index b0417115a5..3eb8e1bfe9 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -237,7 +237,11 @@ function getDisabledInstrumentationsFromEnv() { export function getResourceDetectorsFromEnv(): Array { const resourceDetectors = new Map< string, - Detector | DetectorSync | Detector[] | DetectorSync[] + | Detector + | DetectorSync + | Detector[] + | DetectorSync[] + | Array >([ [RESOURCE_DETECTOR_CONTAINER, containerDetector], [RESOURCE_DETECTOR_ENVIRONMENT, envDetectorSync],