microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fe0a62f3e54ee74cfe2d1f2d0288d4dab7204b36

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

ts/tools/scripts/azureDeploy.mjs

336lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import child_process from "node:child_process";
5import chalk from "chalk";
6import registerDebug from "debug";
7import path from "node:path";
8import { fileURLToPath } from "node:url";
9
10const debug = registerDebug("typeagent:azure:deploy");
11const debugError = registerDebug("typeagent:azure:deploy:error");
12
13const __filename = fileURLToPath(import.meta.url);
14const __dirname = path.dirname(__filename);
15
16function status(message) {
17 console.log(chalk.gray(message));
18}
19
20function success(message) {
21 console.log(chalk.greenBright(message));
22}
23
24function warn(message) {
25 console.error(chalk.yellowBright(message));
26}
27
28function error(message) {
29 console.error(chalk.redBright(message));
30}
31
32const defaultGlobalOptions = {
33 location: "eastus", // deployment location
34 name: "", // deployment name
35};
36
37const commands = ["create", "delete", "purge"];
38function parseArgs() {
39 const args = process.argv;
40 if (args.length < 3) {
41 throw new Error("Command not specified.");
42 }
43 const command = args[2];
44
45 if (!commands.includes(command)) {
46 throw new Error(
47 `Invalid command '${command}'. Valid commands are: ${commands.map((c) => `'${c}'`).join(", ")}`,
48 );
49 }
50
51 const options =
52 command === "delete"
53 ? {
54 ...defaultGlobalOptions,
55 purge: true, // for delete: default to purge.
56 }
57 : { ...defaultGlobalOptions };
58
59 for (let i = 3; i < args.length; i++) {
60 if (!args[i].startsWith("--")) {
61 throw new Error(
62 `Unknown argument for command ${command}: ${args[i]}`,
63 );
64 }
65 const key = args[i].slice(2);
66 if (!(key in options)) {
67 throw new Error(
68 `Unknown options for command ${command}: ${args[i]}`,
69 );
70 }
71
72 const value = args[i + 1];
73 if (typeof options[key] === "boolean") {
74 if (value === "false" || value === "0") {
75 options[key] = false;
76 } else {
77 options[key] = true;
78 if (value !== "true" && value !== "1") {
79 // Don't consume the next argument
80 continue;
81 }
82 }
83 } else {
84 // string
85 options[key] = value;
86 }
87 // Consume the next argument
88 i++;
89 }
90 return { command, options };
91}
92
93function checkAzCliLoggedIn() {
94 // We use this to validate that the user is logged in (already ran `az login`).
95 try {
96 const account = JSON.parse(
97 child_process.execFileSync("az", ["account", "show"]),
98 );
99 console.log(`Logged in as ${chalk.cyanBright(account.user.name)}`);
100 } catch (e) {
101 debugError(e);
102 throw new Error("User not logged in to Azure CLI. Run 'az login'.");
103 }
104}
105
106function getSubscriptionId() {
107 let subscriptionId, subscriptionName;
108
109 try {
110 const subscriptions = JSON.parse(
111 child_process.execFileSync("az", ["account", "list"]),
112 );
113
114 subscriptions.forEach((subscription) => {
115 if (subscription.isDefault) {
116 subscriptionId = subscription.id;
117 subscriptionName = subscription.name;
118 }
119 });
120 } catch (e) {
121 debugError(e);
122 throw new Error("Unable to get principal id of the current user.");
123 }
124
125 if (subscriptionId) {
126 console.log(
127 `Using subscription ${chalk.cyanBright(subscriptionName)} [${chalk.cyanBright(subscriptionId)}].`,
128 );
129 return subscriptionId;
130 } else {
131 throw new Error(
132 "Unable to find default subscription! Unable to continue.",
133 );
134 }
135}
136
137function getDeploymentName(options) {
138 if (options.name) {
139 return options.name;
140 }
141 return `typeagent-${options.location}`;
142}
143
144function createDeployment(options) {
145 const deploymentName = getDeploymentName(options);
146 status(`Creating deployment ${deploymentName}...`);
147 const output = JSON.parse(
148 child_process.execFileSync("az", [
149 "deployment",
150 "sub",
151 "create",
152 "--location",
153 options.location,
154 "--template-file",
155 path.resolve(__dirname, "./armTemplates/template.json"),
156 "--name",
157 getDeploymentName(options),
158 ]),
159 );
160
161 console.log("Resources created:");
162 console.log(
163 output.properties.outputResources.map((r) => ` ${r.id}`).join("\n"),
164 );
165
166 return output.properties.parameters.vaults_name.value;
167}
168
169function deleteDeployment(options, subscriptionId) {
170 const deploymentName = getDeploymentName(options);
171 status("Getting deployment details...");
172 const deployment = JSON.parse(
173 child_process.execFileSync("az", [
174 "deployment",
175 "sub",
176 "show",
177 "--name",
178 deploymentName,
179 ]),
180 );
181 const resourceGroupName = deployment.properties.parameters.group_name.value;
182 try {
183 status(`Deleting resource group ${resourceGroupName}...`);
184 child_process.execFileSync("az", [
185 "group",
186 "delete",
187 "--name",
188 resourceGroupName,
189 "--yes",
190 ]);
191
192 success(`Resource group ${resourceGroupName} deleted`);
193 } catch (e) {
194 if (!e.message.includes(" could not be found")) {
195 throw e;
196 }
197 warn(e.message);
198 }
199
200 status(`Deleting deployment ${deploymentName}...`);
201 child_process.execFileSync("az", [
202 "deployment",
203 "sub",
204 "delete",
205 "--name",
206 deploymentName,
207 ]);
208
209 success(`Deployment ${deploymentName} deleted`);
210
211 if (options.purge) {
212 purgeDeleted(options, subscriptionId);
213 }
214}
215
216function getDeletedResources(uri, tag) {
217 const deleted = child_process.execFileSync("az", [
218 "rest",
219 "--method",
220 "get",
221 "--header",
222 "Accept=application/json",
223 "-u",
224 uri,
225 ]);
226
227 debug(`Get delete: ${uri}\n${deleted}`);
228 const deletedJson = JSON.parse(deleted);
229 return deletedJson.value
230 .filter((r) => r.tags?.typeagent === tag)
231 .map((r) => r.id);
232}
233
234function getDeleteKeyVaults(tag) {
235 const deleted = child_process.execFileSync("az", [
236 "keyvault",
237 "list-deleted",
238 ]);
239 debug(`Get Delete KeyVault: ${deleted}`);
240 const deletedJson = JSON.parse(deleted);
241 return deletedJson
242 .filter((r) => r.properties?.tags?.typeagent === tag)
243 .map((r) => r.name);
244}
245
246function purgeDeleted(options, subscriptionId) {
247 const deploymentName = getDeploymentName(options);
248 status(`Purging resources for deployment ${deploymentName}...`);
249 try {
250 const resources = getDeletedResources(
251 `https://management.azure.com/subscriptions/${subscriptionId}/providers/Microsoft.CognitiveServices/deletedAccounts?api-version=2021-04-30`,
252 deploymentName,
253 );
254
255 if (resources.length !== 0) {
256 status("Purging deleted cognitive services...");
257 status(resources.map((r) => ` ${r}`).join("\n"));
258 child_process.execFileSync(
259 "az",
260 ["resource", "delete", "--ids", ...resources],
261 { encoding: "utf8" },
262 );
263 }
264
265 const kvs = getDeleteKeyVaults(deploymentName);
266 if (kvs.length !== 0) {
267 status("Purging delete keyvault...");
268 status(kvs.map((r) => ` ${r}`).join("\n"));
269 for (const kv of kvs) {
270 const kvPurgeResult = child_process.execFileSync(
271 "az",
272 ["keyvault", "purge", "--no-wait", "--name", kv],
273 { encoding: "utf8" },
274 );
275 }
276 }
277 success("Purged Completed.");
278 } catch (e) {
279 e.message = `Error purging deleted resources.\n${e.message}`;
280 throw e;
281 }
282}
283
284function getErrorMessage(e) {
285 try {
286 const json = JSON.parse(e.message);
287 return JSON.stringify(json, null, 2);
288 } catch {}
289 return e.message;
290}
291
292function getKeys(vaultName) {
293 child_process.execFileSync("node", [
294 path.resolve(__dirname, "./getKeys.mjs"),
295 "--vault",
296 vaultName,
297 ]);
298}
299
300function main() {
301 let usage = true;
302 try {
303 const { command, options } = parseArgs();
304 usage = false;
305 checkAzCliLoggedIn();
306 const subscriptionId = getSubscriptionId();
307 if (command === "create") {
308 const kv = createDeployment(options);
309 getKeys(kv);
310 } else if (command === "delete") {
311 deleteDeployment(options, subscriptionId);
312 } else if (command === "purge") {
313 purgeDeleted(options, subscriptionId);
314 }
315 } catch (e) {
316 error(`ERROR: ${getErrorMessage(e)}`);
317 if (usage) {
318 console.log(
319 [
320 "Usage: ",
321 " node azureDeploy.js create [--location <location>] [--name <name>]",
322 " node azureDeploy.js delete [--location <location>] [--name <name>] [--purge]",
323 " node azureDeploy.js purge [--location <location>] [--name <name>]",
324 "",
325 "Options:",
326 " --location <location> The location the deployment is in. Default: eastus",
327 " --name <name> The name of the deployment. Default: typeagent-<location>",
328 " --purge [true|false] Purge deleted resources. Default: true",
329 ].join("\n"),
330 );
331 }
332 process.exit(1);
333 }
334}
335
336main();
337