microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.1.0-py

Branches

Tags

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

Clone

HTTPS

Download ZIP

ts/tools/scripts/azureDeploy.mjs

340lines · modecode

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