Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added Amazon EKS Resource Detector #1669

Merged
merged 26 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7fa1cf3
feat: add EKS Resource detector
KKelvinLo Oct 29, 2020
d3ad41c
test: add mock tests
KKelvinLo Oct 29, 2020
47ed09c
fix: use file read async instead of sync
KKelvinLo Oct 30, 2020
2f4aeb2
feat: update implementation of https requests
KKelvinLo Oct 30, 2020
e19a781
test: added more concrete unit tests and comments
KKelvinLo Nov 6, 2020
9661a93
fix: update naming conventions consistentcy
KKelvinLo Nov 10, 2020
234872e
fix: updated naming consistency
KKelvinLo Nov 10, 2020
f408023
fix: updated files to adhere to linter
KKelvinLo Nov 11, 2020
6c6c51e
fix: updated files to adhere to linter
KKelvinLo Nov 11, 2020
9b5cb67
fix: add parsing to Cluster JSON response
KKelvinLo Nov 26, 2020
10b89d0
chore: update naming consistency
KKelvinLo Dec 3, 2020
76a5f84
test: add mock tests
KKelvinLo Oct 29, 2020
3a8155d
fix: use file read async instead of sync
KKelvinLo Oct 30, 2020
fecc96d
feat: update implementation of https requests
KKelvinLo Oct 30, 2020
322235a
fix: update naming conventions consistentcy
KKelvinLo Nov 10, 2020
9ff4f17
fix: updated naming consistency
KKelvinLo Nov 10, 2020
1eb4066
fix: updated files to adhere to linter
KKelvinLo Nov 11, 2020
43a8820
fix: updated files to adhere to linter
KKelvinLo Nov 11, 2020
20d7ea4
fix: add parsing to Cluster JSON response
KKelvinLo Nov 26, 2020
c3dd72b
chore: rebase master
KKelvinLo Dec 3, 2020
ecabb30
chore: update casing consistency
KKelvinLo Dec 3, 2020
05470de
chore: add spacing to end of file
KKelvinLo Dec 3, 2020
111b402
chore: adjust naming consistency
KKelvinLo Dec 3, 2020
557d68a
chore: add description to getContainerID
KKelvinLo Dec 3, 2020
ba27742
chore: fix linting error
KKelvinLo Dec 3, 2020
9fcfb96
chore: add space
KKelvinLo Dec 3, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* 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 {
Detector,
Resource,
CONTAINER_RESOURCE,
K8S_RESOURCE,
ResourceDetectionConfigWithLogger,
} from '@opentelemetry/resources';
import * as https from 'https';
import * as fs from 'fs';
import * as util from 'util';

/**
* The AwsEksDetector 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 AwsEksDetector implements Detector {
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 with a required logger
*/
async detect(config: ResourceDetectionConfigWithLogger): Promise<Resource> {
try {
await AwsEksDetector.fileAccessAsync(this.K8S_TOKEN_PATH);
const k8scert = await AwsEksDetector.readFileAsync(this.K8S_CERT_PATH);

if (!this._isEks(config, k8scert)) {
return Resource.empty();
}

const containerId = await this._getContainerId(config);
const clusterName = await this._getClusterName(config, k8scert);

return !containerId && !clusterName
? Resource.empty()
: new Resource({
[K8S_RESOURCE.CLUSTER_NAME]: clusterName || '',
[CONTAINER_RESOURCE.ID]: containerId || '',
});
} catch (e) {
config.logger.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
* @param config The resource detection config with a required logger
*/
private async _isEks(
config: ResourceDetectionConfigWithLogger,
cert: Buffer
): Promise<boolean> {
const options = {
ca: cert,
headers: {
Authorization: await this._getK8sCredHeader(config),
},
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
* @param config The resource detection config with a required logger
*/
private async _getClusterName(
config: ResourceDetectionConfigWithLogger,
cert: Buffer
): Promise<string | undefined> {
const options = {
KKelvinLo marked this conversation as resolved.
Show resolved Hide resolved
ca: cert,
headers: {
Authorization: await this._getK8sCredHeader(config),
},
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) {
config.logger.warn('Cannot get cluster name on EKS', e);
}
return '';
}
/**
* Reads the Kubernetes token path and returns kubernetes
* credential header
* @param config The resource detection config with a required logger
*/
private async _getK8sCredHeader(
config: ResourceDetectionConfigWithLogger
): Promise<string> {
try {
const content = await AwsEksDetector.readFileAsync(
this.K8S_TOKEN_PATH,
this.UTF8_UNICODE
);
return 'Bearer ' + content;
} catch (e) {
config.logger.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(
config: ResourceDetectionConfigWithLogger
): Promise<string | undefined> {
try {
const rawData = await AwsEksDetector.readFileAsync(
this.DEFAULT_CGROUP_PATH,
this.UTF8_UNICODE
);
const splitData = rawData.trim().split('\n');
for (const str of splitData) {
KKelvinLo marked this conversation as resolved.
Show resolved Hide resolved
if (str.length > this.CONTAINER_ID_LENGTH) {
return str.substring(str.length - this.CONTAINER_ID_LENGTH);
}
}
} catch (e) {
config.logger.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<string> {
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 => {
KKelvinLo marked this conversation as resolved.
Show resolved Hide resolved
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 awsEksDetector = new AwsEksDetector();
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
export * from './AwsEc2Detector';
export * from './AwsBeanstalkDetector';
export * from './AwsEcsDetector';
export * from './AwsEksDetector';
Loading