-
Notifications
You must be signed in to change notification settings - Fork 544
/
Copy pathindex.js
205 lines (180 loc) · 6.56 KB
/
index.js
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
204
205
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import child_process from "node:child_process";
import { appendFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { loadRC, saveRC } from "@fluidframework/tool-utils";
// Wraps the given string in quotes, escaping any quotes already present in the string
// with '\"', which is compatible with cmd, bash, and zsh.
function quote(str) {
return `"${str.split('"').join('\\"')}"`;
}
// Converts the given 'entries' [key, value][] array into export statements for bash
// and zsh, appending the result to the given 'shellRc' file.
async function exportToShellRc(shellRc, entries) {
const rcPath = path.join(os.homedir(), shellRc);
console.log(`Writing '${rcPath}'.`);
const stmts = `\n# Fluid dev/test secrets\n${entries
.map(([key, value]) => `export ${key}=${quote(value)}`)
.join("\n")}\n`;
return appendFile(rcPath, stmts, "utf8");
}
// Persists the given 'env' map to the users environment. The method used depends
// on the user's platform and login shell.
async function saveEnv(env) {
const entries = Object.entries(env);
// Note that 'SHELL' return's the user's login shell, which isn't necessarily
// the shell used to launch node (e.g., if the user is running a nested shell).
// However, the environment will be inherited when their preferred shell is
// launched from the login shell.
const shell = process.env.SHELL;
const shellName = shell && path.basename(shell, path.extname(shell));
switch (shellName) {
// Gitbash on windows will appear as bash.exe
case "bash": {
return exportToShellRc(
// '.bash_profile' is used for the "login shell" ('bash -l').
process.env.TERM_PROGRAM === "Apple_Terminal" ? ".bash_profile" : ".bashrc",
entries,
);
}
case "zsh": {
return exportToShellRc(".zshrc", entries);
}
case "fish": {
console.log("Writing '~/.config/fish/fish_variables'.");
// For 'fish' we use 'set -xU', which dedupes and performs its own escaping.
// Note that we must pass the 'shell' option, otherwise node will spawn '/bin/sh'.
return Promise.all(
entries.map(async ([key, value]) =>
execAsync(`set -xU '${key}' '${value}'`, { shell }),
),
);
}
default: {
if (!process.platform === "win32") {
throw new Error(`Unsupported shell: '${shellName}'.`);
} else {
console.log("Writing 'HKEY_CURRENT_USER\\Environment'.");
// On Windows, invoke 'setx' to update the user's persistent environment variables.
return Promise.all(
entries.map(async ([key, value]) => execAsync(`setx ${key} ${quote(value)}`)),
);
}
}
}
}
async function getKeys(keyVaultClient, rc, vaultName) {
console.log(`\nGetting secrets from ${vaultName}:`);
const secretList = await keyVaultClient.getSecrets(vaultName);
const p = [];
for (const secret of secretList) {
if (secret.attributes.enabled) {
const secretName = secret.id.split("/").pop();
// exclude secrets with automation prefix, which should only be used in automation
if (!secretName.startsWith("automation")) {
p.push(
(async () => {
const response = await keyVaultClient.getSecret(vaultName, secretName);
const envName = secretName.split("-").join("__"); // secret name can't contain underscores
console.log(` ${envName}`);
rc.secrets[envName] = response.value;
})(),
);
}
}
}
return Promise.all(p);
}
async function execAsync(command, options) {
return new Promise((res, rej) => {
child_process.exec(command, options, (err, stdout, stderr) => {
if (err) {
rej(err);
return;
}
if (stderr) {
console.log(stderr + stdout);
}
res(stdout);
});
});
}
class AzCliKeyVaultClient {
static async get() {
// We use this to validate that the user is logged in (already ran `az login`).
try {
await execAsync("az ad signed-in-user show");
} catch (error) {
// Depending on how az login was performed, the above command may fail with a variety of errors.
// I've seen the one below in WSL2, but there are probably others.
// FATAL ERROR: Error: Command failed: az ad signed-in-user show
// ERROR: AADSTS530003: Your device is required to be managed to access this resource.
if (error.message.includes("AADSTS530003")) {
console.log(
`\nAn error occurred running \`az ad signed-in-user show\` that suggests you might need to \`az logout\` and ` +
`\`az login\` again. One potential cause for this is having used \`az login --use-device-code\`. Error:\n\n` +
`${error.message}`,
);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
// Note: 'az keyvault' commands work regardless of which subscription is currently "in context",
// as long as the user is listed in the vault's access policy, so we don't need to do 'az account set'.
return new AzCliKeyVaultClient();
}
async getSecrets(vaultName) {
return JSON.parse(await execAsync(`az keyvault secret list --vault-name ${vaultName}`));
}
async getSecret(vaultName, secretName) {
return JSON.parse(
await execAsync(
`az keyvault secret show --vault-name ${vaultName} --name ${secretName}`,
),
);
}
}
async function getClient() {
return AzCliKeyVaultClient.get();
}
try {
const rc = await loadRC();
if (rc.secrets === undefined) {
rc.secrets = {};
}
// For debugging, change the following to 'false' and uncommented the if block to skip connecting to
// Azure Key Vault and instead use the secrets cached in '~/.fluidtoolrc'.
// if (true) {
const client = await getClient();
// Primary key vault for test/dev secrets shared by Microsoft-internal teams working on FF
await getKeys(client, rc, "prague-key-vault");
try {
// Key Vault with restricted access for the FF dev team only
await getKeys(client, rc, "ff-internal-dev-secrets");
console.log(
"\nNote: Default dev/test secrets overwritten with values from internal key vault.",
);
} catch {
// Drop the error
}
// }
console.log(`\nWriting '${path.join(os.homedir(), ".fluidtoolrc")}'.`);
await saveRC(rc);
await saveEnv(rc.secrets);
console.warn(`\nFor the new environment to take effect, please restart your terminal.\n`);
} catch (error) {
if (error.message.includes("'az' is not recognized as an internal or external command")) {
console.error(
`ERROR: Azure CLI is not installed. Install it and run 'az login' before running this tool.`,
);
// eslint-disable-next-line no-undef
exit(0);
}
console.error(`FATAL ERROR: ${error.stack}`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(-1);
}