@bjorntech/alchemy-azure 0.2.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2189 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ import * as Schedule from "effect/Schedule";
4
+ import { Unowned } from "alchemy/AdoptPolicy";
5
+ import { isResolved } from "alchemy/Diff";
6
+ import type { Input } from "alchemy/Input";
7
+ import { Resource, type ResourceClass } from "alchemy";
8
+ import * as Provider from "alchemy/Provider";
9
+ import { makeAzureClients } from "./Clients.ts";
10
+ import { azureError, isNotFound } from "./Errors.ts";
11
+ import {
12
+ collectAzurePages,
13
+ makePhysicalNames,
14
+ requireLocation,
15
+ resourceGroupName,
16
+ resolveResourceValue,
17
+ withHeartbeat,
18
+ type NamedResourceGroup,
19
+ } from "./Internal.ts";
20
+ import type { Providers } from "./Providers.ts";
21
+ import { hasAlchemyTags, withAlchemyTags } from "./ResourceGroup.ts";
22
+ import type { StorageAccount } from "./StorageAccount.ts";
23
+
24
+ type Tags = Record<string, string>;
25
+ type AzureResponse = {
26
+ id?: string;
27
+ location?: string;
28
+ name?: string;
29
+ provisioningState?: string;
30
+ tags?: Tags;
31
+ };
32
+
33
+ interface BaseProps {
34
+ name?: string;
35
+ resourceGroup: NamedResourceGroup;
36
+ location?: string;
37
+ tags?: Tags;
38
+ delete?: boolean;
39
+ }
40
+
41
+ type Attrs<Extra extends object = {}> = Extra & {
42
+ name: string;
43
+ resourceGroupName: string;
44
+ location: string;
45
+ resourceId: string;
46
+ provisioningState?: string;
47
+ tags?: Tags;
48
+ };
49
+
50
+ function diffOnChanges<P, A extends Attrs>(options: {
51
+ identity: (input: { id: string; instanceId: string; props: P }) =>
52
+ | Effect.Effect<Record<string, unknown>, unknown, unknown>
53
+ | Record<string, unknown>;
54
+ replace?: (props: P) => unknown;
55
+ replaceChanged?: (olds: P, news: P) => boolean;
56
+ mutable?: (props: P) => unknown;
57
+ }) {
58
+ return Effect.fnUntraced(function* (input: {
59
+ id: string;
60
+ instanceId: string;
61
+ olds: P;
62
+ news: Input<P>;
63
+ output?: A;
64
+ }) {
65
+ const { id, instanceId, olds, news, output } = input;
66
+ if (!isResolved(news)) return undefined;
67
+ if (!output) return undefined;
68
+ const desiredIdentity = yield* asEffect(options.identity({ id, instanceId, props: news }));
69
+ for (const [key, value] of Object.entries(desiredIdentity)) {
70
+ if (value !== (output as Record<string, unknown>)[key]) return { action: "replace" } as const;
71
+ }
72
+ if (options.replace && !sameValue(options.replace(olds), options.replace(news))) {
73
+ return { action: "replace" } as const;
74
+ }
75
+ if (options.replaceChanged?.(olds, news)) return { action: "replace" } as const;
76
+ if (options.mutable && !sameValue(options.mutable(olds), options.mutable(news))) {
77
+ return { action: "update" } as const;
78
+ }
79
+ return undefined;
80
+ });
81
+ }
82
+
83
+ function resourceGroupIdentity<P extends { name?: string; resourceGroup: NamedResourceGroup; location?: string }>(
84
+ nameOf: (id: string, instanceId: string, props: P) => string,
85
+ ) {
86
+ return ({ id, instanceId, props }: { id: string; instanceId: string; props: P }) =>
87
+ Effect.gen(function* () {
88
+ return {
89
+ name: nameOf(id, instanceId, props),
90
+ resourceGroupName: yield* resourceGroupName(props.resourceGroup),
91
+ location: yield* requireLocation(id, props.location, props.resourceGroup),
92
+ };
93
+ });
94
+ }
95
+
96
+ function sameValue(left: unknown, right: unknown) {
97
+ return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
98
+ }
99
+
100
+ function stableValue(value: unknown): unknown {
101
+ if (Redacted.isRedacted(value)) return Redacted.value(value);
102
+ if (Array.isArray(value)) return value.map(stableValue);
103
+ if (value && typeof value === "object") {
104
+ const record = value as Record<string, unknown>;
105
+ return Object.fromEntries(
106
+ Object.keys(record)
107
+ .filter((key) => record[key] !== undefined)
108
+ .sort()
109
+ .map((key) => [key, stableValue(record[key])]),
110
+ );
111
+ }
112
+ return value;
113
+ }
114
+
115
+ function azurePromise<A>(operation: string, resource: string | undefined, try_: () => Promise<A>) {
116
+ return Effect.tryPromise({
117
+ try: try_,
118
+ catch: (cause) => azureError({ operation, resource, cause }),
119
+ });
120
+ }
121
+
122
+ function deleteIfEnabled(
123
+ operation: () => Promise<unknown>,
124
+ name: string,
125
+ kind: string,
126
+ // Provide a label only for genuinely slow deletes; omitting it keeps fast
127
+ // deletes quiet so the console is not spammed.
128
+ heartbeatLabel?: string,
129
+ ) {
130
+ return Effect.fnUntraced(function* (input: {
131
+ olds?: { delete?: boolean };
132
+ session: { note: (message: string) => Effect.Effect<unknown, unknown, never> };
133
+ }) {
134
+ const { olds, session } = input;
135
+ if (olds?.delete === false) return;
136
+ yield* session.note(`Deleting Azure ${kind}: ${name}`);
137
+ const deletion = azurePromise(`delete ${kind}`, name, operation);
138
+ yield* (heartbeatLabel ? deletion.pipe(withHeartbeat(heartbeatLabel)) : deletion).pipe(
139
+ Effect.catchIf(isNotFound, () => Effect.void),
140
+ );
141
+ });
142
+ }
143
+
144
+ function waitForAzureDeleted(
145
+ get: () => Promise<unknown>,
146
+ name: string,
147
+ kind: string,
148
+ session: { note: (message: string) => Effect.Effect<unknown, unknown, never> },
149
+ ) {
150
+ return azurePromise(`read ${kind}`, name, get).pipe(
151
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
152
+ Effect.flatMap((resource) =>
153
+ resource === undefined
154
+ ? Effect.void
155
+ : session
156
+ .note(`Waiting for Azure ${kind} deletion: ${name}`)
157
+ .pipe(Effect.flatMap(() => Effect.fail(new AzureResourceStillExists(kind, name)))),
158
+ ),
159
+ Effect.retry({
160
+ while: (error) => error instanceof AzureResourceStillExists,
161
+ schedule: Schedule.fixed("5 seconds").pipe(Schedule.both(Schedule.recurs(60))),
162
+ }),
163
+ );
164
+ }
165
+
166
+ function retryAzureDependencyConflicts(
167
+ session: { note: (message: string) => Effect.Effect<unknown, unknown, never> },
168
+ ) {
169
+ return <A, E, R>(effect: Effect.Effect<A, E, R>) =>
170
+ effect.pipe(
171
+ Effect.retry({
172
+ while: isDependencyConflict,
173
+ schedule: Schedule.fixed("5 seconds").pipe(
174
+ Schedule.both(Schedule.recurs(60)),
175
+ Schedule.tapOutput(([, attempt]) =>
176
+ session.note(`Waiting for Azure dependencies to clear... (attempt ${attempt + 1})`),
177
+ ),
178
+ ),
179
+ }),
180
+ );
181
+ }
182
+
183
+ class AzureResourceStillExists extends Error {
184
+ constructor(kind: string, name: string) {
185
+ super(`Azure ${kind} still exists: ${name}`);
186
+ }
187
+ }
188
+
189
+ function isDependencyConflict(error: unknown) {
190
+ const data = error as { statusCode?: number; status?: number; code?: string; message?: string };
191
+ const message = String(data.message ?? "").toLowerCase();
192
+ const code = String(data.code ?? "").toLowerCase();
193
+ return (
194
+ data.statusCode === 409 ||
195
+ data.status === 409 ||
196
+ code.includes("inuse") ||
197
+ code.includes("conflict") ||
198
+ message.includes("in use") ||
199
+ message.includes("being used") ||
200
+ message.includes("cannot be deleted") ||
201
+ message.includes("another operation is in progress")
202
+ );
203
+ }
204
+
205
+ function ownershipAware<T extends object>(
206
+ id: string,
207
+ resource: { tags?: Tags },
208
+ attrs: T,
209
+ trusted = false,
210
+ ): T {
211
+ return (hasAlchemyTags(id, resource.tags) ? attrs : Unowned(attrs)) as T;
212
+ }
213
+
214
+ function ensureTaggedOwnership(
215
+ id: string,
216
+ kind: string,
217
+ name: string,
218
+ output: Attrs | undefined,
219
+ olds: object | undefined,
220
+ get: () => Promise<AzureResponse>,
221
+ ) {
222
+ return Effect.gen(function* () {
223
+ if (!output || !olds) return;
224
+ const existing = yield* azurePromise(`read ${kind} before update`, name, get).pipe(
225
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
226
+ );
227
+ if (existing && hasAlchemyTags(id, output.tags) && !hasAlchemyTags(id, existing.tags)) {
228
+ throw new Error(`Cannot adopt resource "${name}" without --adopt.`);
229
+ }
230
+ });
231
+ }
232
+
233
+ function redacted(value: string | undefined) {
234
+ return value ? Redacted.make(value) : undefined;
235
+ }
236
+
237
+ function records(value: unknown): AzureResponse[] {
238
+ return Array.isArray(value) ? (value as AzureResponse[]) : [];
239
+ }
240
+
241
+ function listResourceGroups(clients: { resources: { resourceGroups: { list: () => unknown } } }) {
242
+ return azurePromise(
243
+ "list resource groups",
244
+ undefined,
245
+ () => collectAzurePages(clients.resources.resourceGroups.list() as never) as Promise<AzureResponse[]>,
246
+ );
247
+ }
248
+
249
+ function listOwnedByResourceGroup<T extends AzureResponse, A>(
250
+ clients: { resources: { resourceGroups: { list: () => unknown } } },
251
+ list: (resourceGroupName: string) => unknown,
252
+ toAttributes: (resource: T, resourceGroupName: string) => A | Effect.Effect<A, unknown, never>,
253
+ ) {
254
+ return Effect.gen(function* () {
255
+ const groups = yield* listResourceGroups(clients);
256
+ const resources = yield* Effect.forEach(
257
+ groups,
258
+ (group) => {
259
+ if (!group.name) return Effect.succeed([] as readonly (readonly [string, T])[]);
260
+ return azurePromise(
261
+ "list resources by resource group",
262
+ group.name,
263
+ () => collectAzurePages(list(group.name!) as never),
264
+ ).pipe(
265
+ Effect.map((items) => items.map((item) => [group.name!, item as T] as const)),
266
+ );
267
+ },
268
+ { concurrency: 4 },
269
+ );
270
+ return yield* Effect.forEach(
271
+ resources.flat().filter(([, resource]) => resource.tags?.["alchemy:logical-id"]),
272
+ ([resourceGroupName, resource]) =>
273
+ asEffect(toAttributes(resource, resourceGroupName)),
274
+ { concurrency: 4 },
275
+ );
276
+ });
277
+ }
278
+
279
+ function listByResourceGroup<T extends AzureResponse>(
280
+ clients: { resources: { resourceGroups: { list: () => unknown } } },
281
+ list: (resourceGroupName: string) => unknown,
282
+ ) {
283
+ return Effect.gen(function* () {
284
+ const groups = yield* listResourceGroups(clients);
285
+ const resources = yield* Effect.forEach(
286
+ groups,
287
+ (group) => {
288
+ if (!group.name) return Effect.succeed([] as readonly (readonly [string, T])[]);
289
+ return azurePromise(
290
+ "list resources by resource group",
291
+ group.name,
292
+ () => collectAzurePages(list(group.name!) as never),
293
+ ).pipe(
294
+ Effect.map((items) => items.map((item) => [group.name!, item as T] as const)),
295
+ );
296
+ },
297
+ { concurrency: 4 },
298
+ );
299
+ return resources.flat();
300
+ });
301
+ }
302
+
303
+ function asEffect<A>(value: A | Effect.Effect<A, unknown, unknown>) {
304
+ return Effect.isEffect(value) ? value : Effect.succeed(value);
305
+ }
306
+
307
+ export interface UserAssignedIdentityProps extends BaseProps {}
308
+ export type UserAssignedIdentity = Resource<
309
+ "Azure.UserAssignedIdentity",
310
+ UserAssignedIdentityProps,
311
+ Attrs<{ principalId?: string; clientId?: string; tenantId?: string }>,
312
+ never,
313
+ Providers
314
+ >;
315
+ export const UserAssignedIdentity = Resource<UserAssignedIdentity>("Azure.UserAssignedIdentity");
316
+ export const UserAssignedIdentityProvider = () =>
317
+ Provider.effect(
318
+ UserAssignedIdentity,
319
+ Effect.gen(function* () {
320
+ const clients = yield* makeAzureClients;
321
+ const names = yield* makePhysicalNames;
322
+ const nameOf = (id: string, instanceId: string, props: UserAssignedIdentityProps) =>
323
+ names.physicalName(id, instanceId, props.name, { maxLength: 128 });
324
+ return UserAssignedIdentity.Provider.of({
325
+ stables: ["name", "resourceGroupName", "resourceId"],
326
+ list: () =>
327
+ listOwnedByResourceGroup(
328
+ clients,
329
+ (rg) => clients.msi.userAssignedIdentities.listByResourceGroup(rg),
330
+ identityAttrs,
331
+ ),
332
+ diff: diffOnChanges({
333
+ identity: resourceGroupIdentity(nameOf),
334
+ mutable: (props) => ({ tags: props.tags }),
335
+ }),
336
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
337
+ if (!output && !olds) return undefined;
338
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
339
+ if (!rg) return undefined;
340
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
341
+ const identity = yield* azurePromise("read user-assigned identity", name, () =>
342
+ clients.msi.userAssignedIdentities.get(rg, name),
343
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
344
+ return identity
345
+ ? ownershipAware(id, identity, identityAttrs(identity, rg), !!output)
346
+ : undefined;
347
+ }),
348
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
349
+ const rg = yield* resourceGroupName(news.resourceGroup);
350
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
351
+ const name = nameOf(id, instanceId, news);
352
+ yield* ensureTaggedOwnership(id, "user-assigned identity", name, output, olds, () =>
353
+ clients.msi.userAssignedIdentities.get(rg, name)
354
+ );
355
+ const identity = yield* azurePromise("reconcile user-assigned identity", name, () =>
356
+ clients.msi.userAssignedIdentities.createOrUpdate(rg, name, {
357
+ location,
358
+ tags: withAlchemyTags(id, news.tags),
359
+ }),
360
+ );
361
+ return identityAttrs(identity, rg);
362
+ }),
363
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
364
+ yield* deleteIfEnabled(
365
+ () => clients.msi.userAssignedIdentities.delete(output.resourceGroupName, output.name),
366
+ output.name,
367
+ "user-assigned identity",
368
+ )({ olds, session });
369
+ }),
370
+ });
371
+ }),
372
+ );
373
+
374
+ function identityAttrs(identity: AzureResponse, resourceGroupName: string) {
375
+ const data = identity as Record<string, unknown>;
376
+ return {
377
+ name: identity.name as string,
378
+ resourceGroupName,
379
+ location: identity.location as string,
380
+ resourceId: identity.id as string,
381
+ principalId: data.principalId as string | undefined,
382
+ clientId: data.clientId as string | undefined,
383
+ tenantId: data.tenantId as string | undefined,
384
+ tags: identity.tags,
385
+ } satisfies UserAssignedIdentity["Attributes"];
386
+ }
387
+
388
+ export interface VirtualNetworkProps extends BaseProps {
389
+ addressSpace?: string[];
390
+ subnets?: Array<{
391
+ name: string;
392
+ addressPrefix: string;
393
+ delegations?: Array<{ name: string; serviceName: string }>;
394
+ }>;
395
+ dnsServers?: string[];
396
+ }
397
+ export type VirtualNetwork = Resource<
398
+ "Azure.VirtualNetwork",
399
+ VirtualNetworkProps,
400
+ Attrs<{
401
+ addressSpace: string[];
402
+ subnets: NonNullable<VirtualNetworkProps["subnets"]>;
403
+ dnsServers?: string[];
404
+ }>,
405
+ never,
406
+ Providers
407
+ >;
408
+ export const VirtualNetwork = Resource<VirtualNetwork>("Azure.VirtualNetwork");
409
+ export const VirtualNetworkProvider = () =>
410
+ Provider.effect(
411
+ VirtualNetwork,
412
+ Effect.gen(function* () {
413
+ const clients = yield* makeAzureClients;
414
+ const names = yield* makePhysicalNames;
415
+ const nameOf = (id: string, instanceId: string, props: VirtualNetworkProps) =>
416
+ names.physicalName(id, instanceId, props.name, { maxLength: 64 });
417
+ return VirtualNetwork.Provider.of({
418
+ stables: ["name", "resourceGroupName", "resourceId"],
419
+ list: () =>
420
+ listOwnedByResourceGroup(
421
+ clients,
422
+ (rg) => clients.network.virtualNetworks.list(rg),
423
+ virtualNetworkAttrs,
424
+ ),
425
+ diff: diffOnChanges({
426
+ identity: resourceGroupIdentity(nameOf),
427
+ mutable: (props) => ({
428
+ addressSpace: props.addressSpace ?? ["10.0.0.0/16"],
429
+ subnets: props.subnets ?? [{ name: "default", addressPrefix: "10.0.0.0/24" }],
430
+ dnsServers: props.dnsServers,
431
+ tags: props.tags,
432
+ }),
433
+ }),
434
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
435
+ if (!output && !olds) return undefined;
436
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
437
+ if (!rg) return undefined;
438
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
439
+ const vnet = yield* azurePromise("read virtual network", name, () =>
440
+ clients.network.virtualNetworks.get(rg, name),
441
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
442
+ return vnet
443
+ ? ownershipAware(id, vnet, virtualNetworkAttrs(vnet, rg), !!output)
444
+ : undefined;
445
+ }),
446
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
447
+ const rg = yield* resourceGroupName(news.resourceGroup);
448
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
449
+ const name = nameOf(id, instanceId, news);
450
+ const addressPrefixes = news.addressSpace ?? ["10.0.0.0/16"];
451
+ const subnets = news.subnets ?? [{ name: "default", addressPrefix: "10.0.0.0/24" }];
452
+ yield* ensureTaggedOwnership(id, "virtual network", name, output, olds, () =>
453
+ clients.network.virtualNetworks.get(rg, name)
454
+ );
455
+ const vnet = yield* azurePromise("reconcile virtual network", name, () =>
456
+ clients.network.virtualNetworks.beginCreateOrUpdateAndWait(rg, name, {
457
+ location,
458
+ addressSpace: { addressPrefixes },
459
+ dhcpOptions: news.dnsServers ? { dnsServers: news.dnsServers } : undefined,
460
+ subnets: subnets.map((s) => ({
461
+ name: s.name,
462
+ addressPrefix: s.addressPrefix,
463
+ delegations: s.delegations?.map((d) => ({
464
+ name: d.name,
465
+ serviceName: d.serviceName,
466
+ })),
467
+ })),
468
+ tags: withAlchemyTags(id, news.tags),
469
+ } as Parameters<typeof clients.network.virtualNetworks.beginCreateOrUpdateAndWait>[2]),
470
+ );
471
+ return virtualNetworkAttrs(vnet, rg);
472
+ }),
473
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
474
+ yield* deleteIfEnabled(
475
+ () =>
476
+ clients.network.virtualNetworks.beginDeleteAndWait(
477
+ output.resourceGroupName,
478
+ output.name,
479
+ ),
480
+ output.name,
481
+ "virtual network",
482
+ )({ olds, session });
483
+ }),
484
+ });
485
+ }),
486
+ );
487
+
488
+ function virtualNetworkAttrs(vnet: AzureResponse, resourceGroupName: string) {
489
+ const data = vnet as Record<string, unknown>;
490
+ const addressSpace = data.addressSpace as { addressPrefixes?: string[] } | undefined;
491
+ const dhcpOptions = data.dhcpOptions as { dnsServers?: string[] } | undefined;
492
+ return {
493
+ name: vnet.name as string,
494
+ resourceGroupName,
495
+ location: vnet.location as string,
496
+ resourceId: vnet.id as string,
497
+ addressSpace: addressSpace?.addressPrefixes ?? [],
498
+ subnets: records(data.subnets).map((s) => ({
499
+ name: s.name as string,
500
+ addressPrefix: (s as Record<string, unknown>).addressPrefix as string,
501
+ })),
502
+ dnsServers: dhcpOptions?.dnsServers,
503
+ provisioningState: vnet.provisioningState,
504
+ tags: vnet.tags,
505
+ } satisfies VirtualNetwork["Attributes"];
506
+ }
507
+
508
+ export interface SecurityRule {
509
+ name: string;
510
+ priority: number;
511
+ direction: "Inbound" | "Outbound";
512
+ access: "Allow" | "Deny";
513
+ protocol: "Tcp" | "Udp" | "Icmp" | "Esp" | "Ah" | "*";
514
+ sourceAddressPrefix?: string;
515
+ sourcePortRange?: string;
516
+ destinationAddressPrefix?: string;
517
+ destinationPortRange?: string;
518
+ description?: string;
519
+ }
520
+ export interface NetworkSecurityGroupProps extends BaseProps {
521
+ securityRules?: SecurityRule[];
522
+ }
523
+ export type NetworkSecurityGroup = Resource<
524
+ "Azure.NetworkSecurityGroup",
525
+ NetworkSecurityGroupProps,
526
+ Attrs<{ securityRules: SecurityRule[] }>,
527
+ never,
528
+ Providers
529
+ >;
530
+ export const NetworkSecurityGroup = Resource<NetworkSecurityGroup>("Azure.NetworkSecurityGroup");
531
+ export const NetworkSecurityGroupProvider = () =>
532
+ Provider.effect(
533
+ NetworkSecurityGroup,
534
+ Effect.gen(function* () {
535
+ const clients = yield* makeAzureClients;
536
+ const names = yield* makePhysicalNames;
537
+ const nameOf = (id: string, instanceId: string, props: NetworkSecurityGroupProps) =>
538
+ names.physicalName(id, instanceId, props.name, { maxLength: 80 });
539
+ return NetworkSecurityGroup.Provider.of({
540
+ stables: ["name", "resourceGroupName", "resourceId"],
541
+ list: () =>
542
+ listOwnedByResourceGroup(
543
+ clients,
544
+ (rg) => clients.network.networkSecurityGroups.list(rg),
545
+ nsgAttrs,
546
+ ),
547
+ diff: diffOnChanges({
548
+ identity: resourceGroupIdentity(nameOf),
549
+ mutable: (props) => ({ securityRules: props.securityRules ?? [], tags: props.tags }),
550
+ }),
551
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
552
+ if (!output && !olds) return undefined;
553
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
554
+ if (!rg) return undefined;
555
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
556
+ const nsg = yield* azurePromise("read network security group", name, () =>
557
+ clients.network.networkSecurityGroups.get(rg, name),
558
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
559
+ return nsg ? ownershipAware(id, nsg, nsgAttrs(nsg, rg), !!output) : undefined;
560
+ }),
561
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
562
+ const rg = yield* resourceGroupName(news.resourceGroup);
563
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
564
+ const name = nameOf(id, instanceId, news);
565
+ yield* ensureTaggedOwnership(id, "network security group", name, output, olds, () =>
566
+ clients.network.networkSecurityGroups.get(rg, name)
567
+ );
568
+ const nsg = yield* azurePromise("reconcile network security group", name, () =>
569
+ clients.network.networkSecurityGroups.beginCreateOrUpdateAndWait(rg, name, {
570
+ location,
571
+ securityRules: (news.securityRules ?? []).map(toSecurityRule),
572
+ tags: withAlchemyTags(id, news.tags),
573
+ } as Parameters<
574
+ typeof clients.network.networkSecurityGroups.beginCreateOrUpdateAndWait
575
+ >[2]),
576
+ );
577
+ return nsgAttrs(nsg, rg);
578
+ }),
579
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
580
+ yield* deleteIfEnabled(
581
+ () =>
582
+ clients.network.networkSecurityGroups.beginDeleteAndWait(
583
+ output.resourceGroupName,
584
+ output.name,
585
+ ),
586
+ output.name,
587
+ "network security group",
588
+ )({ olds, session });
589
+ }),
590
+ });
591
+ }),
592
+ );
593
+
594
+ const toSecurityRule = (r: SecurityRule) => ({
595
+ name: r.name,
596
+ priority: r.priority,
597
+ direction: r.direction,
598
+ access: r.access,
599
+ protocol: r.protocol,
600
+ sourceAddressPrefix: r.sourceAddressPrefix ?? "*",
601
+ sourcePortRange: r.sourcePortRange ?? "*",
602
+ destinationAddressPrefix: r.destinationAddressPrefix ?? "*",
603
+ destinationPortRange: r.destinationPortRange ?? "*",
604
+ description: r.description,
605
+ });
606
+ function nsgAttrs(nsg: AzureResponse, resourceGroupName: string) {
607
+ const data = nsg as Record<string, unknown>;
608
+ return {
609
+ name: nsg.name as string,
610
+ resourceGroupName,
611
+ location: nsg.location as string,
612
+ resourceId: nsg.id as string,
613
+ securityRules: records(data.securityRules).map((r) => {
614
+ const rule = r as Record<string, unknown>;
615
+ return {
616
+ name: r.name as string,
617
+ priority: rule.priority as number,
618
+ direction: rule.direction as SecurityRule["direction"],
619
+ access: rule.access as SecurityRule["access"],
620
+ protocol: rule.protocol as SecurityRule["protocol"],
621
+ sourceAddressPrefix: rule.sourceAddressPrefix as string | undefined,
622
+ sourcePortRange: rule.sourcePortRange as string | undefined,
623
+ destinationAddressPrefix: rule.destinationAddressPrefix as string | undefined,
624
+ destinationPortRange: rule.destinationPortRange as string | undefined,
625
+ description: rule.description as string | undefined,
626
+ };
627
+ }),
628
+ provisioningState: nsg.provisioningState,
629
+ tags: nsg.tags,
630
+ } satisfies NetworkSecurityGroup["Attributes"];
631
+ }
632
+
633
+ export interface PublicIPAddressProps extends BaseProps {
634
+ sku?: "Basic" | "Standard";
635
+ allocationMethod?: "Static" | "Dynamic";
636
+ ipVersion?: "IPv4" | "IPv6";
637
+ domainNameLabel?: string;
638
+ idleTimeoutInMinutes?: number;
639
+ zones?: string[];
640
+ }
641
+ export type PublicIPAddress = Resource<
642
+ "Azure.PublicIPAddress",
643
+ PublicIPAddressProps,
644
+ Attrs<{ ipAddress?: string; fqdn?: string }>,
645
+ never,
646
+ Providers
647
+ >;
648
+ export const PublicIPAddress = Resource<PublicIPAddress>("Azure.PublicIPAddress");
649
+ export const PublicIPAddressProvider = () =>
650
+ Provider.effect(
651
+ PublicIPAddress,
652
+ Effect.gen(function* () {
653
+ const clients = yield* makeAzureClients;
654
+ const names = yield* makePhysicalNames;
655
+ const nameOf = (id: string, instanceId: string, props: PublicIPAddressProps) =>
656
+ names.physicalName(id, instanceId, props.name, { maxLength: 80 });
657
+ return PublicIPAddress.Provider.of({
658
+ stables: ["name", "resourceGroupName", "resourceId"],
659
+ list: () =>
660
+ listOwnedByResourceGroup(
661
+ clients,
662
+ (rg) => clients.network.publicIPAddresses.list(rg),
663
+ publicIpAttrs,
664
+ ),
665
+ diff: diffOnChanges({
666
+ identity: resourceGroupIdentity(nameOf),
667
+ mutable: (props) => {
668
+ const sku = props.sku ?? "Basic";
669
+ return {
670
+ domainNameLabel: props.domainNameLabel,
671
+ idleTimeoutInMinutes: props.idleTimeoutInMinutes ?? 4,
672
+ tags: props.tags,
673
+ };
674
+ },
675
+ replace: (props) => {
676
+ const sku = props.sku ?? "Basic";
677
+ return {
678
+ sku,
679
+ allocationMethod: props.allocationMethod ?? (sku === "Standard" ? "Static" : "Dynamic"),
680
+ ipVersion: props.ipVersion ?? "IPv4",
681
+ zones: props.zones,
682
+ };
683
+ },
684
+ }),
685
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
686
+ if (!output && !olds) return undefined;
687
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
688
+ if (!rg) return undefined;
689
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
690
+ const ip = yield* azurePromise("read public IP address", name, () =>
691
+ clients.network.publicIPAddresses.get(rg, name),
692
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
693
+ return ip ? ownershipAware(id, ip, publicIpAttrs(ip, rg), !!output) : undefined;
694
+ }),
695
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
696
+ const rg = yield* resourceGroupName(news.resourceGroup);
697
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
698
+ const name = nameOf(id, instanceId, news);
699
+ const skuName = news.sku ?? "Basic";
700
+ yield* ensureTaggedOwnership(id, "public IP address", name, output, olds, () =>
701
+ clients.network.publicIPAddresses.get(rg, name)
702
+ );
703
+ const ip = yield* azurePromise("reconcile public IP address", name, () =>
704
+ clients.network.publicIPAddresses.beginCreateOrUpdateAndWait(rg, name, {
705
+ location,
706
+ sku: { name: skuName },
707
+ publicIPAllocationMethod:
708
+ news.allocationMethod ?? (skuName === "Standard" ? "Static" : "Dynamic"),
709
+ publicIPAddressVersion: news.ipVersion ?? "IPv4",
710
+ dnsSettings: news.domainNameLabel
711
+ ? { domainNameLabel: news.domainNameLabel }
712
+ : undefined,
713
+ idleTimeoutInMinutes: news.idleTimeoutInMinutes ?? 4,
714
+ zones: news.zones,
715
+ tags: withAlchemyTags(id, news.tags),
716
+ } as Parameters<
717
+ typeof clients.network.publicIPAddresses.beginCreateOrUpdateAndWait
718
+ >[2]),
719
+ );
720
+ return publicIpAttrs(ip, rg);
721
+ }),
722
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
723
+ yield* deleteIfEnabled(
724
+ () =>
725
+ clients.network.publicIPAddresses.beginDeleteAndWait(
726
+ output.resourceGroupName,
727
+ output.name,
728
+ ),
729
+ output.name,
730
+ "public IP address",
731
+ )({ olds, session });
732
+ }),
733
+ });
734
+ }),
735
+ );
736
+ function publicIpAttrs(ip: AzureResponse, resourceGroupName: string) {
737
+ const data = ip as Record<string, unknown>;
738
+ const dnsSettings = data.dnsSettings as { fqdn?: string } | undefined;
739
+ return {
740
+ name: ip.name as string,
741
+ resourceGroupName,
742
+ location: ip.location as string,
743
+ resourceId: ip.id as string,
744
+ ipAddress: data.ipAddress as string | undefined,
745
+ fqdn: dnsSettings?.fqdn,
746
+ provisioningState: ip.provisioningState,
747
+ tags: ip.tags,
748
+ } satisfies PublicIPAddress["Attributes"];
749
+ }
750
+
751
+ export interface CognitiveServicesProps extends BaseProps {
752
+ kind?: string;
753
+ sku?: string;
754
+ publicNetworkAccess?: boolean;
755
+ customSubDomain?: string;
756
+ networkAcls?: {
757
+ defaultAction?: "Allow" | "Deny";
758
+ ipRules?: string[];
759
+ virtualNetworkRules?: string[];
760
+ };
761
+ }
762
+ export type CognitiveServices = Resource<
763
+ "Azure.CognitiveServices",
764
+ CognitiveServicesProps,
765
+ Attrs<{
766
+ kind: string;
767
+ sku: string;
768
+ endpoint?: string;
769
+ primaryKey?: Redacted.Redacted<string>;
770
+ secondaryKey?: Redacted.Redacted<string>;
771
+ }>,
772
+ never,
773
+ Providers
774
+ >;
775
+ export const CognitiveServices = Resource<CognitiveServices>("Azure.CognitiveServices");
776
+ export const CognitiveServicesProvider = () =>
777
+ Provider.effect(
778
+ CognitiveServices,
779
+ Effect.gen(function* () {
780
+ const clients = yield* makeAzureClients;
781
+ const names = yield* makePhysicalNames;
782
+ const nameOf = (id: string, instanceId: string, props: CognitiveServicesProps) =>
783
+ names.physicalName(id, instanceId, props.name, { maxLength: 64 });
784
+ return CognitiveServices.Provider.of({
785
+ stables: ["name", "resourceGroupName", "resourceId"],
786
+ list: () =>
787
+ listOwnedByResourceGroup(
788
+ clients,
789
+ (rg) => clients.cognitiveServices.accounts.listByResourceGroup(rg),
790
+ cognitiveAttrs,
791
+ ),
792
+ diff: diffOnChanges({
793
+ identity: resourceGroupIdentity(nameOf),
794
+ replace: (props) => ({
795
+ kind: props.kind ?? "CognitiveServices",
796
+ customSubDomain: props.customSubDomain,
797
+ }),
798
+ mutable: (props) => ({
799
+ sku: props.sku ?? "S0",
800
+ publicNetworkAccess: props.publicNetworkAccess !== false,
801
+ networkAcls: props.networkAcls,
802
+ tags: props.tags,
803
+ }),
804
+ }),
805
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
806
+ if (!output && !olds) return undefined;
807
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
808
+ if (!rg) return undefined;
809
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
810
+ const a = yield* azurePromise("read Cognitive Services account", name, () =>
811
+ clients.cognitiveServices.accounts.get(rg, name),
812
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
813
+ return a ? ownershipAware(id, a, yield* cognitiveAttrs(a, rg), !!output) : undefined;
814
+ }),
815
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
816
+ const rg = yield* resourceGroupName(news.resourceGroup);
817
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
818
+ const name = nameOf(id, instanceId, news);
819
+ yield* ensureTaggedOwnership(id, "Cognitive Services account", name, output, olds, () =>
820
+ clients.cognitiveServices.accounts.get(rg, name)
821
+ );
822
+ const account = yield* azurePromise("reconcile Cognitive Services account", name, () =>
823
+ clients.cognitiveServices.accounts.beginCreateAndWait(rg, name, {
824
+ location,
825
+ kind: news.kind ?? "CognitiveServices",
826
+ sku: { name: news.sku ?? "S0" },
827
+ properties: {
828
+ customSubDomainName: news.customSubDomain,
829
+ publicNetworkAccess: news.publicNetworkAccess === false ? "Disabled" : "Enabled",
830
+ networkAcls: news.networkAcls
831
+ ? {
832
+ defaultAction: news.networkAcls.defaultAction ?? "Allow",
833
+ ipRules: news.networkAcls.ipRules?.map((value) => ({ value })),
834
+ virtualNetworkRules: news.networkAcls.virtualNetworkRules?.map((id) => ({
835
+ id,
836
+ })),
837
+ }
838
+ : undefined,
839
+ },
840
+ tags: withAlchemyTags(id, news.tags),
841
+ } as Parameters<typeof clients.cognitiveServices.accounts.beginCreateAndWait>[2]),
842
+ );
843
+ return yield* cognitiveAttrs(account, rg);
844
+ }),
845
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
846
+ yield* deleteIfEnabled(
847
+ () =>
848
+ clients.cognitiveServices.accounts.beginDeleteAndWait(
849
+ output.resourceGroupName,
850
+ output.name,
851
+ ),
852
+ output.name,
853
+ "Cognitive Services account",
854
+ )({ olds, session });
855
+ }),
856
+ });
857
+ function cognitiveAttrs(account: AzureResponse, resourceGroupName: string) {
858
+ return Effect.gen(function* () {
859
+ const data = account as Record<string, unknown>;
860
+ const properties = data.properties as
861
+ | { endpoint?: string; provisioningState?: string }
862
+ | undefined;
863
+ const sku = data.sku as { name?: string } | undefined;
864
+ const keys = yield* azurePromise("list Cognitive Services keys", account.name, () =>
865
+ clients.cognitiveServices.accounts.listKeys(resourceGroupName, account.name as string),
866
+ ).pipe(Effect.catch(() => Effect.succeed(undefined)));
867
+ return {
868
+ name: account.name as string,
869
+ resourceGroupName,
870
+ location: account.location as string,
871
+ resourceId: account.id as string,
872
+ kind: data.kind as string,
873
+ sku: sku?.name as string,
874
+ endpoint: properties?.endpoint,
875
+ primaryKey: redacted(keys?.key1),
876
+ secondaryKey: redacted(keys?.key2),
877
+ provisioningState: properties?.provisioningState,
878
+ tags: account.tags,
879
+ } satisfies CognitiveServices["Attributes"];
880
+ });
881
+ }
882
+ }),
883
+ );
884
+
885
+ export interface ServiceBusProps extends BaseProps {
886
+ sku?: "Basic" | "Standard" | "Premium";
887
+ capacity?: number;
888
+ zoneRedundant?: boolean;
889
+ disableLocalAuth?: boolean;
890
+ }
891
+ export type ServiceBus = Resource<
892
+ "Azure.ServiceBus",
893
+ ServiceBusProps,
894
+ Attrs<{
895
+ sku: string;
896
+ endpoint: string;
897
+ primaryConnectionString?: Redacted.Redacted<string>;
898
+ secondaryConnectionString?: Redacted.Redacted<string>;
899
+ primaryKey?: Redacted.Redacted<string>;
900
+ secondaryKey?: Redacted.Redacted<string>;
901
+ }>,
902
+ never,
903
+ Providers
904
+ >;
905
+ export const ServiceBus = Resource<ServiceBus>("Azure.ServiceBus");
906
+ export const ServiceBusProvider = () =>
907
+ Provider.effect(
908
+ ServiceBus,
909
+ Effect.gen(function* () {
910
+ const clients = yield* makeAzureClients;
911
+ const names = yield* makePhysicalNames;
912
+ const nameOf = (id: string, instanceId: string, props: ServiceBusProps) =>
913
+ names.physicalName(id, instanceId, props.name, { maxLength: 50, lowercase: true });
914
+ return ServiceBus.Provider.of({
915
+ stables: ["name", "resourceGroupName", "resourceId"],
916
+ list: () =>
917
+ listOwnedByResourceGroup(
918
+ clients,
919
+ (rg) => clients.serviceBus.namespaces.listByResourceGroup(rg),
920
+ serviceBusAttrs,
921
+ ),
922
+ diff: diffOnChanges({
923
+ identity: resourceGroupIdentity(nameOf),
924
+ replace: (props) => ({
925
+ sku: props.sku ?? "Standard",
926
+ zoneRedundant: props.zoneRedundant ?? false,
927
+ }),
928
+ mutable: (props) => {
929
+ const sku = props.sku ?? "Standard";
930
+ return {
931
+ capacity: sku === "Premium" ? (props.capacity ?? 1) : undefined,
932
+ disableLocalAuth: props.disableLocalAuth,
933
+ tags: props.tags,
934
+ };
935
+ },
936
+ }),
937
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
938
+ if (!output && !olds) return undefined;
939
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
940
+ if (!rg) return undefined;
941
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
942
+ const ns = yield* azurePromise("read Service Bus namespace", name, () =>
943
+ clients.serviceBus.namespaces.get(rg, name),
944
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
945
+ return ns ? ownershipAware(id, ns, yield* serviceBusAttrs(ns, rg), !!output) : undefined;
946
+ }),
947
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
948
+ const rg = yield* resourceGroupName(news.resourceGroup);
949
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
950
+ const name = nameOf(id, instanceId, news);
951
+ const sku = news.sku ?? "Standard";
952
+ yield* ensureTaggedOwnership(id, "Service Bus namespace", name, output, olds, () =>
953
+ clients.serviceBus.namespaces.get(rg, name)
954
+ );
955
+ const ns = yield* azurePromise("reconcile Service Bus namespace", name, () =>
956
+ clients.serviceBus.namespaces.beginCreateOrUpdateAndWait(rg, name, {
957
+ location,
958
+ sku: {
959
+ name: sku,
960
+ tier: sku,
961
+ capacity: sku === "Premium" ? (news.capacity ?? 1) : undefined,
962
+ },
963
+ zoneRedundant: news.zoneRedundant,
964
+ disableLocalAuth: news.disableLocalAuth,
965
+ tags: withAlchemyTags(id, news.tags),
966
+ } as Parameters<typeof clients.serviceBus.namespaces.beginCreateOrUpdateAndWait>[2]),
967
+ );
968
+ return yield* serviceBusAttrs(ns, rg);
969
+ }),
970
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
971
+ yield* deleteIfEnabled(
972
+ () =>
973
+ clients.serviceBus.namespaces.beginDeleteAndWait(
974
+ output.resourceGroupName,
975
+ output.name,
976
+ ),
977
+ output.name,
978
+ "Service Bus namespace",
979
+ )({ olds, session });
980
+ }),
981
+ });
982
+ function serviceBusAttrs(ns: AzureResponse, resourceGroupName: string) {
983
+ return Effect.gen(function* () {
984
+ const data = ns as Record<string, unknown>;
985
+ const sku = data.sku as { name?: string } | undefined;
986
+ const keys = yield* azurePromise("list Service Bus keys", ns.name, () =>
987
+ clients.serviceBus.namespaces.listKeys(
988
+ resourceGroupName,
989
+ ns.name as string,
990
+ "RootManageSharedAccessKey",
991
+ ),
992
+ ).pipe(Effect.catch(() => Effect.succeed(undefined)));
993
+ return {
994
+ name: ns.name as string,
995
+ resourceGroupName,
996
+ location: ns.location as string,
997
+ resourceId: ns.id as string,
998
+ sku: sku?.name as string,
999
+ endpoint: `https://${ns.name}.servicebus.windows.net`,
1000
+ primaryConnectionString: redacted(keys?.primaryConnectionString),
1001
+ secondaryConnectionString: redacted(keys?.secondaryConnectionString),
1002
+ primaryKey: redacted(keys?.primaryKey),
1003
+ secondaryKey: redacted(keys?.secondaryKey),
1004
+ provisioningState: ns.provisioningState,
1005
+ tags: ns.tags,
1006
+ } satisfies ServiceBus["Attributes"];
1007
+ });
1008
+ }
1009
+ }),
1010
+ );
1011
+
1012
+ export interface CosmosDBAccountProps extends BaseProps {
1013
+ kind?: string;
1014
+ defaultConsistencyLevel?: string;
1015
+ enableFreeTier?: boolean;
1016
+ locations?: Array<{ locationName: string; failoverPriority?: number; isZoneRedundant?: boolean }>;
1017
+ }
1018
+ export type CosmosDBAccount = Resource<
1019
+ "Azure.CosmosDBAccount",
1020
+ CosmosDBAccountProps,
1021
+ Attrs<{
1022
+ endpoint?: string;
1023
+ primaryKey?: Redacted.Redacted<string>;
1024
+ connectionString?: Redacted.Redacted<string>;
1025
+ kind?: string;
1026
+ }>,
1027
+ never,
1028
+ Providers
1029
+ >;
1030
+ export const CosmosDBAccount = Resource<CosmosDBAccount>("Azure.CosmosDBAccount");
1031
+ export const CosmosDBAccountProvider = () =>
1032
+ Provider.effect(
1033
+ CosmosDBAccount,
1034
+ Effect.gen(function* () {
1035
+ const clients = yield* makeAzureClients;
1036
+ const names = yield* makePhysicalNames;
1037
+ const nameOf = (id: string, instanceId: string, props: CosmosDBAccountProps) =>
1038
+ names.physicalName(id, instanceId, props.name, { maxLength: 44, lowercase: true });
1039
+ return CosmosDBAccount.Provider.of({
1040
+ stables: ["name", "resourceGroupName", "resourceId"],
1041
+ list: () =>
1042
+ listOwnedByResourceGroup(
1043
+ clients,
1044
+ (rg) => clients.cosmosDB.databaseAccounts.listByResourceGroup(rg),
1045
+ cosmosAttrs,
1046
+ ),
1047
+ diff: diffOnChanges({
1048
+ identity: resourceGroupIdentity(nameOf),
1049
+ replace: (props) => ({
1050
+ kind: props.kind ?? "GlobalDocumentDB",
1051
+ enableFreeTier: props.enableFreeTier ?? false,
1052
+ }),
1053
+ mutable: (props) => ({
1054
+ defaultConsistencyLevel: props.defaultConsistencyLevel ?? "Session",
1055
+ locations: props.locations,
1056
+ tags: props.tags,
1057
+ }),
1058
+ }),
1059
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1060
+ if (!output && !olds) return undefined;
1061
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1062
+ if (!rg) return undefined;
1063
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1064
+ const account = yield* azurePromise("read Cosmos DB account", name, () =>
1065
+ clients.cosmosDB.databaseAccounts.get(rg, name),
1066
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
1067
+ return account
1068
+ ? ownershipAware(id, account, yield* cosmosAttrs(account, rg), !!output)
1069
+ : undefined;
1070
+ }),
1071
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1072
+ const rg = yield* resourceGroupName(news.resourceGroup);
1073
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1074
+ const name = nameOf(id, instanceId, news);
1075
+ yield* ensureTaggedOwnership(id, "Cosmos DB account", name, output, olds, () =>
1076
+ clients.cosmosDB.databaseAccounts.get(rg, name)
1077
+ );
1078
+ const account = yield* azurePromise("reconcile Cosmos DB account", name, async () => {
1079
+ const poller = await clients.cosmosDB.databaseAccounts.beginCreateOrUpdate(rg, name, {
1080
+ location,
1081
+ kind: news.kind ?? "GlobalDocumentDB",
1082
+ databaseAccountOfferType: "Standard",
1083
+ consistencyPolicy: {
1084
+ defaultConsistencyLevel: news.defaultConsistencyLevel ?? "Session",
1085
+ },
1086
+ enableFreeTier: news.enableFreeTier,
1087
+ locations: news.locations ?? [{ locationName: location, failoverPriority: 0 }],
1088
+ tags: withAlchemyTags(id, news.tags),
1089
+ } as Parameters<typeof clients.cosmosDB.databaseAccounts.beginCreateOrUpdate>[2]);
1090
+ await poller.pollUntilFinished();
1091
+ return clients.cosmosDB.databaseAccounts.get(rg, name);
1092
+ }).pipe(withHeartbeat(`Cosmos DB account "${name}"`));
1093
+ return yield* cosmosAttrs(account, rg);
1094
+ }),
1095
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1096
+ yield* deleteIfEnabled(
1097
+ async () => {
1098
+ const poller = await clients.cosmosDB.databaseAccounts.beginDeleteMethod(
1099
+ output.resourceGroupName,
1100
+ output.name,
1101
+ );
1102
+ return poller.pollUntilFinished();
1103
+ },
1104
+ output.name,
1105
+ "Cosmos DB account",
1106
+ `deleting Cosmos DB account "${output.name}"`,
1107
+ )({ olds, session });
1108
+ }),
1109
+ });
1110
+ function cosmosAttrs(account: AzureResponse, resourceGroupName: string) {
1111
+ return Effect.gen(function* () {
1112
+ const data = account as Record<string, unknown>;
1113
+ const keys = yield* azurePromise("list Cosmos DB keys", account.name, () =>
1114
+ clients.cosmosDB.databaseAccounts.listKeys(resourceGroupName, account.name as string),
1115
+ ).pipe(Effect.catch(() => Effect.succeed(undefined)));
1116
+ const primaryKey = keys?.primaryMasterKey;
1117
+ const endpoint = data.documentEndpoint as string | undefined;
1118
+ return {
1119
+ name: account.name as string,
1120
+ resourceGroupName,
1121
+ location: account.location as string,
1122
+ resourceId: account.id as string,
1123
+ endpoint,
1124
+ primaryKey: redacted(primaryKey),
1125
+ connectionString: primaryKey
1126
+ ? redacted(`AccountEndpoint=${endpoint};AccountKey=${primaryKey};`)
1127
+ : undefined,
1128
+ kind: data.kind as string | undefined,
1129
+ provisioningState: account.provisioningState,
1130
+ tags: account.tags,
1131
+ } satisfies CosmosDBAccount["Attributes"];
1132
+ });
1133
+ }
1134
+ }),
1135
+ );
1136
+
1137
+ export interface SqlServerProps extends BaseProps {
1138
+ administratorLogin: string;
1139
+ administratorLoginPassword: string | Redacted.Redacted<string>;
1140
+ version?: string;
1141
+ publicNetworkAccess?: boolean;
1142
+ }
1143
+ export type SqlServer = Resource<
1144
+ "Azure.SqlServer",
1145
+ SqlServerProps,
1146
+ Attrs<{ fullyQualifiedDomainName?: string; administratorLogin: string }>,
1147
+ never,
1148
+ Providers
1149
+ >;
1150
+ export const SqlServer = Resource<SqlServer>("Azure.SqlServer");
1151
+ export const SqlServerProvider = () =>
1152
+ Provider.effect(
1153
+ SqlServer,
1154
+ Effect.gen(function* () {
1155
+ const clients = yield* makeAzureClients;
1156
+ const names = yield* makePhysicalNames;
1157
+ const nameOf = (id: string, instanceId: string, props: SqlServerProps) =>
1158
+ names.physicalName(id, instanceId, props.name, { maxLength: 63, lowercase: true });
1159
+ return SqlServer.Provider.of({
1160
+ stables: ["name", "resourceGroupName", "resourceId"],
1161
+ list: () =>
1162
+ listOwnedByResourceGroup(
1163
+ clients,
1164
+ (rg) => clients.sql.servers.listByResourceGroup(rg),
1165
+ (server, rg) => sqlServerAttrs(server, rg, (server as Record<string, unknown>).administratorLogin as string),
1166
+ ),
1167
+ diff: diffOnChanges({
1168
+ identity: resourceGroupIdentity(nameOf),
1169
+ replace: (props) => ({
1170
+ administratorLogin: props.administratorLogin,
1171
+ version: props.version ?? "12.0",
1172
+ }),
1173
+ mutable: (props) => ({
1174
+ administratorLoginPassword: props.administratorLoginPassword,
1175
+ publicNetworkAccess: props.publicNetworkAccess !== false,
1176
+ tags: props.tags,
1177
+ }),
1178
+ }),
1179
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1180
+ if (!output && !olds) return undefined;
1181
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1182
+ if (!rg) return undefined;
1183
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1184
+ const server = yield* azurePromise("read SQL server", name, () => clients.sql.servers.get(rg, name)).pipe(
1185
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
1186
+ );
1187
+ const administratorLogin = olds?.administratorLogin ?? server?.administratorLogin;
1188
+ return server
1189
+ ? ownershipAware(id, server, sqlServerAttrs(server, rg, administratorLogin), !!output)
1190
+ : undefined;
1191
+ }),
1192
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1193
+ const rg = yield* resourceGroupName(news.resourceGroup);
1194
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1195
+ const name = nameOf(id, instanceId, news);
1196
+ yield* ensureTaggedOwnership(id, "SQL server", name, output, olds, () =>
1197
+ clients.sql.servers.get(rg, name)
1198
+ );
1199
+ const server = yield* azurePromise("reconcile SQL server", name, () =>
1200
+ clients.sql.servers.beginCreateOrUpdateAndWait(rg, name, {
1201
+ location,
1202
+ administratorLogin: news.administratorLogin,
1203
+ administratorLoginPassword:
1204
+ typeof news.administratorLoginPassword === "string"
1205
+ ? news.administratorLoginPassword
1206
+ : Redacted.value(news.administratorLoginPassword),
1207
+ version: news.version ?? "12.0",
1208
+ publicNetworkAccess: news.publicNetworkAccess === false ? "Disabled" : "Enabled",
1209
+ tags: withAlchemyTags(id, news.tags),
1210
+ } as Parameters<typeof clients.sql.servers.beginCreateOrUpdateAndWait>[2]),
1211
+ ).pipe(withHeartbeat(`SQL server "${name}"`));
1212
+ return sqlServerAttrs(server, rg, news.administratorLogin);
1213
+ }),
1214
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1215
+ yield* deleteIfEnabled(
1216
+ () => clients.sql.servers.beginDeleteAndWait(output.resourceGroupName, output.name),
1217
+ output.name,
1218
+ "SQL server",
1219
+ )({ olds, session });
1220
+ }),
1221
+ });
1222
+ }),
1223
+ );
1224
+ function sqlServerAttrs(
1225
+ server: AzureResponse,
1226
+ resourceGroupName: string,
1227
+ administratorLogin: string,
1228
+ ) {
1229
+ const data = server as Record<string, unknown>;
1230
+ return {
1231
+ name: server.name as string,
1232
+ resourceGroupName,
1233
+ location: server.location as string,
1234
+ resourceId: server.id as string,
1235
+ fullyQualifiedDomainName: data.fullyQualifiedDomainName as string | undefined,
1236
+ administratorLogin,
1237
+ provisioningState: data.state as string | undefined,
1238
+ tags: server.tags,
1239
+ } satisfies SqlServer["Attributes"];
1240
+ }
1241
+
1242
+ export interface SqlDatabaseProps extends BaseProps {
1243
+ server: string | SqlServer;
1244
+ sku?: string;
1245
+ tier?: string;
1246
+ maxSizeBytes?: number;
1247
+ collation?: string;
1248
+ }
1249
+ export type SqlDatabase = Resource<
1250
+ "Azure.SqlDatabase",
1251
+ SqlDatabaseProps,
1252
+ Attrs<{ serverName: string; sku?: string; maxSizeBytes?: number }>,
1253
+ never,
1254
+ Providers
1255
+ >;
1256
+ export const SqlDatabase = Resource<SqlDatabase>("Azure.SqlDatabase");
1257
+ export const SqlDatabaseProvider = () =>
1258
+ Provider.effect(
1259
+ SqlDatabase,
1260
+ Effect.gen(function* () {
1261
+ const clients = yield* makeAzureClients;
1262
+ const names = yield* makePhysicalNames;
1263
+ const nameOf = (id: string, instanceId: string, props: SqlDatabaseProps) =>
1264
+ names.physicalName(id, instanceId, props.name, { maxLength: 128 });
1265
+ const serverName = (server: string | SqlServer) =>
1266
+ typeof server === "string"
1267
+ ? Effect.succeed(server)
1268
+ : resolveResourceValue(server.name);
1269
+ return SqlDatabase.Provider.of({
1270
+ stables: ["name", "resourceGroupName", "resourceId", "serverName"],
1271
+ list: () =>
1272
+ Effect.gen(function* () {
1273
+ const servers = yield* listByResourceGroup<AzureResponse>(
1274
+ clients,
1275
+ (rg) => clients.sql.servers.listByResourceGroup(rg),
1276
+ );
1277
+ const databases = yield* Effect.forEach(
1278
+ servers,
1279
+ ([resourceGroupName, server]) =>
1280
+ azurePromise("list SQL databases", server.name, () =>
1281
+ collectAzurePages(
1282
+ clients.sql.databases.listByServer(resourceGroupName, server.name as string) as never,
1283
+ ),
1284
+ ).pipe(
1285
+ Effect.map((items) =>
1286
+ items.map(
1287
+ (database) =>
1288
+ [resourceGroupName, server.name as string, database as AzureResponse] as const,
1289
+ ),
1290
+ ),
1291
+ ),
1292
+ { concurrency: 4 },
1293
+ );
1294
+ return databases
1295
+ .flat()
1296
+ .filter(([, , database]) => database.tags?.["alchemy:logical-id"])
1297
+ .map(([rg, serverName, database]) => sqlDbAttrs(database as AzureResponse, rg, serverName));
1298
+ }),
1299
+ diff: diffOnChanges({
1300
+ identity: ({ id, instanceId, props }) =>
1301
+ Effect.gen(function* () {
1302
+ return {
1303
+ name: nameOf(id, instanceId, props),
1304
+ resourceGroupName: yield* resourceGroupName(props.resourceGroup),
1305
+ location: yield* requireLocation(id, props.location, props.resourceGroup),
1306
+ serverName: yield* serverName(props.server),
1307
+ };
1308
+ }),
1309
+ replace: (props) => ({ collation: props.collation }),
1310
+ mutable: (props) => ({
1311
+ sku: props.sku,
1312
+ tier: props.tier,
1313
+ maxSizeBytes: props.maxSizeBytes,
1314
+ tags: props.tags,
1315
+ }),
1316
+ }),
1317
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1318
+ if (!output && !olds) return undefined;
1319
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1320
+ const sn = output?.serverName ?? (yield* serverName(olds!.server));
1321
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1322
+ const db = yield* azurePromise("read SQL database", name, () => clients.sql.databases.get(rg, sn, name)).pipe(
1323
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
1324
+ );
1325
+ return db ? ownershipAware(id, db, sqlDbAttrs(db, rg, sn), !!output) : undefined;
1326
+ }),
1327
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1328
+ const rg = yield* resourceGroupName(news.resourceGroup);
1329
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1330
+ const sn = yield* serverName(news.server);
1331
+ const name = nameOf(id, instanceId, news);
1332
+ yield* ensureTaggedOwnership(id, "SQL database", name, output, olds, () =>
1333
+ clients.sql.databases.get(rg, sn, name)
1334
+ );
1335
+ const db = yield* azurePromise("reconcile SQL database", name, () =>
1336
+ clients.sql.databases.beginCreateOrUpdateAndWait(rg, sn, name, {
1337
+ location,
1338
+ sku: news.sku ? { name: news.sku, tier: news.tier } : undefined,
1339
+ maxSizeBytes: news.maxSizeBytes,
1340
+ collation: news.collation,
1341
+ tags: withAlchemyTags(id, news.tags),
1342
+ } as Parameters<typeof clients.sql.databases.beginCreateOrUpdateAndWait>[3]),
1343
+ );
1344
+ return sqlDbAttrs(db, rg, sn);
1345
+ }),
1346
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1347
+ yield* deleteIfEnabled(
1348
+ () =>
1349
+ clients.sql.databases.beginDeleteAndWait(
1350
+ output.resourceGroupName,
1351
+ output.serverName,
1352
+ output.name,
1353
+ ),
1354
+ output.name,
1355
+ "SQL database",
1356
+ )({ olds, session });
1357
+ }),
1358
+ });
1359
+ }),
1360
+ );
1361
+ function sqlDbAttrs(db: AzureResponse, resourceGroupName: string, serverName: string) {
1362
+ const data = db as Record<string, unknown>;
1363
+ const sku = data.sku as { name?: string } | undefined;
1364
+ return {
1365
+ name: db.name as string,
1366
+ resourceGroupName,
1367
+ location: db.location as string,
1368
+ resourceId: db.id as string,
1369
+ serverName,
1370
+ sku: sku?.name,
1371
+ maxSizeBytes: data.maxSizeBytes as number | undefined,
1372
+ provisioningState: data.status as string | undefined,
1373
+ tags: db.tags,
1374
+ } satisfies SqlDatabase["Attributes"];
1375
+ }
1376
+
1377
+ export interface KeyVaultProps extends BaseProps {
1378
+ tenantId?: string;
1379
+ sku?: "standard" | "premium";
1380
+ accessPolicies?: unknown[];
1381
+ enableRbacAuthorization?: boolean;
1382
+ enableSoftDelete?: boolean;
1383
+ softDeleteRetentionInDays?: number;
1384
+ publicNetworkAccess?: boolean;
1385
+ }
1386
+ export type KeyVault = Resource<
1387
+ "Azure.KeyVault",
1388
+ KeyVaultProps,
1389
+ Attrs<{ vaultUri?: string; sku?: string }>,
1390
+ never,
1391
+ Providers
1392
+ >;
1393
+ export const KeyVault = Resource<KeyVault>("Azure.KeyVault");
1394
+ export const KeyVaultProvider = () =>
1395
+ Provider.effect(
1396
+ KeyVault,
1397
+ Effect.gen(function* () {
1398
+ const clients = yield* makeAzureClients;
1399
+ const names = yield* makePhysicalNames;
1400
+ const nameOf = (id: string, instanceId: string, props: KeyVaultProps) =>
1401
+ names.physicalName(id, instanceId, props.name, { maxLength: 24, lowercase: true });
1402
+ return KeyVault.Provider.of({
1403
+ stables: ["name", "resourceGroupName", "resourceId"],
1404
+ list: () =>
1405
+ listOwnedByResourceGroup(
1406
+ clients,
1407
+ (rg) => clients.keyVault.vaults.listByResourceGroup(rg),
1408
+ keyVaultAttrs,
1409
+ ),
1410
+ diff: diffOnChanges({
1411
+ identity: resourceGroupIdentity(nameOf),
1412
+ replace: (props) => ({
1413
+ sku: props.sku ?? "standard",
1414
+ enableSoftDelete: props.enableSoftDelete ?? true,
1415
+ softDeleteRetentionInDays: props.softDeleteRetentionInDays ?? 90,
1416
+ }),
1417
+ replaceChanged: (olds, news) => olds.tenantId !== news.tenantId,
1418
+ mutable: (props) => ({
1419
+ accessPolicies: props.accessPolicies ?? [],
1420
+ enableRbacAuthorization: props.enableRbacAuthorization,
1421
+ publicNetworkAccess: props.publicNetworkAccess !== false,
1422
+ tags: props.tags,
1423
+ }),
1424
+ }),
1425
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1426
+ if (!output && !olds) return undefined;
1427
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1428
+ if (!rg) return undefined;
1429
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1430
+ const vault = yield* azurePromise("read Key Vault", name, () => clients.keyVault.vaults.get(rg, name)).pipe(
1431
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
1432
+ );
1433
+ return vault ? ownershipAware(id, vault, keyVaultAttrs(vault, rg), !!output) : undefined;
1434
+ }),
1435
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1436
+ const rg = yield* resourceGroupName(news.resourceGroup);
1437
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1438
+ const name = nameOf(id, instanceId, news);
1439
+ const tenantId = news.tenantId ?? clients.tenantId;
1440
+ if (!tenantId) throw new Error(`KeyVault ${id} requires tenantId.`);
1441
+ yield* ensureTaggedOwnership(id, "Key Vault", name, output, olds, () =>
1442
+ clients.keyVault.vaults.get(rg, name)
1443
+ );
1444
+ const vault = yield* azurePromise("reconcile Key Vault", name, () =>
1445
+ clients.keyVault.vaults.beginCreateOrUpdateAndWait(rg, name, {
1446
+ location,
1447
+ properties: {
1448
+ tenantId,
1449
+ sku: { family: "A", name: news.sku ?? "standard" },
1450
+ accessPolicies: news.accessPolicies ?? [],
1451
+ enableRbacAuthorization: news.enableRbacAuthorization,
1452
+ enableSoftDelete: news.enableSoftDelete ?? true,
1453
+ softDeleteRetentionInDays: news.softDeleteRetentionInDays,
1454
+ publicNetworkAccess: news.publicNetworkAccess === false ? "Disabled" : "Enabled",
1455
+ },
1456
+ tags: withAlchemyTags(id, news.tags),
1457
+ } as Parameters<typeof clients.keyVault.vaults.beginCreateOrUpdateAndWait>[2]),
1458
+ );
1459
+ return keyVaultAttrs(vault, rg);
1460
+ }),
1461
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1462
+ yield* deleteIfEnabled(
1463
+ () => clients.keyVault.vaults.delete(output.resourceGroupName, output.name),
1464
+ output.name,
1465
+ "Key Vault",
1466
+ )({ olds, session });
1467
+ }),
1468
+ });
1469
+ }),
1470
+ );
1471
+ function keyVaultAttrs(vault: AzureResponse, resourceGroupName: string) {
1472
+ const data = vault as Record<string, unknown>;
1473
+ const properties = data.properties as
1474
+ | { provisioningState?: string; sku?: { name?: string }; vaultUri?: string }
1475
+ | undefined;
1476
+ return {
1477
+ name: vault.name as string,
1478
+ resourceGroupName,
1479
+ location: vault.location as string,
1480
+ resourceId: vault.id as string,
1481
+ vaultUri: properties?.vaultUri,
1482
+ sku: properties?.sku?.name,
1483
+ provisioningState: properties?.provisioningState,
1484
+ tags: vault.tags,
1485
+ } satisfies KeyVault["Attributes"];
1486
+ }
1487
+
1488
+ export interface AppServicePlanProps extends BaseProps {
1489
+ sku?: string;
1490
+ tier?: string;
1491
+ capacity?: number;
1492
+ reserved?: boolean;
1493
+ kind?: string;
1494
+ }
1495
+ export type AppServicePlan = Resource<
1496
+ "Azure.AppServicePlan",
1497
+ AppServicePlanProps,
1498
+ Attrs<{ serverFarmId: string; sku?: string; tier?: string; capacity?: number; reserved?: boolean }>,
1499
+ never,
1500
+ Providers
1501
+ >;
1502
+ export const AppServicePlan = Resource<AppServicePlan>("Azure.AppServicePlan");
1503
+ export const AppServicePlanProvider = () =>
1504
+ Provider.effect(
1505
+ AppServicePlan,
1506
+ Effect.gen(function* () {
1507
+ const clients = yield* makeAzureClients;
1508
+ const names = yield* makePhysicalNames;
1509
+ const nameOf = (id: string, instanceId: string, props: AppServicePlanProps) =>
1510
+ names.physicalName(id, instanceId, props.name, { maxLength: 40, lowercase: true });
1511
+ return AppServicePlan.Provider.of({
1512
+ stables: ["name", "resourceGroupName", "resourceId", "serverFarmId"],
1513
+ list: () =>
1514
+ listOwnedByResourceGroup(
1515
+ clients,
1516
+ (rg) => clients.appService.appServicePlans.listByResourceGroup(rg),
1517
+ appServicePlanAttrs,
1518
+ ),
1519
+ diff: diffOnChanges({
1520
+ identity: resourceGroupIdentity(nameOf),
1521
+ replace: (props) => ({
1522
+ reserved: props.reserved ?? false,
1523
+ kind: props.kind,
1524
+ }),
1525
+ mutable: (props) => {
1526
+ const sku = props.sku ?? "B1";
1527
+ return {
1528
+ sku,
1529
+ tier: props.tier ?? skuTier(sku),
1530
+ capacity: props.capacity ?? 1,
1531
+ tags: props.tags,
1532
+ };
1533
+ },
1534
+ }),
1535
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1536
+ if (!output && !olds) return undefined;
1537
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1538
+ if (!rg) return undefined;
1539
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1540
+ const plan = yield* azurePromise("read App Service plan", name, () => clients.appService.appServicePlans.get(rg, name)).pipe(
1541
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
1542
+ );
1543
+ return plan ? ownershipAware(id, plan, appServicePlanAttrs(plan, rg), !!output) : undefined;
1544
+ }),
1545
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1546
+ const rg = yield* resourceGroupName(news.resourceGroup);
1547
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1548
+ const name = nameOf(id, instanceId, news);
1549
+ const sku = news.sku ?? "B1";
1550
+ yield* ensureTaggedOwnership(id, "App Service plan", name, output, olds, () =>
1551
+ clients.appService.appServicePlans.get(rg, name)
1552
+ );
1553
+ const plan = yield* azurePromise("reconcile App Service plan", name, () =>
1554
+ clients.appService.appServicePlans.beginCreateOrUpdateAndWait(rg, name, {
1555
+ location,
1556
+ kind: news.kind,
1557
+ reserved: news.reserved ?? false,
1558
+ sku: {
1559
+ name: sku,
1560
+ tier: news.tier ?? skuTier(sku),
1561
+ capacity: news.capacity ?? 1,
1562
+ },
1563
+ tags: withAlchemyTags(id, news.tags),
1564
+ } as Parameters<typeof clients.appService.appServicePlans.beginCreateOrUpdateAndWait>[2]),
1565
+ );
1566
+ return appServicePlanAttrs(plan, rg);
1567
+ }),
1568
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1569
+ yield* deleteIfEnabled(
1570
+ () => clients.appService.appServicePlans.delete(output.resourceGroupName, output.name),
1571
+ output.name,
1572
+ "App Service plan",
1573
+ )({ olds, session });
1574
+ }),
1575
+ });
1576
+ }),
1577
+ );
1578
+
1579
+ function skuTier(sku: string) {
1580
+ if (sku.startsWith("F")) return "Free";
1581
+ if (sku.startsWith("B")) return "Basic";
1582
+ if (sku.startsWith("S")) return "Standard";
1583
+ if (sku.startsWith("P")) return "PremiumV3";
1584
+ if (sku.startsWith("Y")) return "Dynamic";
1585
+ return undefined;
1586
+ }
1587
+
1588
+ function appServicePlanAttrs(plan: AzureResponse, resourceGroupName: string) {
1589
+ const data = plan as Record<string, unknown>;
1590
+ const sku = data.sku as { name?: string; tier?: string; capacity?: number } | undefined;
1591
+ return {
1592
+ name: plan.name as string,
1593
+ resourceGroupName,
1594
+ location: plan.location as string,
1595
+ resourceId: plan.id as string,
1596
+ serverFarmId: plan.id as string,
1597
+ sku: sku?.name,
1598
+ tier: sku?.tier,
1599
+ capacity: sku?.capacity,
1600
+ reserved: data.reserved as boolean | undefined,
1601
+ provisioningState: data.provisioningState as string | undefined,
1602
+ tags: plan.tags,
1603
+ } satisfies AppServicePlan["Attributes"];
1604
+ }
1605
+
1606
+ export interface AppServiceProps extends BaseProps {
1607
+ serverFarmId: string | AppServicePlan;
1608
+ httpsOnly?: boolean;
1609
+ appSettings?: Record<string, string>;
1610
+ kind?: string;
1611
+ }
1612
+ export type AppService = Resource<
1613
+ "Azure.AppService",
1614
+ AppServiceProps,
1615
+ Attrs<{ defaultHostName?: string; url?: string }>,
1616
+ never,
1617
+ Providers
1618
+ >;
1619
+ export const AppService = Resource<AppService>("Azure.AppService");
1620
+ export const AppServiceProvider = () => webAppProvider(AppService, "Azure.AppService", false);
1621
+
1622
+ export interface FunctionAppProps extends AppServiceProps {
1623
+ storageAccount: string | StorageAccount;
1624
+ functionsVersion?: string;
1625
+ }
1626
+ export type FunctionApp = Resource<
1627
+ "Azure.FunctionApp",
1628
+ FunctionAppProps,
1629
+ Attrs<{ defaultHostName?: string; url?: string }>,
1630
+ never,
1631
+ Providers
1632
+ >;
1633
+ export const FunctionApp = Resource<FunctionApp>("Azure.FunctionApp");
1634
+ export const FunctionAppProvider = () => webAppProvider(FunctionApp, "Azure.FunctionApp", true);
1635
+
1636
+ function webAppProvider(
1637
+ resource: typeof AppService | typeof FunctionApp,
1638
+ type: string,
1639
+ isFunction: boolean,
1640
+ ) {
1641
+ const webAppResource = resource as ResourceClass<AppService | FunctionApp>;
1642
+ return Provider.effect(
1643
+ webAppResource,
1644
+ Effect.gen(function* () {
1645
+ const clients = yield* makeAzureClients;
1646
+ const names = yield* makePhysicalNames;
1647
+ const nameOf = (id: string, instanceId: string, props: AppServiceProps) =>
1648
+ names.physicalName(id, instanceId, props.name, { maxLength: 60, lowercase: true });
1649
+ return webAppResource.Provider.of({
1650
+ stables: ["name", "resourceGroupName", "resourceId"],
1651
+ list: () =>
1652
+ listByResourceGroup<AzureResponse>(
1653
+ clients,
1654
+ (rg) => clients.appService.webApps.listByResourceGroup(rg),
1655
+ ).pipe(
1656
+ Effect.map((apps) =>
1657
+ apps
1658
+ .filter(([, app]) => app.tags?.["alchemy:logical-id"])
1659
+ .filter(([, app]) => {
1660
+ const kind = (app as Record<string, unknown>).kind as string | undefined;
1661
+ return isFunction ? kind?.includes("functionapp") : !kind?.includes("functionapp");
1662
+ })
1663
+ .map(([rg, app]) => webAppAttrs(app, rg)),
1664
+ ),
1665
+ ),
1666
+ diff: diffOnChanges({
1667
+ identity: ({ id, instanceId, props }) =>
1668
+ Effect.gen(function* () {
1669
+ return {
1670
+ name: nameOf(id, instanceId, props),
1671
+ resourceGroupName: yield* resourceGroupName(props.resourceGroup),
1672
+ location: yield* requireLocation(id, props.location, props.resourceGroup),
1673
+ };
1674
+ }),
1675
+ replace: (props) => ({
1676
+ serverFarmId: props.serverFarmId,
1677
+ kind: props.kind ?? (isFunction ? "functionapp" : "app"),
1678
+ }),
1679
+ mutable: (props) => ({
1680
+ httpsOnly: props.httpsOnly ?? true,
1681
+ appSettings: props.appSettings ?? {},
1682
+ storageAccount: isFunction ? (props as FunctionAppProps).storageAccount : undefined,
1683
+ functionsVersion: isFunction ? ((props as FunctionAppProps).functionsVersion ?? "~4") : undefined,
1684
+ tags: props.tags,
1685
+ }),
1686
+ }),
1687
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1688
+ if (!output && !olds) return undefined;
1689
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1690
+ if (!rg) return undefined;
1691
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1692
+ const app = yield* azurePromise("read web app", name, () => clients.appService.webApps.get(rg, name)).pipe(
1693
+ Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
1694
+ );
1695
+ return app ? ownershipAware(id, app, webAppAttrs(app, rg), !!output) : undefined;
1696
+ }),
1697
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1698
+ const rg = yield* resourceGroupName(news.resourceGroup);
1699
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1700
+ const name = nameOf(id, instanceId, news);
1701
+ const serverFarmId = yield* resolveServerFarmId(news.serverFarmId);
1702
+ yield* ensureTaggedOwnership(id, type, name, output, olds, () =>
1703
+ clients.appService.webApps.get(rg, name)
1704
+ );
1705
+ const appSettings = { ...news.appSettings };
1706
+ if (isFunction) {
1707
+ const fn = news as FunctionAppProps;
1708
+ const storageConnectionString = yield* resolveStorageConnectionString(fn.storageAccount);
1709
+ appSettings.AzureWebJobsStorage = storageConnectionString;
1710
+ appSettings.WEBSITE_CONTENTAZUREFILECONNECTIONSTRING = storageConnectionString;
1711
+ appSettings.FUNCTIONS_EXTENSION_VERSION = fn.functionsVersion ?? "~4";
1712
+ appSettings.FUNCTIONS_WORKER_RUNTIME = appSettings.FUNCTIONS_WORKER_RUNTIME ?? "node";
1713
+ }
1714
+ const app = yield* azurePromise("reconcile web app", name, () =>
1715
+ clients.appService.webApps.beginCreateOrUpdateAndWait(rg, name, {
1716
+ location,
1717
+ serverFarmId,
1718
+ httpsOnly: news.httpsOnly ?? true,
1719
+ kind: news.kind ?? (isFunction ? "functionapp" : "app"),
1720
+ siteConfig: {
1721
+ appSettings: Object.entries(appSettings).map(([name, value]) => ({
1722
+ name,
1723
+ value,
1724
+ })),
1725
+ },
1726
+ tags: withAlchemyTags(id, news.tags),
1727
+ } as Parameters<typeof clients.appService.webApps.beginCreateOrUpdateAndWait>[2]),
1728
+ );
1729
+ return webAppAttrs(app, rg);
1730
+ }),
1731
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1732
+ yield* deleteIfEnabled(
1733
+ () => clients.appService.webApps.delete(output.resourceGroupName, output.name),
1734
+ output.name,
1735
+ type,
1736
+ )({ olds, session });
1737
+ }),
1738
+ });
1739
+ }),
1740
+ );
1741
+ }
1742
+
1743
+ function resolveServerFarmId(serverFarmId: string | AppServicePlan) {
1744
+ return typeof serverFarmId === "string"
1745
+ ? Effect.succeed(serverFarmId)
1746
+ : resolveResourceValue(serverFarmId.serverFarmId);
1747
+ }
1748
+
1749
+ function resolveStorageConnectionString(storageAccount: string | StorageAccount): Effect.Effect<string, unknown, unknown> {
1750
+ if (typeof storageAccount === "string") return Effect.succeed(storageAccount);
1751
+ return Effect.gen(function* () {
1752
+ const connectionString = yield* resolveResourceValue(storageAccount.primaryConnectionString);
1753
+ return Redacted.value(connectionString);
1754
+ });
1755
+ }
1756
+ function webAppAttrs(app: AzureResponse, resourceGroupName: string) {
1757
+ const data = app as Record<string, unknown>;
1758
+ return {
1759
+ name: app.name as string,
1760
+ resourceGroupName,
1761
+ location: app.location as string,
1762
+ resourceId: app.id as string,
1763
+ defaultHostName: data.defaultHostName as string | undefined,
1764
+ url: data.defaultHostName ? `https://${data.defaultHostName}` : undefined,
1765
+ provisioningState: data.state as string | undefined,
1766
+ tags: app.tags,
1767
+ };
1768
+ }
1769
+
1770
+ export interface StaticWebAppProps extends BaseProps {
1771
+ sku?: string;
1772
+ repositoryUrl?: string;
1773
+ branch?: string;
1774
+ appLocation?: string;
1775
+ apiLocation?: string;
1776
+ outputLocation?: string;
1777
+ appSettings?: Record<string, string>;
1778
+ }
1779
+ export type StaticWebApp = Resource<
1780
+ "Azure.StaticWebApp",
1781
+ StaticWebAppProps,
1782
+ Attrs<{ defaultHostname?: string; url?: string }>,
1783
+ never,
1784
+ Providers
1785
+ >;
1786
+ export const StaticWebApp = Resource<StaticWebApp>("Azure.StaticWebApp");
1787
+ export const StaticWebAppProvider = () =>
1788
+ Provider.effect(
1789
+ StaticWebApp,
1790
+ Effect.gen(function* () {
1791
+ const clients = yield* makeAzureClients;
1792
+ const names = yield* makePhysicalNames;
1793
+ const nameOf = (id: string, instanceId: string, props: StaticWebAppProps) =>
1794
+ names.physicalName(id, instanceId, props.name, { maxLength: 60, lowercase: true });
1795
+ return StaticWebApp.Provider.of({
1796
+ stables: ["name", "resourceGroupName", "resourceId"],
1797
+ list: () =>
1798
+ listOwnedByResourceGroup(
1799
+ clients,
1800
+ (rg) => clients.appService.staticSites.listStaticSitesByResourceGroup(rg),
1801
+ staticSiteAttrs,
1802
+ ),
1803
+ diff: diffOnChanges({
1804
+ identity: resourceGroupIdentity(nameOf),
1805
+ replace: (props) => ({
1806
+ repositoryUrl: props.repositoryUrl,
1807
+ branch: props.branch,
1808
+ appLocation: props.appLocation,
1809
+ apiLocation: props.apiLocation,
1810
+ outputLocation: props.outputLocation,
1811
+ }),
1812
+ mutable: (props) => ({
1813
+ sku: props.sku ?? "Free",
1814
+ appSettings: props.appSettings ?? {},
1815
+ tags: props.tags,
1816
+ }),
1817
+ }),
1818
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1819
+ if (!output && !olds) return undefined;
1820
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1821
+ if (!rg) return undefined;
1822
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1823
+ const site = yield* azurePromise("read Static Web App", name, () =>
1824
+ clients.appService.staticSites.getStaticSite(rg, name),
1825
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
1826
+ return site ? ownershipAware(id, site, staticSiteAttrs(site, rg), !!output) : undefined;
1827
+ }),
1828
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1829
+ const rg = yield* resourceGroupName(news.resourceGroup);
1830
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1831
+ const name = nameOf(id, instanceId, news);
1832
+ yield* ensureTaggedOwnership(id, "Static Web App", name, output, olds, () =>
1833
+ clients.appService.staticSites.getStaticSite(rg, name)
1834
+ );
1835
+ const site = yield* azurePromise("reconcile Static Web App", name, () =>
1836
+ clients.appService.staticSites.beginCreateOrUpdateStaticSiteAndWait(rg, name, {
1837
+ location,
1838
+ sku: { name: news.sku ?? "Free" },
1839
+ repositoryUrl: news.repositoryUrl,
1840
+ branch: news.branch,
1841
+ buildProperties: {
1842
+ appLocation: news.appLocation,
1843
+ apiLocation: news.apiLocation,
1844
+ appArtifactLocation: news.outputLocation,
1845
+ },
1846
+ tags: withAlchemyTags(id, news.tags),
1847
+ } as Parameters<
1848
+ typeof clients.appService.staticSites.beginCreateOrUpdateStaticSiteAndWait
1849
+ >[2]),
1850
+ );
1851
+ if (news.appSettings) {
1852
+ yield* azurePromise("update Static Web App settings", name, () =>
1853
+ clients.appService.staticSites.createOrUpdateStaticSiteAppSettings(rg, name, {
1854
+ properties: news.appSettings,
1855
+ } as Parameters<
1856
+ typeof clients.appService.staticSites.createOrUpdateStaticSiteAppSettings
1857
+ >[2]),
1858
+ );
1859
+ }
1860
+ return staticSiteAttrs(site, rg);
1861
+ }),
1862
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1863
+ yield* deleteIfEnabled(
1864
+ () =>
1865
+ clients.appService.staticSites.beginDeleteStaticSiteAndWait(
1866
+ output.resourceGroupName,
1867
+ output.name,
1868
+ ),
1869
+ output.name,
1870
+ "Static Web App",
1871
+ )({ olds, session });
1872
+ }),
1873
+ });
1874
+ }),
1875
+ );
1876
+ function staticSiteAttrs(site: AzureResponse, resourceGroupName: string) {
1877
+ const data = site as Record<string, unknown>;
1878
+ return {
1879
+ name: site.name as string,
1880
+ resourceGroupName,
1881
+ location: site.location as string,
1882
+ resourceId: site.id as string,
1883
+ defaultHostname: data.defaultHostname as string | undefined,
1884
+ url: data.defaultHostname ? `https://${data.defaultHostname}` : undefined,
1885
+ provisioningState: site.provisioningState,
1886
+ tags: site.tags,
1887
+ } satisfies StaticWebApp["Attributes"];
1888
+ }
1889
+
1890
+ export interface ContainerInstanceProps extends BaseProps {
1891
+ image: string;
1892
+ cpu?: number;
1893
+ memoryInGB?: number;
1894
+ ports?: Array<{ port: number; protocol?: "TCP" | "UDP" }>;
1895
+ environmentVariables?: Record<string, string>;
1896
+ restartPolicy?: "Always" | "Never" | "OnFailure";
1897
+ osType?: "Linux" | "Windows";
1898
+ }
1899
+ export type ContainerInstance = Resource<
1900
+ "Azure.ContainerInstance",
1901
+ ContainerInstanceProps,
1902
+ Attrs<{ fqdn?: string; ipAddress?: string }>,
1903
+ never,
1904
+ Providers
1905
+ >;
1906
+ export const ContainerInstance = Resource<ContainerInstance>("Azure.ContainerInstance");
1907
+ export const ContainerInstanceProvider = () =>
1908
+ Provider.effect(
1909
+ ContainerInstance,
1910
+ Effect.gen(function* () {
1911
+ const clients = yield* makeAzureClients;
1912
+ const names = yield* makePhysicalNames;
1913
+ const nameOf = (id: string, instanceId: string, props: ContainerInstanceProps) =>
1914
+ names.physicalName(id, instanceId, props.name, { maxLength: 63, lowercase: true });
1915
+ return ContainerInstance.Provider.of({
1916
+ stables: ["name", "resourceGroupName", "resourceId"],
1917
+ list: () =>
1918
+ listOwnedByResourceGroup(
1919
+ clients,
1920
+ (rg) => clients.containerInstance.containerGroups.listByResourceGroup(rg),
1921
+ containerAttrs,
1922
+ ),
1923
+ diff: diffOnChanges({
1924
+ identity: resourceGroupIdentity(nameOf),
1925
+ replace: (props) => ({
1926
+ image: props.image,
1927
+ cpu: props.cpu ?? 1,
1928
+ memoryInGB: props.memoryInGB ?? 1.5,
1929
+ ports: props.ports ?? [{ port: 80, protocol: "TCP" }],
1930
+ environmentVariables: props.environmentVariables ?? {},
1931
+ restartPolicy: props.restartPolicy ?? "Always",
1932
+ osType: props.osType ?? "Linux",
1933
+ }),
1934
+ mutable: (props) => ({
1935
+ tags: props.tags,
1936
+ }),
1937
+ }),
1938
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
1939
+ if (!output && !olds) return undefined;
1940
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
1941
+ if (!rg) return undefined;
1942
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
1943
+ const group = yield* azurePromise("read container instance", name, () =>
1944
+ clients.containerInstance.containerGroups.get(rg, name),
1945
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
1946
+ return group ? ownershipAware(id, group, containerAttrs(group, rg), !!output) : undefined;
1947
+ }),
1948
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
1949
+ const rg = yield* resourceGroupName(news.resourceGroup);
1950
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
1951
+ const name = nameOf(id, instanceId, news);
1952
+ const ports = news.ports ?? [{ port: 80, protocol: "TCP" }];
1953
+ yield* ensureTaggedOwnership(id, "container instance", name, output, olds, () =>
1954
+ clients.containerInstance.containerGroups.get(rg, name)
1955
+ );
1956
+ const group = yield* azurePromise("reconcile container instance", name, () =>
1957
+ clients.containerInstance.containerGroups.beginCreateOrUpdateAndWait(rg, name, {
1958
+ location,
1959
+ osType: news.osType ?? "Linux",
1960
+ restartPolicy: news.restartPolicy ?? "Always",
1961
+ containers: [
1962
+ {
1963
+ name,
1964
+ image: news.image,
1965
+ resources: {
1966
+ requests: { cpu: news.cpu ?? 1, memoryInGB: news.memoryInGB ?? 1.5 },
1967
+ },
1968
+ ports,
1969
+ environmentVariables: Object.entries(news.environmentVariables ?? {}).map(
1970
+ ([name, value]) => ({ name, value }),
1971
+ ),
1972
+ },
1973
+ ],
1974
+ ipAddress: { type: "Public", ports },
1975
+ tags: withAlchemyTags(id, news.tags),
1976
+ } as Parameters<
1977
+ typeof clients.containerInstance.containerGroups.beginCreateOrUpdateAndWait
1978
+ >[2]),
1979
+ );
1980
+ return containerAttrs(group, rg);
1981
+ }),
1982
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
1983
+ yield* deleteIfEnabled(
1984
+ () =>
1985
+ clients.containerInstance.containerGroups.beginDeleteAndWait(
1986
+ output.resourceGroupName,
1987
+ output.name,
1988
+ ),
1989
+ output.name,
1990
+ "container instance",
1991
+ )({ olds, session });
1992
+ }),
1993
+ });
1994
+ }),
1995
+ );
1996
+ function containerAttrs(group: AzureResponse, resourceGroupName: string) {
1997
+ const data = group as Record<string, unknown>;
1998
+ const ipAddress = data.ipAddress as { fqdn?: string; ip?: string } | undefined;
1999
+ return {
2000
+ name: group.name as string,
2001
+ resourceGroupName,
2002
+ location: group.location as string,
2003
+ resourceId: group.id as string,
2004
+ fqdn: ipAddress?.fqdn,
2005
+ ipAddress: ipAddress?.ip,
2006
+ provisioningState: group.provisioningState,
2007
+ tags: group.tags,
2008
+ } satisfies ContainerInstance["Attributes"];
2009
+ }
2010
+
2011
+ export interface VirtualMachineProps extends BaseProps {
2012
+ adminUsername: string;
2013
+ adminPassword?: string | Redacted.Redacted<string>;
2014
+ sshPublicKey?: string;
2015
+ vmSize?: string;
2016
+ image?: { publisher: string; offer: string; sku: string; version?: string };
2017
+ subnetId?: string;
2018
+ }
2019
+ export type VirtualMachine = Resource<
2020
+ "Azure.VirtualMachine",
2021
+ VirtualMachineProps,
2022
+ Attrs<{ vmId?: string; privateIpAddress?: string }>,
2023
+ never,
2024
+ Providers
2025
+ >;
2026
+ export const VirtualMachine = Resource<VirtualMachine>("Azure.VirtualMachine");
2027
+ export const VirtualMachineProvider = () =>
2028
+ Provider.effect(
2029
+ VirtualMachine,
2030
+ Effect.gen(function* () {
2031
+ const clients = yield* makeAzureClients;
2032
+ const names = yield* makePhysicalNames;
2033
+ const nameOf = (id: string, instanceId: string, props: VirtualMachineProps) =>
2034
+ names.physicalName(id, instanceId, props.name, { maxLength: 64 });
2035
+ return VirtualMachine.Provider.of({
2036
+ stables: ["name", "resourceGroupName", "resourceId"],
2037
+ list: () =>
2038
+ listOwnedByResourceGroup(
2039
+ clients,
2040
+ (rg) => clients.compute.virtualMachines.list(rg),
2041
+ vmAttrs,
2042
+ ),
2043
+ diff: diffOnChanges({
2044
+ identity: resourceGroupIdentity(nameOf),
2045
+ replace: (props) => ({
2046
+ adminUsername: props.adminUsername,
2047
+ adminPassword: props.adminPassword,
2048
+ sshPublicKey: props.sshPublicKey,
2049
+ image: props.image ?? {
2050
+ publisher: "Canonical",
2051
+ offer: "0001-com-ubuntu-server-jammy",
2052
+ sku: "22_04-lts",
2053
+ version: "latest",
2054
+ },
2055
+ subnetId: props.subnetId,
2056
+ }),
2057
+ mutable: (props) => ({
2058
+ vmSize: props.vmSize ?? "Standard_B1s",
2059
+ tags: props.tags,
2060
+ }),
2061
+ }),
2062
+ read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
2063
+ if (!output && !olds) return undefined;
2064
+ const rg = output?.resourceGroupName ?? (yield* resourceGroupName(olds!.resourceGroup));
2065
+ if (!rg) return undefined;
2066
+ const name = output?.name ?? nameOf(id, instanceId, olds!);
2067
+ const vm = yield* azurePromise("read virtual machine", name, () =>
2068
+ clients.compute.virtualMachines.get(rg, name),
2069
+ ).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
2070
+ return vm ? ownershipAware(id, vm, vmAttrs(vm, rg), !!output) : undefined;
2071
+ }),
2072
+ reconcile: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
2073
+ if (!news.subnetId)
2074
+ throw new Error(`VirtualMachine ${id} requires subnetId in the external v2 provider.`);
2075
+ const rg = yield* resourceGroupName(news.resourceGroup);
2076
+ const location = yield* requireLocation(id, news.location, news.resourceGroup);
2077
+ const name = nameOf(id, instanceId, news);
2078
+ const nicName = `${name}-nic`;
2079
+ yield* ensureTaggedOwnership(id, "virtual machine", name, output, olds, () =>
2080
+ clients.compute.virtualMachines.get(rg, name)
2081
+ );
2082
+ yield* azurePromise("reconcile virtual machine network interface", nicName, () =>
2083
+ clients.network.networkInterfaces.beginCreateOrUpdateAndWait(rg, nicName, {
2084
+ location,
2085
+ ipConfigurations: [
2086
+ {
2087
+ name: "ipconfig1",
2088
+ subnet: { id: news.subnetId },
2089
+ privateIPAllocationMethod: "Dynamic",
2090
+ },
2091
+ ],
2092
+ tags: withAlchemyTags(id, news.tags),
2093
+ } as Parameters<
2094
+ typeof clients.network.networkInterfaces.beginCreateOrUpdateAndWait
2095
+ >[2]),
2096
+ );
2097
+ const image = news.image ?? {
2098
+ publisher: "Canonical",
2099
+ offer: "0001-com-ubuntu-server-jammy",
2100
+ sku: "22_04-lts",
2101
+ version: "latest",
2102
+ };
2103
+ const vm = yield* azurePromise("reconcile virtual machine", name, () =>
2104
+ clients.compute.virtualMachines.beginCreateOrUpdateAndWait(rg, name, {
2105
+ location,
2106
+ hardwareProfile: { vmSize: news.vmSize ?? "Standard_B1s" },
2107
+ storageProfile: { imageReference: image, osDisk: { createOption: "FromImage" } },
2108
+ osProfile: {
2109
+ computerName: name,
2110
+ adminUsername: news.adminUsername,
2111
+ adminPassword:
2112
+ typeof news.adminPassword === "string"
2113
+ ? news.adminPassword
2114
+ : news.adminPassword
2115
+ ? Redacted.value(news.adminPassword)
2116
+ : undefined,
2117
+ linuxConfiguration: news.sshPublicKey
2118
+ ? {
2119
+ disablePasswordAuthentication: true,
2120
+ ssh: {
2121
+ publicKeys: [
2122
+ {
2123
+ path: `/home/${news.adminUsername}/.ssh/authorized_keys`,
2124
+ keyData: news.sshPublicKey,
2125
+ },
2126
+ ],
2127
+ },
2128
+ }
2129
+ : undefined,
2130
+ },
2131
+ networkProfile: {
2132
+ networkInterfaces: [
2133
+ {
2134
+ id: `/subscriptions/${clients.subscriptionId}/resourceGroups/${rg}/providers/Microsoft.Network/networkInterfaces/${nicName}`,
2135
+ },
2136
+ ],
2137
+ },
2138
+ tags: withAlchemyTags(id, news.tags),
2139
+ } as Parameters<typeof clients.compute.virtualMachines.beginCreateOrUpdateAndWait>[2]),
2140
+ ).pipe(withHeartbeat(`virtual machine "${name}"`));
2141
+ return vmAttrs(vm, rg);
2142
+ }),
2143
+ delete: Effect.fnUntraced(function* ({ olds, output, session }) {
2144
+ if (olds?.delete === false) return;
2145
+
2146
+ yield* session.note(`Deleting Azure virtual machine: ${output.name}`);
2147
+ yield* azurePromise("delete virtual machine", output.name, () =>
2148
+ clients.compute.virtualMachines.beginDeleteAndWait(output.resourceGroupName, output.name),
2149
+ ).pipe(
2150
+ withHeartbeat(`deleting virtual machine "${output.name}"`),
2151
+ Effect.catchIf(isNotFound, () => Effect.void),
2152
+ );
2153
+ yield* waitForAzureDeleted(
2154
+ () => clients.compute.virtualMachines.get(output.resourceGroupName, output.name),
2155
+ output.name,
2156
+ "virtual machine",
2157
+ session,
2158
+ );
2159
+
2160
+ const nicName = `${output.name}-nic`;
2161
+ yield* session.note(`Deleting Azure network interface: ${nicName}`);
2162
+ yield* azurePromise("delete network interface", nicName, () =>
2163
+ clients.network.networkInterfaces.beginDeleteAndWait(output.resourceGroupName, nicName),
2164
+ ).pipe(
2165
+ retryAzureDependencyConflicts(session),
2166
+ Effect.catchIf(isNotFound, () => Effect.void),
2167
+ );
2168
+ yield* waitForAzureDeleted(
2169
+ () => clients.network.networkInterfaces.get(output.resourceGroupName, nicName),
2170
+ nicName,
2171
+ "network interface",
2172
+ session,
2173
+ );
2174
+ }),
2175
+ });
2176
+ }),
2177
+ );
2178
+ function vmAttrs(vm: AzureResponse, resourceGroupName: string) {
2179
+ const data = vm as Record<string, unknown>;
2180
+ return {
2181
+ name: vm.name as string,
2182
+ resourceGroupName,
2183
+ location: vm.location as string,
2184
+ resourceId: vm.id as string,
2185
+ vmId: data.vmId as string | undefined,
2186
+ provisioningState: vm.provisioningState,
2187
+ tags: vm.tags,
2188
+ } satisfies VirtualMachine["Attributes"];
2189
+ }