@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.
- package/ARCHITECTURE.md +170 -0
- package/CHANGELOG.md +71 -0
- package/LICENSE +201 -0
- package/README.md +241 -0
- package/env.example +13 -0
- package/package.json +87 -0
- package/src/AuthProvider.ts +332 -0
- package/src/BlobContainer.ts +268 -0
- package/src/BlobState.ts +289 -0
- package/src/Clients.ts +122 -0
- package/src/ContainerApp.ts +555 -0
- package/src/ContainerAppEnvironment.ts +253 -0
- package/src/ContainerImage.ts +181 -0
- package/src/ContainerRegistry.ts +232 -0
- package/src/Credentials.ts +69 -0
- package/src/Errors.ts +101 -0
- package/src/Internal.ts +184 -0
- package/src/MoreResources.ts +2189 -0
- package/src/Providers.ts +125 -0
- package/src/ResourceGroup.ts +171 -0
- package/src/ResourceProviderRegistration.ts +177 -0
- package/src/StorageAccount.ts +292 -0
- package/src/index.ts +15 -0
package/src/Providers.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
|
|
3
|
+
import { ProfileLive } from "alchemy/Auth/Profile";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { AzureAuth } from "./AuthProvider.ts";
|
|
6
|
+
import { AzureClientsLive } from "./Clients.ts";
|
|
7
|
+
import { BlobContainer, BlobContainerProvider } from "./BlobContainer.ts";
|
|
8
|
+
import * as Credentials from "./Credentials.ts";
|
|
9
|
+
import { ContainerApp, ContainerAppProvider } from "./ContainerApp.ts";
|
|
10
|
+
import {
|
|
11
|
+
ContainerAppEnvironment,
|
|
12
|
+
ContainerAppEnvironmentProvider,
|
|
13
|
+
} from "./ContainerAppEnvironment.ts";
|
|
14
|
+
import { ContainerImage, ContainerImageProvider } from "./ContainerImage.ts";
|
|
15
|
+
import { ContainerRegistry, ContainerRegistryProvider } from "./ContainerRegistry.ts";
|
|
16
|
+
import {
|
|
17
|
+
AppService,
|
|
18
|
+
AppServicePlan,
|
|
19
|
+
AppServicePlanProvider,
|
|
20
|
+
AppServiceProvider,
|
|
21
|
+
CognitiveServices,
|
|
22
|
+
CognitiveServicesProvider,
|
|
23
|
+
ContainerInstance,
|
|
24
|
+
ContainerInstanceProvider,
|
|
25
|
+
CosmosDBAccount,
|
|
26
|
+
CosmosDBAccountProvider,
|
|
27
|
+
FunctionApp,
|
|
28
|
+
FunctionAppProvider,
|
|
29
|
+
KeyVault,
|
|
30
|
+
KeyVaultProvider,
|
|
31
|
+
NetworkSecurityGroup,
|
|
32
|
+
NetworkSecurityGroupProvider,
|
|
33
|
+
PublicIPAddress,
|
|
34
|
+
PublicIPAddressProvider,
|
|
35
|
+
ServiceBus,
|
|
36
|
+
ServiceBusProvider,
|
|
37
|
+
SqlDatabase,
|
|
38
|
+
SqlDatabaseProvider,
|
|
39
|
+
SqlServer,
|
|
40
|
+
SqlServerProvider,
|
|
41
|
+
StaticWebApp,
|
|
42
|
+
StaticWebAppProvider,
|
|
43
|
+
UserAssignedIdentity,
|
|
44
|
+
UserAssignedIdentityProvider,
|
|
45
|
+
VirtualMachine,
|
|
46
|
+
VirtualMachineProvider,
|
|
47
|
+
VirtualNetwork,
|
|
48
|
+
VirtualNetworkProvider,
|
|
49
|
+
} from "./MoreResources.ts";
|
|
50
|
+
import { ResourceGroup, ResourceGroupProvider } from "./ResourceGroup.ts";
|
|
51
|
+
import {
|
|
52
|
+
ResourceProviderRegistration,
|
|
53
|
+
ResourceProviderRegistrationProvider,
|
|
54
|
+
} from "./ResourceProviderRegistration.ts";
|
|
55
|
+
import { StorageAccount, StorageAccountProvider } from "./StorageAccount.ts";
|
|
56
|
+
|
|
57
|
+
export class Providers extends Provider.ProviderCollection<Providers>()("Azure") {}
|
|
58
|
+
|
|
59
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
60
|
+
|
|
61
|
+
export const providers = () =>
|
|
62
|
+
Layer.effect(
|
|
63
|
+
Providers,
|
|
64
|
+
Provider.collection([
|
|
65
|
+
ResourceGroup,
|
|
66
|
+
ResourceProviderRegistration,
|
|
67
|
+
StorageAccount,
|
|
68
|
+
BlobContainer,
|
|
69
|
+
UserAssignedIdentity,
|
|
70
|
+
VirtualNetwork,
|
|
71
|
+
NetworkSecurityGroup,
|
|
72
|
+
PublicIPAddress,
|
|
73
|
+
CognitiveServices,
|
|
74
|
+
ServiceBus,
|
|
75
|
+
CosmosDBAccount,
|
|
76
|
+
SqlServer,
|
|
77
|
+
SqlDatabase,
|
|
78
|
+
KeyVault,
|
|
79
|
+
AppServicePlan,
|
|
80
|
+
AppService,
|
|
81
|
+
FunctionApp,
|
|
82
|
+
StaticWebApp,
|
|
83
|
+
ContainerInstance,
|
|
84
|
+
ContainerAppEnvironment,
|
|
85
|
+
ContainerRegistry,
|
|
86
|
+
ContainerImage,
|
|
87
|
+
ContainerApp,
|
|
88
|
+
VirtualMachine,
|
|
89
|
+
]),
|
|
90
|
+
).pipe(
|
|
91
|
+
Layer.provide(
|
|
92
|
+
Layer.mergeAll(
|
|
93
|
+
ResourceGroupProvider(),
|
|
94
|
+
ResourceProviderRegistrationProvider(),
|
|
95
|
+
StorageAccountProvider(),
|
|
96
|
+
BlobContainerProvider(),
|
|
97
|
+
UserAssignedIdentityProvider(),
|
|
98
|
+
VirtualNetworkProvider(),
|
|
99
|
+
NetworkSecurityGroupProvider(),
|
|
100
|
+
PublicIPAddressProvider(),
|
|
101
|
+
CognitiveServicesProvider(),
|
|
102
|
+
ServiceBusProvider(),
|
|
103
|
+
CosmosDBAccountProvider(),
|
|
104
|
+
SqlServerProvider(),
|
|
105
|
+
SqlDatabaseProvider(),
|
|
106
|
+
KeyVaultProvider(),
|
|
107
|
+
AppServicePlanProvider(),
|
|
108
|
+
AppServiceProvider(),
|
|
109
|
+
FunctionAppProvider(),
|
|
110
|
+
StaticWebAppProvider(),
|
|
111
|
+
ContainerInstanceProvider(),
|
|
112
|
+
ContainerAppEnvironmentProvider(),
|
|
113
|
+
ContainerRegistryProvider(),
|
|
114
|
+
ContainerImageProvider(),
|
|
115
|
+
ContainerAppProvider(),
|
|
116
|
+
VirtualMachineProvider(),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
Layer.provide(AzureClientsLive),
|
|
120
|
+
Layer.provideMerge(Credentials.fromAuthProvider()),
|
|
121
|
+
Layer.provideMerge(AzureAuth),
|
|
122
|
+
Layer.provideMerge(ProfileLive),
|
|
123
|
+
Layer.provideMerge(CredentialsStoreLive),
|
|
124
|
+
Layer.orDie,
|
|
125
|
+
);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ResourceGroup as AzureResourceGroup } from "@azure/arm-resources";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
4
|
+
import { isResolved } from "alchemy/Diff";
|
|
5
|
+
import { Resource } from "alchemy";
|
|
6
|
+
import * as Provider from "alchemy/Provider";
|
|
7
|
+
import { makeAzureClients } from "./Clients.ts";
|
|
8
|
+
import { azureError, isNotFound } from "./Errors.ts";
|
|
9
|
+
import { collectAzurePages, diffValueEqual, makePhysicalNames } from "./Internal.ts";
|
|
10
|
+
import type { Providers } from "./Providers.ts";
|
|
11
|
+
|
|
12
|
+
export interface ResourceGroupProps {
|
|
13
|
+
/**
|
|
14
|
+
* Resource group name. Defaults to a deterministic physical name.
|
|
15
|
+
*/
|
|
16
|
+
name?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Azure region, for example `eastus`, `westus2`, or `westeurope`.
|
|
19
|
+
*/
|
|
20
|
+
location: string;
|
|
21
|
+
/**
|
|
22
|
+
* Tags to apply to the resource group.
|
|
23
|
+
*/
|
|
24
|
+
tags?: Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to delete the resource group when removed from Alchemy.
|
|
27
|
+
*
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
delete?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ResourceGroup = Resource<
|
|
34
|
+
"Azure.ResourceGroup",
|
|
35
|
+
ResourceGroupProps,
|
|
36
|
+
{
|
|
37
|
+
name: string;
|
|
38
|
+
location: string;
|
|
39
|
+
resourceGroupId: string;
|
|
40
|
+
provisioningState?: string;
|
|
41
|
+
tags?: Record<string, string>;
|
|
42
|
+
},
|
|
43
|
+
never,
|
|
44
|
+
Providers
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Azure Resource Group — a logical container for Azure resources.
|
|
49
|
+
*
|
|
50
|
+
* @example Basic resource group
|
|
51
|
+
* ```ts
|
|
52
|
+
* const group = yield* Azure.ResourceGroup("Group", {
|
|
53
|
+
* location: "westeurope",
|
|
54
|
+
* tags: { app: "demo" },
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export const ResourceGroup = Resource<ResourceGroup>("Azure.ResourceGroup");
|
|
59
|
+
|
|
60
|
+
export const ResourceGroupProvider = () =>
|
|
61
|
+
Provider.effect(
|
|
62
|
+
ResourceGroup,
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const clients = yield* makeAzureClients;
|
|
65
|
+
const names = yield* makePhysicalNames;
|
|
66
|
+
|
|
67
|
+
const resourceGroupName = (id: string, instanceId: string, name?: string) =>
|
|
68
|
+
names.physicalName(id, instanceId, name, { maxLength: 90 });
|
|
69
|
+
|
|
70
|
+
const toAttributes = (group: AzureResourceGroup) => {
|
|
71
|
+
if (!group.name || !group.id || !group.location) {
|
|
72
|
+
throw new Error("Azure returned an incomplete resource group response");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: group.name,
|
|
76
|
+
location: group.location,
|
|
77
|
+
resourceGroupId: group.id,
|
|
78
|
+
provisioningState: group.properties?.provisioningState,
|
|
79
|
+
tags: group.tags,
|
|
80
|
+
} satisfies ResourceGroup["Attributes"];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return ResourceGroup.Provider.of({
|
|
84
|
+
stables: ["name", "resourceGroupId"],
|
|
85
|
+
list: () =>
|
|
86
|
+
Effect.tryPromise({
|
|
87
|
+
try: () => collectAzurePages(clients.resources.resourceGroups.list()),
|
|
88
|
+
catch: (cause) => azureError({ operation: "list resource groups", cause }),
|
|
89
|
+
}).pipe(
|
|
90
|
+
Effect.map((groups) =>
|
|
91
|
+
groups
|
|
92
|
+
.filter((group) => group.tags?.["alchemy:logical-id"])
|
|
93
|
+
.map((group) => toAttributes(group)),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
diff: Effect.fnUntraced(function* ({ id, instanceId, olds, news, output }) {
|
|
97
|
+
if (!isResolved(news)) return undefined;
|
|
98
|
+
if (!output) return undefined;
|
|
99
|
+
const name = resourceGroupName(id, instanceId, news.name);
|
|
100
|
+
const oldName = output.name;
|
|
101
|
+
if (name !== oldName || news.location !== output.location) {
|
|
102
|
+
return { action: "replace" } as const;
|
|
103
|
+
}
|
|
104
|
+
if (!diffValueEqual(olds.tags ?? {}, news.tags ?? {})) {
|
|
105
|
+
return { action: "update" } as const;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}),
|
|
109
|
+
read: Effect.fnUntraced(function* ({ id, instanceId, olds, output }) {
|
|
110
|
+
const name = output?.name ?? resourceGroupName(id, instanceId, olds?.name);
|
|
111
|
+
const group = yield* Effect.tryPromise({
|
|
112
|
+
try: () => clients.resources.resourceGroups.get(name),
|
|
113
|
+
catch: (cause) => azureError({ operation: "read resource group", resource: name, cause }),
|
|
114
|
+
}).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
|
|
115
|
+
if (!group) return undefined;
|
|
116
|
+
const attrs = toAttributes(group);
|
|
117
|
+
return hasAlchemyTags(id, group.tags) ? attrs : Unowned(attrs);
|
|
118
|
+
}),
|
|
119
|
+
reconcile: Effect.fnUntraced(function* ({ id, instanceId, news }) {
|
|
120
|
+
const name = resourceGroupName(id, instanceId, news.name);
|
|
121
|
+
validateResourceGroupName(name);
|
|
122
|
+
const tags = withAlchemyTags(id, news.tags);
|
|
123
|
+
const group = yield* Effect.tryPromise({
|
|
124
|
+
try: () =>
|
|
125
|
+
clients.resources.resourceGroups.createOrUpdate(name, {
|
|
126
|
+
location: news.location,
|
|
127
|
+
tags,
|
|
128
|
+
}),
|
|
129
|
+
catch: (cause) =>
|
|
130
|
+
azureError({ operation: "reconcile resource group", resource: name, cause }),
|
|
131
|
+
});
|
|
132
|
+
return toAttributes(group);
|
|
133
|
+
}),
|
|
134
|
+
delete: Effect.fnUntraced(function* ({ olds, output, session }) {
|
|
135
|
+
if (olds.delete === false) return;
|
|
136
|
+
yield* session.note(`Deleting Azure resource group: ${output.name}`);
|
|
137
|
+
const poller = yield* Effect.tryPromise({
|
|
138
|
+
try: () => clients.resources.resourceGroups.beginDelete(output.name),
|
|
139
|
+
catch: (cause) => azureError({ operation: "delete resource group", resource: output.name, cause }),
|
|
140
|
+
}).pipe(Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
|
|
141
|
+
if (poller) {
|
|
142
|
+
yield* Effect.tryPromise({
|
|
143
|
+
try: () => poller.pollUntilDone(),
|
|
144
|
+
catch: (cause) => azureError({ operation: "wait for resource group deletion", resource: output.name, cause }),
|
|
145
|
+
}).pipe(
|
|
146
|
+
Effect.catchIf(isNotFound, () => Effect.void),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
function validateResourceGroupName(name: string) {
|
|
155
|
+
if (!/^[\w\-.()]{1,90}$/.test(name)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Azure resource group name "${name}" is invalid. It must be 1-90 characters and contain only letters, numbers, underscores, hyphens, periods, and parentheses.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function withAlchemyTags(id: string, tags?: Record<string, string>) {
|
|
163
|
+
return {
|
|
164
|
+
...tags,
|
|
165
|
+
"alchemy:logical-id": id,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function hasAlchemyTags(id: string, tags: Record<string, string> | undefined) {
|
|
170
|
+
return tags?.["alchemy:logical-id"] === id;
|
|
171
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Provider as AzureResourceProvider } from "@azure/arm-resources";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { isResolved } from "alchemy/Diff";
|
|
4
|
+
import { Resource } from "alchemy";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import { makeAzureClients } from "./Clients.ts";
|
|
7
|
+
import { azureError, isNotFound } from "./Errors.ts";
|
|
8
|
+
import { collectAzurePages } from "./Internal.ts";
|
|
9
|
+
import type { Providers } from "./Providers.ts";
|
|
10
|
+
|
|
11
|
+
export interface ResourceProviderRegistrationProps {
|
|
12
|
+
/** Azure resource provider namespace, for example `Microsoft.App`. */
|
|
13
|
+
namespace: string;
|
|
14
|
+
/** Seconds to wait for Azure to report `Registered`. @default 600 */
|
|
15
|
+
timeoutSeconds?: number;
|
|
16
|
+
/** Seconds between registration-state polls. @default 5 */
|
|
17
|
+
pollIntervalSeconds?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Whether destroy should unregister the provider namespace.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to false because provider registrations are subscription-scoped
|
|
22
|
+
* prerequisites and unregistering can break unrelated workloads.
|
|
23
|
+
*/
|
|
24
|
+
unregisterOnDelete?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ResourceProviderRegistration = Resource<
|
|
28
|
+
"Azure.ResourceProviderRegistration",
|
|
29
|
+
ResourceProviderRegistrationProps,
|
|
30
|
+
{
|
|
31
|
+
namespace: string;
|
|
32
|
+
registrationState?: string;
|
|
33
|
+
providerId?: string;
|
|
34
|
+
unregisterOnDelete: boolean;
|
|
35
|
+
},
|
|
36
|
+
never,
|
|
37
|
+
Providers
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export const ResourceProviderRegistration = Resource<ResourceProviderRegistration>(
|
|
41
|
+
"Azure.ResourceProviderRegistration",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const ResourceProviderRegistrationProvider = () =>
|
|
45
|
+
Provider.effect(
|
|
46
|
+
ResourceProviderRegistration,
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
const clients = yield* makeAzureClients;
|
|
49
|
+
|
|
50
|
+
return ResourceProviderRegistration.Provider.of({
|
|
51
|
+
stables: ["namespace"],
|
|
52
|
+
list: () =>
|
|
53
|
+
Effect.tryPromise({
|
|
54
|
+
try: () => collectAzurePages(clients.resources.providers.list()),
|
|
55
|
+
catch: (cause) =>
|
|
56
|
+
azureError({ operation: "list Azure resource provider registrations", cause }),
|
|
57
|
+
}).pipe(
|
|
58
|
+
Effect.map((providers) => providers.map((provider) => toAttributes(provider, false))),
|
|
59
|
+
),
|
|
60
|
+
diff: Effect.fnUntraced(function* ({ news, output }) {
|
|
61
|
+
if (!isResolved(news)) return undefined;
|
|
62
|
+
if (!output) return undefined;
|
|
63
|
+
if (normalizeNamespace(news.namespace) !== output.namespace) {
|
|
64
|
+
return { action: "replace" } as const;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}),
|
|
68
|
+
read: Effect.fnUntraced(function* ({ olds, output }) {
|
|
69
|
+
const namespace = output?.namespace ?? (olds ? normalizeNamespace(olds.namespace) : undefined);
|
|
70
|
+
if (!namespace) return undefined;
|
|
71
|
+
const provider = yield* getProvider(namespace);
|
|
72
|
+
return toAttributes(provider, output?.unregisterOnDelete ?? olds?.unregisterOnDelete ?? false);
|
|
73
|
+
}),
|
|
74
|
+
reconcile: Effect.fnUntraced(function* ({ news }) {
|
|
75
|
+
const namespace = normalizeNamespace(news.namespace);
|
|
76
|
+
validateNamespace(namespace);
|
|
77
|
+
|
|
78
|
+
const current = yield* getProvider(namespace);
|
|
79
|
+
if (current.registrationState === "Registered") {
|
|
80
|
+
return toAttributes(current, news.unregisterOnDelete ?? false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
yield* Effect.tryPromise({
|
|
84
|
+
try: () => clients.resources.providers.register(namespace),
|
|
85
|
+
catch: (cause) =>
|
|
86
|
+
azureError({
|
|
87
|
+
operation: "register Azure resource provider",
|
|
88
|
+
resource: namespace,
|
|
89
|
+
cause,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const registered = yield* Effect.tryPromise({
|
|
94
|
+
try: () =>
|
|
95
|
+
waitForRegistered(
|
|
96
|
+
() => clients.resources.providers.get(namespace),
|
|
97
|
+
news.timeoutSeconds ?? 600,
|
|
98
|
+
news.pollIntervalSeconds ?? 5,
|
|
99
|
+
),
|
|
100
|
+
catch: (cause) =>
|
|
101
|
+
azureError({
|
|
102
|
+
operation: "wait for Azure resource provider registration",
|
|
103
|
+
resource: namespace,
|
|
104
|
+
cause,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
return toAttributes(registered, news.unregisterOnDelete ?? false);
|
|
108
|
+
}),
|
|
109
|
+
delete: Effect.fnUntraced(function* ({ olds, output, session }) {
|
|
110
|
+
if (!olds.unregisterOnDelete && !output.unregisterOnDelete) return;
|
|
111
|
+
yield* session.note(`Unregistering Azure resource provider: ${output.namespace}`);
|
|
112
|
+
yield* Effect.tryPromise({
|
|
113
|
+
try: () => clients.resources.providers.unregister(output.namespace),
|
|
114
|
+
catch: (cause) =>
|
|
115
|
+
azureError({
|
|
116
|
+
operation: "unregister Azure resource provider",
|
|
117
|
+
resource: output.namespace,
|
|
118
|
+
cause,
|
|
119
|
+
}),
|
|
120
|
+
}).pipe(
|
|
121
|
+
Effect.catchIf(isNotFound, () => Effect.void),
|
|
122
|
+
);
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function getProvider(namespace: string) {
|
|
127
|
+
return Effect.tryPromise({
|
|
128
|
+
try: () => clients.resources.providers.get(namespace),
|
|
129
|
+
catch: (cause) =>
|
|
130
|
+
azureError({
|
|
131
|
+
operation: "read Azure resource provider registration",
|
|
132
|
+
resource: namespace,
|
|
133
|
+
cause,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
function toAttributes(provider: AzureResourceProvider, unregisterOnDelete: boolean) {
|
|
141
|
+
const namespace = normalizeNamespace(provider.namespace ?? provider.id?.split("/").pop() ?? "");
|
|
142
|
+
return {
|
|
143
|
+
namespace,
|
|
144
|
+
registrationState: provider.registrationState,
|
|
145
|
+
providerId: provider.id,
|
|
146
|
+
unregisterOnDelete,
|
|
147
|
+
} satisfies ResourceProviderRegistration["Attributes"];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeNamespace(namespace: string) {
|
|
151
|
+
return namespace.trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateNamespace(namespace: string) {
|
|
155
|
+
if (!/^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+$/.test(namespace)) {
|
|
156
|
+
throw new Error(`Azure resource provider namespace "${namespace}" is invalid.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function waitForRegistered(
|
|
161
|
+
getProvider: () => Promise<AzureResourceProvider>,
|
|
162
|
+
timeoutSeconds: number,
|
|
163
|
+
pollIntervalSeconds: number,
|
|
164
|
+
) {
|
|
165
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
166
|
+
let latest = await getProvider();
|
|
167
|
+
while (latest.registrationState !== "Registered") {
|
|
168
|
+
if (Date.now() >= deadline) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Timed out waiting for ${latest.namespace ?? "provider"} to register; latest state: ${latest.registrationState ?? "unknown"}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalSeconds * 1000));
|
|
174
|
+
latest = await getProvider();
|
|
175
|
+
}
|
|
176
|
+
return latest;
|
|
177
|
+
}
|