-
Notifications
You must be signed in to change notification settings - Fork 227
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implements: #1815 * feat: cloud metadata request * chore: move test file * feat: test complete * feat: working through test failures * fix: tests * chore: re-org * feat: reorg metadata * chore: undo test removal * chore: remove console.log * feat: request module * chore: moving on up * chore: instrumentation path * chore: bringing over test server fixtures from other branch * chore: lint cleanup post-elrequest * fix: failure handlers for request * chore: lint * feat: add cli interface for quick testing on cloud servers * chore: exit codes for cli * feat: fetch metadata for all, corrdinate messages * feat: error handling for non-json responses * feat: adding agent configuration of cloud provider and tests for same * chore: module constants for timeouts * ci: new test server endpoint for v2 amazon * feat: add v2 server tests and implementation * fix: error handling * chore: lint * test: full testing of unexpected responses from metadata server * chore: lint * test: tests for coordination object * chore: rename to CallbackCoordination * chore: rename library file * fix: cleanup timeout in coordination * chore: lint * fix: better pattern for no double callbacks on errors * feat: logging, comment proofread * feat: APM Server wants strings for everything, add type casting/checking * chore: fix port * chore: lint * chore: changelog * chore: adjust log level * chore: add link to issue * chore: nits -- variable name and error type export * chore: lint, remove unused data * fix: better pattern for request callback * chore: variable rename * chore: change from passing entire agent to just passing provider name * chore: use logger arg instead of global agent instance * chore: for loop style * chore: cast with string objects instead of concats * chore: not new String, String * chore: lint * chore: logger into other tests * chore: more getLogger calls * chore: lint * feat: unified IMDSv1 and IMDSv2 method * feat: unify aws APIs * fix: avoid catching the callback's exceptions * fix: removes instances of "callback function in a try" anti-pattern * feat: refactor to use URLs, move constants into modules * fix: 32 is not x32 * chore: lint * fix: node 8 and URL * feat: config normalization and testing * feat: improve API of getMetadata... methods, baseApiOverride param * fix: log a "shouldn't happen" situation * chore: update comments * chore: err vs. error * chore: no-op logger * chore: one last logger * chore: lint * chore: module path
- Loading branch information
Showing
14 changed files
with
2,141 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
'use strict' | ||
const URL = require('url').URL | ||
const { httpRequest } = require('../http-request') | ||
const DEFAULT_BASE_URL = new URL('/', 'http://169.254.169.254:80') | ||
|
||
function sanitizeHttpHeaderValue (value) { | ||
// no newlines, carriage returns, or ascii characters outside of 32 (\x20) - 127 (\x7F) | ||
const newValue = value.replace(/[\r\n]/g, '').replace(/[^\x20-\x7F]/g, '') | ||
return newValue | ||
} | ||
|
||
/** | ||
* Logic for making request to /latest/dynamic/instance-identity/document | ||
* | ||
* The headers parameter allow us to, if needed, set the IMDSv2 token | ||
*/ | ||
function getMetadataAwsWithHeaders (baseUrl, headers, connectTimeoutMs, resTimeoutMs, logger, cb) { | ||
const options = { | ||
method: 'GET', | ||
timeout: resTimeoutMs, | ||
connectTimeout: connectTimeoutMs | ||
} | ||
if (headers) { | ||
options.headers = headers | ||
} | ||
const url = baseUrl + 'latest/dynamic/instance-identity/document' | ||
const req = httpRequest( | ||
url, | ||
options, | ||
function (res) { | ||
const finalData = [] | ||
res.on('data', function (data) { | ||
finalData.push(data) | ||
}) | ||
|
||
res.on('end', function (data) { | ||
let result | ||
try { | ||
result = formatMetadataStringIntoObject(finalData.join('')) | ||
} catch (err) { | ||
logger.trace('aws metadata server responded, but there was an ' + | ||
'error parsing the result: %o', err) | ||
cb(err) | ||
return | ||
} | ||
cb(null, result) | ||
}) | ||
} | ||
) | ||
|
||
req.on('timeout', function () { | ||
req.destroy(new Error('request to metadata server timed out')) | ||
}) | ||
|
||
req.on('connectTimeout', function () { | ||
req.destroy(new Error('could not ping metadata server')) | ||
}) | ||
|
||
req.on('error', function (err) { | ||
cb(err) | ||
}) | ||
|
||
req.end() | ||
} | ||
|
||
/** | ||
* Fetches metadata from either an IMDSv2 or IMDSv1 endpoint | ||
* | ||
* Attempts to fetch a token for an IMDSv2 service call. If this call | ||
* is a success, then we call the instance endpoint with the token. If | ||
* this call fails, then we call the instance endpoint without the token. | ||
* | ||
* A "success" to the token endpoint means an HTTP status code of 200 | ||
* and a non-zero-length return value for the token. | ||
* | ||
* https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html | ||
*/ | ||
function getMetadataAws (connectTimeoutMs, resTimeoutMs, logger, baseUrlOverride, cb) { | ||
const baseUrl = baseUrlOverride || DEFAULT_BASE_URL | ||
const url = baseUrl + 'latest/api/token' | ||
const options = { | ||
method: 'PUT', | ||
headers: { | ||
'X-aws-ec2-metadata-token-ttl-seconds': 300 | ||
}, | ||
timeout: resTimeoutMs, | ||
connectTimeout: connectTimeoutMs | ||
} | ||
const req = httpRequest( | ||
url, | ||
options, | ||
function (res) { | ||
const finalData = [] | ||
res.on('data', function (data) { | ||
finalData.push(data) | ||
}) | ||
|
||
res.on('end', function () { | ||
const token = sanitizeHttpHeaderValue(finalData.join('')) | ||
const headers = {} | ||
if (token && res.statusCode === 200) { | ||
// uses return value from call to latest/api/token as token, | ||
// and takes extra steps to ensure characters are valid | ||
headers['X-aws-ec2-metadata-token'] = token | ||
} | ||
|
||
getMetadataAwsWithHeaders( | ||
baseUrl, | ||
headers, | ||
connectTimeoutMs, | ||
resTimeoutMs, | ||
logger, | ||
cb | ||
) | ||
}) | ||
} | ||
) | ||
req.on('timeout', function () { | ||
req.destroy(new Error('request for metadata token timed out')) | ||
}) | ||
|
||
req.on('connectTimeout', function () { | ||
req.destroy(new Error('socket connection to metadata token server timed out')) | ||
}) | ||
|
||
req.on('error', function (err) { | ||
cb(err) | ||
}) | ||
|
||
req.end() | ||
} | ||
|
||
/** | ||
* Builds metadata object | ||
* | ||
* Takes the response from a /latest/dynamic/instance-identity/document | ||
* service request and formats it into the cloud metadata object | ||
*/ | ||
function formatMetadataStringIntoObject (string) { | ||
const data = JSON.parse(string) | ||
const metadata = { | ||
account: { | ||
id: String(data.accountId) | ||
}, | ||
instance: { | ||
id: String(data.instanceId) | ||
}, | ||
availability_zone: String(data.availabilityZone), | ||
machine: { | ||
type: String(data.instanceType) | ||
}, | ||
provider: 'aws', | ||
region: String(data.region) | ||
} | ||
|
||
return metadata | ||
} | ||
|
||
module.exports = { getMetadataAws } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
'use strict' | ||
const URL = require('url').URL | ||
const { httpRequest } = require('../http-request') | ||
|
||
const DEFAULT_BASE_URL = new URL('/', 'http://169.254.169.254:80') | ||
|
||
/** | ||
* Checks for metadata server then fetches data | ||
* | ||
* The getMetadataAzure method will fetch cloud metadata information | ||
* from Amazon's IMDSv1 endpoint and return (via callback) | ||
* the formatted metadata. | ||
* | ||
* Before fetching data, the server will be "pinged" by attempting | ||
* to connect via TCP with a short timeout (`connectTimeoutMs`). | ||
* | ||
* https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=windows | ||
*/ | ||
function getMetadataAzure (connectTimeoutMs, resTimeoutMs, logger, baseUrlOverride, cb) { | ||
const baseUrl = baseUrlOverride || DEFAULT_BASE_URL | ||
const options = { | ||
method: 'GET', | ||
timeout: resTimeoutMs, | ||
connectTimeout: connectTimeoutMs, | ||
headers: { | ||
Metadata: 'true' | ||
} | ||
} | ||
|
||
const url = baseUrl.toString() + 'metadata/instance?api-version=2020-09-01' | ||
|
||
const req = httpRequest( | ||
url, | ||
options, | ||
function (res) { | ||
const finalData = [] | ||
res.on('data', function (data) { | ||
finalData.push(data) | ||
}) | ||
|
||
res.on('end', function (data) { | ||
let result | ||
try { | ||
result = formatMetadataStringIntoObject(finalData.join('')) | ||
} catch (err) { | ||
logger.trace('azure metadata server responded, but there was an ' + | ||
'error parsing the result: %o', err) | ||
cb(err) | ||
return | ||
} | ||
cb(null, result) | ||
}) | ||
} | ||
) | ||
|
||
req.on('timeout', function () { | ||
req.destroy(new Error('request to azure metadata server timed out')) | ||
}) | ||
|
||
req.on('connectTimeout', function () { | ||
req.destroy(new Error('could not ping azure metadata server')) | ||
}) | ||
|
||
req.on('error', function (err) { | ||
cb(err) | ||
}) | ||
|
||
req.end() | ||
} | ||
|
||
/** | ||
* Builds metadata object | ||
* | ||
* Takes the response from /metadata/instance?api-version=2020-09-01 | ||
* service request and formats it into the cloud metadata object | ||
*/ | ||
function formatMetadataStringIntoObject (string) { | ||
const metadata = { | ||
account: { | ||
id: null | ||
}, | ||
instance: { | ||
id: null, | ||
name: null | ||
}, | ||
project: { | ||
name: null | ||
}, | ||
availability_zone: null, | ||
machine: { | ||
type: null | ||
}, | ||
provider: 'azure', | ||
region: null | ||
} | ||
const parsed = JSON.parse(string) | ||
if (!parsed.compute) { | ||
return metadata | ||
} | ||
const data = parsed.compute | ||
metadata.account.id = String(data.subscriptionId) | ||
metadata.instance.id = String(data.vmId) | ||
metadata.instance.name = String(data.name) | ||
metadata.project.name = String(data.resourceGroupName) | ||
metadata.availability_zone = String(data.zone) | ||
metadata.machine.type = String(data.vmSize) | ||
metadata.region = String(data.location) | ||
|
||
return metadata | ||
} | ||
|
||
module.exports = { getMetadataAzure } |
Oops, something went wrong.