-
Notifications
You must be signed in to change notification settings - Fork 4k
/
awscli-compatible.ts
203 lines (179 loc) · 7.18 KB
/
awscli-compatible.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import * as AWS from 'aws-sdk';
import * as child_process from 'child_process';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import { debug } from '../../logging';
import { SharedIniFile } from './sdk_ini_file';
/**
* Behaviors to match AWS CLI
*
* See these links:
*
* https://docs.aws.amazon.com/cli/latest/topic/config-vars.html
* https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
*/
export class AwsCliCompatible {
/**
* Build an AWS CLI-compatible credential chain provider
*
* This is similar to the default credential provider chain created by the SDK
* except:
*
* 1. Accepts profile argument in the constructor (the SDK must have it prepopulated
* in the environment).
* 2. Conditionally checks EC2 credentials, because checking for EC2
* credentials on a non-EC2 machine may lead to long delays (in the best case)
* or an exception (in the worst case).
* 3. Respects $AWS_SHARED_CREDENTIALS_FILE.
* 4. Respects $AWS_DEFAULT_PROFILE in addition to $AWS_PROFILE.
*/
public static async credentialChain(
profile: string | undefined,
ec2creds: boolean | undefined,
containerCreds: boolean | undefined,
httpOptions: AWS.HTTPOptions | undefined) {
await forceSdkToReadConfigIfPresent();
profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default';
const sources = [
() => new AWS.EnvironmentCredentials('AWS'),
() => new AWS.EnvironmentCredentials('AMAZON'),
];
if (await fs.pathExists(credentialsFileName())) {
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
}
if (await fs.pathExists(configFileName())) {
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
}
if (containerCreds ?? hasEcsCredentials()) {
sources.push(() => new AWS.ECSCredentials());
} else if (ec2creds ?? await hasEc2Credentials()) {
// else if: don't get EC2 creds if we should have gotten ECS creds--ECS instances also
// run on EC2 boxes but the creds represent something different. Same behavior as
// upstream code.
sources.push(() => new AWS.EC2MetadataCredentials());
}
return new AWS.CredentialProviderChain(sources);
}
/**
* Return the default region in a CLI-compatible way
*
* Mostly copied from node_loader.js, but with the following differences to make it
* AWS CLI compatible:
*
* 1. Takes a profile name as an argument (instead of forcing it to be taken from $AWS_PROFILE).
* This requires having made a copy of the SDK's `SharedIniFile` (the original
* does not take an argument).
* 2. $AWS_DEFAULT_PROFILE and $AWS_DEFAULT_REGION are also respected.
*
* Lambda and CodeBuild set the $AWS_REGION variable.
*
* FIXME: EC2 instances require querying the metadata service to determine the current region.
*/
public static async region(profile: string | undefined): Promise<string> {
profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default';
// Defaults inside constructor
const toCheck = [
{ filename: credentialsFileName(), profile },
{ isConfig: true, filename: configFileName(), profile },
{ isConfig: true, filename: configFileName(), profile: 'default' },
];
let region = process.env.AWS_REGION || process.env.AMAZON_REGION ||
process.env.AWS_DEFAULT_REGION || process.env.AMAZON_DEFAULT_REGION;
while (!region && toCheck.length > 0) {
const options = toCheck.shift()!;
if (await fs.pathExists(options.filename)) {
const configFile = new SharedIniFile(options);
const section = await configFile.getProfile(options.profile);
region = section?.region;
}
}
if (!region) {
const usedProfile = !profile ? '' : ` (profile: "${profile}")`;
region = 'us-east-1'; // This is what the AWS CLI does
debug(`Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${region}'`);
}
return region;
}
}
/**
* Return whether it looks like we'll have ECS credentials available
*/
function hasEcsCredentials(): boolean {
return (AWS.ECSCredentials.prototype as any).isConfiguredForEcsCredentials();
}
/**
* Return whether we're on an EC2 instance
*/
async function hasEc2Credentials() {
debug("Determining whether we're on an EC2 instance.");
let instance = false;
if (process.platform === 'win32') {
// https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/identify_ec2_instances.html
const result = await util.promisify(child_process.exec)('wmic path win32_computersystemproduct get uuid', { encoding: 'utf-8' });
// output looks like
// UUID
// EC2AE145-D1DC-13B2-94ED-01234ABCDEF
const lines = result.stdout.toString().split('\n');
instance = lines.some(x => matchesRegex(/^ec2/i, x));
} else {
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html
const files: Array<[string, RegExp]> = [
// This recognizes the Xen hypervisor based instances (pre-5th gen)
['/sys/hypervisor/uuid', /^ec2/i],
// This recognizes the new Hypervisor (5th-gen instances and higher)
// Can't use the advertised file '/sys/devices/virtual/dmi/id/product_uuid' because it requires root to read.
// Instead, sys_vendor contains something like 'Amazon EC2'.
['/sys/devices/virtual/dmi/id/sys_vendor', /ec2/i],
];
for (const [file, re] of files) {
if (matchesRegex(re, readIfPossible(file))) {
instance = true;
break;
}
}
}
debug(instance ? 'Looks like EC2 instance.' : 'Does not look like EC2 instance.');
return instance;
}
function homeDir() {
return process.env.HOME || process.env.USERPROFILE
|| (process.env.HOMEPATH ? ((process.env.HOMEDRIVE || 'C:/') + process.env.HOMEPATH) : null) || os.homedir();
}
function credentialsFileName() {
return process.env.AWS_SHARED_CREDENTIALS_FILE || path.join(homeDir(), '.aws', 'credentials');
}
function configFileName() {
return process.env.AWS_CONFIG_FILE || path.join(homeDir(), '.aws', 'config');
}
/**
* Force the JS SDK to honor the ~/.aws/config file (and various settings therein)
*
* For example, ther is just *NO* way to do AssumeRole credentials as long as AWS_SDK_LOAD_CONFIG is not set,
* or read credentials from that file.
*
* The SDK crashes if the variable is set but the file does not exist, so conditionally set it.
*/
async function forceSdkToReadConfigIfPresent() {
if (await fs.pathExists(configFileName())) {
process.env.AWS_SDK_LOAD_CONFIG = '1';
}
}
function matchesRegex(re: RegExp, s: string | undefined) {
return s !== undefined && re.exec(s) !== null;
}
/**
* Read a file if it exists, or return undefined
*
* Not async because it is used in the constructor
*/
function readIfPossible(filename: string): string | undefined {
try {
if (!fs.pathExistsSync(filename)) { return undefined; }
return fs.readFileSync(filename, { encoding: 'utf-8' });
} catch (e) {
debug(e);
return undefined;
}
}