Skip to content

Commit bea17e9

Browse files
authored
Support IPv6 endpoints in IMDS (#3853)
1 parent ca08c83 commit bea17e9

13 files changed

+275
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "IMDS",
4+
"description": "Support IPv6 endpoints in IMDS"
5+
}

lib/metadata_service.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
var AWS = require('./core');
22
require('./http');
33
var inherit = AWS.util.inherit;
4+
var getMetadataServiceEndpoint = require('./metadata_service/get_metadata_service_endpoint');
5+
var URL = require('url').URL;
46

57
/**
68
* Represents a metadata service available on EC2 instances. Using the
@@ -20,9 +22,9 @@ var inherit = AWS.util.inherit;
2022
*/
2123
AWS.MetadataService = inherit({
2224
/**
23-
* @return [String] the hostname of the instance metadata service
25+
* @return [String] the endpoint of the instance metadata service
2426
*/
25-
host: '169.254.169.254',
27+
endpoint: getMetadataServiceEndpoint(),
2628

2729
/**
2830
* @!ignore
@@ -58,6 +60,10 @@ AWS.MetadataService = inherit({
5860
* retry delay on retryable errors. See AWS.Config for details.
5961
*/
6062
constructor: function MetadataService(options) {
63+
if (options && options.host) {
64+
options.endpoint = 'http://' + options.host;
65+
delete options.host;
66+
}
6167
AWS.util.update(this, options);
6268
},
6369

@@ -90,7 +96,11 @@ AWS.MetadataService = inherit({
9096
}
9197

9298
path = path || '/';
93-
var httpRequest = new AWS.HttpRequest('http://' + this.host + path);
99+
100+
// Verify that host is a valid URL
101+
if (URL) { new URL(this.endpoint); }
102+
103+
var httpRequest = new AWS.HttpRequest(this.endpoint + path);
94104
httpRequest.method = options.method || 'GET';
95105
if (options.headers) {
96106
httpRequest.headers = options.headers;

lib/metadata_service/endpoint.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var Endpoint = {
2+
IPv4: 'http://169.254.169.254',
3+
IPv6: 'http://[fd00:ec2::254]',
4+
};
5+
6+
module.exports = Endpoint;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
var ENV_ENDPOINT_NAME = 'AWS_EC2_METADATA_SERVICE_ENDPOINT';
2+
var CONFIG_ENDPOINT_NAME = 'ec2_metadata_service_endpoint';
3+
4+
var ENDPOINT_CONFIG_OPTIONS = {
5+
environmentVariableSelector: function(env) { return env[ENV_ENDPOINT_NAME]; },
6+
configFileSelector: function(profile) { return profile[CONFIG_ENDPOINT_NAME]; },
7+
default: undefined,
8+
};
9+
10+
module.exports = {
11+
ENV_ENDPOINT_NAME: ENV_ENDPOINT_NAME,
12+
CONFIG_ENDPOINT_NAME: CONFIG_ENDPOINT_NAME,
13+
ENDPOINT_CONFIG_OPTIONS: ENDPOINT_CONFIG_OPTIONS
14+
};

lib/metadata_service/endpoint_mode.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var EndpointMode = {
2+
IPv4: 'IPv4',
3+
IPv6: 'IPv6',
4+
};
5+
6+
module.exports = EndpointMode;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
var EndpointMode = require('./endpoint_mode');
2+
3+
var ENV_ENDPOINT_MODE_NAME = 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE';
4+
var CONFIG_ENDPOINT_MODE_NAME = 'ec2_metadata_service_endpoint_mode';
5+
6+
var ENDPOINT_MODE_CONFIG_OPTIONS = {
7+
environmentVariableSelector: function(env) { return env[ENV_ENDPOINT_MODE_NAME]; },
8+
configFileSelector: function(profile) { return profile[CONFIG_ENDPOINT_MODE_NAME]; },
9+
default: EndpointMode.IPv4,
10+
};
11+
12+
module.exports = {
13+
ENV_ENDPOINT_MODE_NAME: ENV_ENDPOINT_MODE_NAME,
14+
CONFIG_ENDPOINT_MODE_NAME: CONFIG_ENDPOINT_MODE_NAME,
15+
ENDPOINT_MODE_CONFIG_OPTIONS: ENDPOINT_MODE_CONFIG_OPTIONS
16+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
var AWS = require('../core');
2+
3+
var Endpoint = require('./endpoint');
4+
var EndpointMode = require('./endpoint_mode');
5+
6+
var ENDPOINT_CONFIG_OPTIONS = require('./endpoint_config_options').ENDPOINT_CONFIG_OPTIONS;
7+
var ENDPOINT_MODE_CONFIG_OPTIONS = require('./endpoint_mode_config_options').ENDPOINT_MODE_CONFIG_OPTIONS;
8+
9+
var getMetadataServiceEndpoint = function() {
10+
var endpoint = AWS.util.loadConfig(ENDPOINT_CONFIG_OPTIONS);
11+
if (endpoint !== undefined) return endpoint;
12+
13+
var endpointMode = AWS.util.loadConfig(ENDPOINT_MODE_CONFIG_OPTIONS);
14+
switch (endpointMode) {
15+
case EndpointMode.IPv4:
16+
return Endpoint.IPv4;
17+
case EndpointMode.IPv6:
18+
return Endpoint.IPv6;
19+
default:
20+
throw new Error('Unsupported endpoint mode: ' + endpointMode);
21+
}
22+
};
23+
24+
module.exports = getMetadataServiceEndpoint;

lib/node_loader.js

+27
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ util.clientSideMonitoring = {
2121
util.iniLoader = require('./shared-ini').iniLoader;
2222
util.getSystemErrorName = require('util').getSystemErrorName;
2323

24+
util.loadConfig = function(options) {
25+
var envValue = options.environmentVariableSelector(process.env);
26+
if (envValue !== undefined) {
27+
return envValue;
28+
}
29+
30+
var configFile = {};
31+
try {
32+
configFile = util.iniLoader ? util.iniLoader.loadFrom({
33+
isConfig: true,
34+
filename: process.env[util.sharedConfigFileEnv]
35+
}) : {};
36+
} catch (e) {}
37+
var sharedFileConfig = configFile[
38+
process.env.AWS_PROFILE || util.defaultProfile
39+
] || {};
40+
var configValue = options.configFileSelector(sharedFileConfig);
41+
if (configValue !== undefined) {
42+
return configValue;
43+
}
44+
45+
if (typeof options.default === 'function') {
46+
return options.default();
47+
}
48+
return options.default;
49+
};
50+
2451
var AWS;
2552

2653
/**

test/credentials.spec.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/metadata_service.spec.js

+14
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,20 @@ if (AWS.util.isNode()) {
261261
}));
262262
}
263263
});
264+
265+
it('should load credentials when host is not passed', function(done) {
266+
tokenToReturn = 'TOKEN';
267+
service = new AWS.MetadataService();
268+
// Simulate endpoint is received from call to getMetadataServiceEndpoint
269+
service.endpoint = 'http://127.0.0.1:' + port;
270+
service.loadCredentials(function(err, data) {
271+
expect(err).to.equal(null);
272+
expect(data.Code).to.equal('Success');
273+
expect(data.AccessKeyId).to.equal('KEY');
274+
expect(data.SecretAccessKey).to.equal('SECRET');
275+
done();
276+
});
277+
});
264278
});
265279

266280
describe('request', function() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
var helpers = require('./../helpers');
2+
var options = require('../../lib/metadata_service/endpoint_config_options');
3+
4+
var AWS = helpers.AWS;
5+
var ENDPOINT_CONFIG_OPTIONS = options.ENDPOINT_CONFIG_OPTIONS;
6+
var ENV_ENDPOINT_NAME = options.ENV_ENDPOINT_NAME;
7+
var CONFIG_ENDPOINT_NAME = options.CONFIG_ENDPOINT_NAME;
8+
9+
if (AWS.util.isNode()) {
10+
describe('endpointConfigOptions', function() {
11+
describe('environmentVariableSelector', function() {
12+
var environmentVariableSelector = ENDPOINT_CONFIG_OPTIONS.environmentVariableSelector;
13+
[undefined, 'mockEndpoint'].forEach(function(mockEndpoint) {
14+
it(
15+
'when env[' + ENV_ENDPOINT_NAME + ']: ' + mockEndpoint,
16+
function() {
17+
expect(
18+
environmentVariableSelector({
19+
[ENV_ENDPOINT_NAME]: mockEndpoint,
20+
})
21+
).to.equal(mockEndpoint);
22+
}
23+
);
24+
});
25+
});
26+
27+
describe('configFileSelector', function() {
28+
var configFileSelector = ENDPOINT_CONFIG_OPTIONS.configFileSelector;
29+
[undefined, 'mockEndpoint'].forEach(function(mockEndpoint) {
30+
it(
31+
'when env[' + CONFIG_ENDPOINT_NAME + ']: ' + mockEndpoint,
32+
function() {
33+
expect(
34+
configFileSelector({
35+
[CONFIG_ENDPOINT_NAME]: mockEndpoint,
36+
})
37+
).to.equal(mockEndpoint);
38+
}
39+
);
40+
});
41+
});
42+
43+
it('default returns undefined', function() {
44+
var defaultKey = ENDPOINT_CONFIG_OPTIONS.default;
45+
expect(defaultKey).to.equal(undefined);
46+
});
47+
});
48+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
var helpers = require('./../helpers');
2+
var options = require('../../lib/metadata_service/endpoint_mode_config_options');
3+
var EndpointMode = require('../../lib/metadata_service/endpoint_mode');
4+
5+
var AWS = helpers.AWS;
6+
var ENDPOINT_MODE_CONFIG_OPTIONS = options.ENDPOINT_MODE_CONFIG_OPTIONS;
7+
var ENV_ENDPOINT_MODE_NAME = options.ENV_ENDPOINT_MODE_NAME;
8+
var CONFIG_ENDPOINT_MODE_NAME = options.CONFIG_ENDPOINT_MODE_NAME;
9+
10+
if (AWS.util.isNode()) {
11+
describe('endpointModeConfigOptions', function() {
12+
describe('environmentVariableSelector', function() {
13+
var environmentVariableSelector = ENDPOINT_MODE_CONFIG_OPTIONS.environmentVariableSelector;
14+
[undefined, 'mockEndpointMode'].forEach(function(mockEndpointMode) {
15+
it(
16+
'when env[' + ENV_ENDPOINT_MODE_NAME + ']: ' + mockEndpointMode,
17+
function() {
18+
expect(
19+
environmentVariableSelector({
20+
[ENV_ENDPOINT_MODE_NAME]: mockEndpointMode,
21+
})
22+
).to.equal(mockEndpointMode);
23+
}
24+
);
25+
});
26+
});
27+
28+
describe('configFileSelector', function() {
29+
var configFileSelector = ENDPOINT_MODE_CONFIG_OPTIONS.configFileSelector;
30+
[undefined, 'mockEndpointMode'].forEach(function(mockEndpointMode) {
31+
it(
32+
'when env[' + CONFIG_ENDPOINT_MODE_NAME + ']: ' + mockEndpointMode,
33+
function() {
34+
expect(
35+
configFileSelector({
36+
[CONFIG_ENDPOINT_MODE_NAME]: mockEndpointMode,
37+
})
38+
).to.equal(mockEndpointMode);
39+
}
40+
);
41+
});
42+
});
43+
44+
it('default returns ' + EndpointMode.IPv4, function() {
45+
var defaultKey = ENDPOINT_MODE_CONFIG_OPTIONS.default;
46+
expect(defaultKey).to.equal(EndpointMode.IPv4);
47+
});
48+
});
49+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
var helpers = require('./../helpers');
2+
var getMetadataServiceEndpoint = require('../../lib/metadata_service/get_metadata_service_endpoint');
3+
4+
var Endpoint = require('../../lib/metadata_service/endpoint');
5+
var EndpointMode = require('../../lib/metadata_service/endpoint_mode');
6+
var ENDPOINT_CONFIG_OPTIONS = require('../../lib/metadata_service/endpoint_config_options').ENDPOINT_CONFIG_OPTIONS;
7+
8+
var AWS = helpers.AWS;
9+
10+
if (AWS.util.isNode()) {
11+
describe('getMetadataServiceEndpoint', function() {
12+
describe('when endpoint is defined', function() {
13+
it('returns endpoint', function() {
14+
var mockEndpoint = 'http://127.0.0.1';
15+
helpers.spyOn(AWS.util, 'loadConfig').andReturn(mockEndpoint);
16+
expect(getMetadataServiceEndpoint()).to.equal(mockEndpoint);
17+
});
18+
});
19+
20+
describe('when endpoint is not defined', function() {
21+
[
22+
[Endpoint.IPv4, EndpointMode.IPv4],
23+
[Endpoint.IPv6, EndpointMode.IPv6],
24+
].forEach(function([endpoint, endpointMode]) {
25+
it('returns endpoint:'+ endpoint + ' for endpointMode:' + endpointMode, function() {
26+
helpers.spyOn(AWS.util, 'loadConfig').andCallFake(function(options) {
27+
if (options === ENDPOINT_CONFIG_OPTIONS) {
28+
return undefined;
29+
} else {
30+
return endpointMode;
31+
}
32+
});
33+
expect(getMetadataServiceEndpoint()).to.equal(endpoint);
34+
});
35+
});
36+
37+
it('throws error for invalid endpointMode:invalid', function() {
38+
const invalidEndpointMode = 'invalid';
39+
helpers.spyOn(AWS.util, 'loadConfig').andCallFake(function(options) {
40+
if (options === ENDPOINT_CONFIG_OPTIONS) {
41+
return undefined;
42+
} else {
43+
return invalidEndpointMode;
44+
}
45+
});
46+
expect(function() {
47+
getMetadataServiceEndpoint();
48+
}).to['throw']('Unsupported endpoint mode: ' + invalidEndpointMode);
49+
});
50+
});
51+
});
52+
}

0 commit comments

Comments
 (0)