From f1005f225862bb4ecda85e08655bd886dfb728bf Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 12 Jul 2024 13:40:04 +0200 Subject: [PATCH 1/3] refactor(detector-aws): change implementation to DetectorSync interface for EKS detector --- .../src/detectors/AwsEksDetector.ts | 29 +++++++++++-------- .../test/detectors/AwsEksDetector.test.ts | 26 ++++++++++++----- .../auto-instrumentations-node/src/utils.ts | 6 +++- 3 files changed, 40 insertions(+), 21 deletions(-) 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..fc0437ad04 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts @@ -15,8 +15,10 @@ */ import { - Detector, + DetectorSync, + IResource, Resource, + ResourceAttributes, ResourceDetectionConfig, } from '@opentelemetry/resources'; import { @@ -41,7 +43,7 @@ import { diag } from '@opentelemetry/api'; * for more details about detecting information for Elastic Kubernetes plugins */ -export class AwsEksDetector implements Detector { +export class AwsEksDetector implements DetectorSync { readonly K8S_SVC_URL = 'kubernetes.default.svc'; readonly K8S_TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'; @@ -59,37 +61,40 @@ export class AwsEksDetector implements Detector { 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 Resource} - * populated with instance metadata. Returns a promise containing an - * empty {@link Resource} if the connection to kubernetes process + * 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 - * @param config The resource detection config */ - async detect(_config?: ResourceDetectionConfig): Promise { + private async _getAttributes(): 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(); + return {}; } 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(); + return {}; } } 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..22dd4d795e 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 @@ -71,7 +71,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -108,7 +109,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -137,7 +139,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -167,7 +170,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -194,7 +198,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.done(); @@ -237,7 +242,8 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); scope.isDone(); @@ -264,7 +270,9 @@ describe('awsEksDetector', () => { .delayConnection(2500) .reply(200, () => mockedAwsAuth); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); + scope.done(); assert.ok(resource); @@ -287,7 +295,9 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(404, () => new Error()); - const resource: Resource = await awsEksDetector.detect(); + const resource = awsEksDetector.detect(); + await resource.waitForAsyncAttributes?.(); + scope.done(); assert.ok(resource); diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index c258dfc597..481dbb68ec 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -235,7 +235,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], From 8ca9707b40f3cfd9d86d005e90ab5f8a4f32c402 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 1 Aug 2024 14:05:38 +0200 Subject: [PATCH 2/3] refactor(detector-aws): add EKS detector implementing DetectorSync interface --- .../src/detectors/AwsEksDetector.ts | 206 +----------- .../src/detectors/AwsEksDetectorSync.ts | 237 ++++++++++++++ .../src/detectors/index.ts | 3 +- .../test/detectors/AwsEksDetector.test.ts | 68 ++-- .../test/detectors/AwsEksDetectorSync.test.ts | 307 ++++++++++++++++++ 5 files changed, 589 insertions(+), 232 deletions(-) create mode 100644 detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts create mode 100644 detectors/node/opentelemetry-resource-detector-aws/test/detectors/AwsEksDetectorSync.test.ts 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 fc0437ad04..2d67b13c0e 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts @@ -15,24 +15,12 @@ */ import { - DetectorSync, + Detector, 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'; + +import { awsEksDetectorSync } from './AwsEksDetectorSync'; /** * The AwsEksDetector can be used to detect if a process is running in AWS Elastic @@ -41,196 +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 DetectorSync { +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); - - 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 AwsEksDetector.fileAccessAsync(this.K8S_TOKEN_PATH); - const k8scert = await AwsEksDetector.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 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..b0493cfd59 --- /dev/null +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts @@ -0,0 +1,237 @@ +/* + * 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 22dd4d795e..8e6833a6fb 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,7 @@ 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 +54,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 +71,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -91,14 +91,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() @@ -109,7 +109,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -122,13 +122,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() @@ -139,7 +139,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -152,14 +152,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() @@ -170,7 +170,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => mockedClusterResponse); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -184,13 +184,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() @@ -198,7 +198,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -212,7 +212,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(); @@ -223,15 +223,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() @@ -242,7 +242,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(200, () => ''); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.isDone(); @@ -255,13 +255,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() @@ -270,7 +270,7 @@ describe('awsEksDetector', () => { .delayConnection(2500) .reply(200, () => mockedAwsAuth); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); @@ -281,13 +281,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() @@ -295,7 +295,7 @@ describe('awsEksDetector', () => { .matchHeader('Authorization', k8s_token) .reply(404, () => new Error()); - const resource = awsEksDetector.detect(); + const resource = await awsEksDetector.detect(); await resource.waitForAsyncAttributes?.(); scope.done(); 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); + }); + }); +}); From acf865063381be64ab636d2c77dc3acc2e6681fd Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 1 Aug 2024 14:19:52 +0200 Subject: [PATCH 3/3] chore(detector-aws): fix lint issues --- .../src/detectors/AwsEksDetector.ts | 2 +- .../src/detectors/AwsEksDetectorSync.ts | 4 +++- .../test/detectors/AwsEksDetector.test.ts | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) 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 2d67b13c0e..fdf7d626db 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetector.ts @@ -29,7 +29,7 @@ import { awsEksDetectorSync } from './AwsEksDetectorSync'; * * 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 { diff --git a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts index b0493cfd59..9ac2b4345c 100644 --- a/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts +++ b/detectors/node/opentelemetry-resource-detector-aws/src/detectors/AwsEksDetectorSync.ts @@ -75,7 +75,9 @@ export class AwsEksDetectorSync implements DetectorSync { private async _getAttributes(): Promise { try { await AwsEksDetectorSync.fileAccessAsync(this.K8S_TOKEN_PATH); - const k8scert = await AwsEksDetectorSync.readFileAsync(this.K8S_CERT_PATH); + const k8scert = await AwsEksDetectorSync.readFileAsync( + this.K8S_CERT_PATH + ); if (!(await this._isEks(k8scert))) { return {}; 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 8e6833a6fb..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, awsEksDetectorSync, AwsEksDetectorSync } from '../../src'; +import { + awsEksDetector, + awsEksDetectorSync, + AwsEksDetectorSync, +} from '../../src'; import { assertK8sResource, assertContainerResource,