@intentius/chant-lexicon-azure 0.124.39 → 0.124.40
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/dist/components/arm-deploy.d.ts +80 -0
- package/dist/components/arm-deploy.d.ts.map +1 -0
- package/dist/components/arm-deploy.js +398 -0
- package/dist/components/capability-plugin.d.ts +6 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/components/capability-plugin.js +17 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +2 -0
- package/dist/composites/container-app.d.ts.map +1 -1
- package/dist/composites/container-app.js +25 -8
- package/dist/deep-observe.d.ts +30 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +7 -2
- package/src/components/arm-deploy-floci.e2e.test.ts +80 -0
- package/src/components/arm-deploy.test.ts +256 -0
- package/src/components/arm-deploy.ts +491 -0
- package/src/components/capability-plugin.ts +26 -0
- package/src/components/index.ts +9 -0
- package/src/composites/composites.test.ts +39 -0
- package/src/composites/container-app.ts +30 -9
- package/src/deep-observe.ts +2 -2
- package/src/index.ts +3 -0
- package/src/plugin.test.ts +10 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `arm-deploy`: the azure lexicon's apply step for components (#4273). It
|
|
3
|
+
* applies a built ARM template into one resource group through `azApply`
|
|
4
|
+
* (../op/activities/az-apply.ts), the same per-resource applier `ApplyOp`'s
|
|
5
|
+
* `arm` target and the floci-az lane use, so a component step, an Op and the
|
|
6
|
+
* emulator run share one code path.
|
|
7
|
+
*
|
|
8
|
+
* `plan` reads each template resource live with the read client
|
|
9
|
+
* (../api/read-client.ts) and compares it with the body `azApply` would PUT,
|
|
10
|
+
* normalized with the noise rules deep observation uses
|
|
11
|
+
* (../deep-observe.ts). A resource that is not there is a create, one whose
|
|
12
|
+
* claimed properties all match is a no-op, and one that differs is an update
|
|
13
|
+
* with each changed path before and after. With `prune: true`, each
|
|
14
|
+
* chant-owned resource in the group the template no longer declares is a
|
|
15
|
+
* delete, the set `pruneArmOrphans` would delete.
|
|
16
|
+
*
|
|
17
|
+
* ARM's PUT is a create-or-update with no preview, so the plan cannot ask
|
|
18
|
+
* Azure what would change the way `cfn-deploy` asks for a change set. What
|
|
19
|
+
* holds the apply to the approved plan is the digest: `run` plans again and
|
|
20
|
+
* applies only when it gets the approved one.
|
|
21
|
+
*/
|
|
22
|
+
import type { Capability } from "@intentius/chant/components/capability";
|
|
23
|
+
import { type AzHttp, type AzNotPrunable } from "../op/activities/az-apply.js";
|
|
24
|
+
export interface ArmDeployInput {
|
|
25
|
+
/** Path to the built ARM template (`deploymentTemplate.json`). */
|
|
26
|
+
template: string;
|
|
27
|
+
/** The resource group the template applies into. Default: the component's name. */
|
|
28
|
+
resourceGroup?: string;
|
|
29
|
+
/** Region for the group and for `resourceGroup().location`. Default: `eastus`. */
|
|
30
|
+
location?: string;
|
|
31
|
+
/** ARM endpoint. Default: `AZURE_ENDPOINT_URL`, else real Azure. */
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
/** Subscription id. Default: `AZURE_SUBSCRIPTION_ID`, else floci-az's local subscription. */
|
|
34
|
+
subscriptionId?: string;
|
|
35
|
+
/** Delete chant-owned resources of the group the template no longer declares. Default: false. */
|
|
36
|
+
prune?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ArmDeployOutput {
|
|
39
|
+
applied: Array<{
|
|
40
|
+
type: string;
|
|
41
|
+
name: string;
|
|
42
|
+
}>;
|
|
43
|
+
pruned: Array<{
|
|
44
|
+
type: string;
|
|
45
|
+
name: string;
|
|
46
|
+
deleted: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
notPrunable: AzNotPrunable[];
|
|
49
|
+
/** The template's `outputs`, evaluated against the group once it applied. */
|
|
50
|
+
outputs: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/** What an `arm-deploy` plan hands `run` through `DeployContext.plans[resourceGroup]`. */
|
|
53
|
+
export interface ArmDeployPlanArtifact {
|
|
54
|
+
kind: "arm-deploy-plan";
|
|
55
|
+
resourceGroup: string;
|
|
56
|
+
planDigest: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thrown by `run` when planning again gives another digest than the approved
|
|
60
|
+
* plan: the group, the template or the endpoint moved since. Nothing is
|
|
61
|
+
* applied. `planMoved` is core's mark for a moved plan, so the command exits 3.
|
|
62
|
+
*/
|
|
63
|
+
export declare class ArmDeployPlanMovedError extends Error {
|
|
64
|
+
readonly resourceGroup: string;
|
|
65
|
+
readonly planned: string;
|
|
66
|
+
readonly now: string;
|
|
67
|
+
readonly planMoved = true;
|
|
68
|
+
constructor(resourceGroup: string, planned: string, now: string);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The `arm-deploy` capability over an injectable HTTP transport (the
|
|
72
|
+
* `AzHttp` seam `azApply` and the read client share). Tests pass a fake;
|
|
73
|
+
* the default is `fetch`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createArmDeployCapability(options?: {
|
|
76
|
+
http?: AzHttp;
|
|
77
|
+
}): Capability<ArmDeployInput, ArmDeployOutput>;
|
|
78
|
+
/** The default `arm-deploy` capability, over `fetch`. */
|
|
79
|
+
export declare const armDeployCapability: Capability<ArmDeployInput, ArmDeployOutput>;
|
|
80
|
+
//# sourceMappingURL=arm-deploy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arm-deploy.d.ts","sourceRoot":"","sources":["../../src/components/arm-deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAiD,MAAM,wCAAwC,CAAC;AAQxH,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAOnC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChE,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAG9B,aAAa,EAAE,MAAM;aACrB,OAAO,EAAE,MAAM;aACf,GAAG,EAAE,MAAM;IAJ7B,QAAQ,CAAC,SAAS,QAAQ;IAC1B,YACkB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EAO5B;CACF;AAkVD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,UAAU,CAAC,cAAc,EAAE,eAAe,CAAC,CAwCtH;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,EAAE,UAAU,CAAC,cAAc,EAAE,eAAe,CAA+B,CAAC"}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `arm-deploy`: the azure lexicon's apply step for components (#4273). It
|
|
3
|
+
* applies a built ARM template into one resource group through `azApply`
|
|
4
|
+
* (../op/activities/az-apply.ts), the same per-resource applier `ApplyOp`'s
|
|
5
|
+
* `arm` target and the floci-az lane use, so a component step, an Op and the
|
|
6
|
+
* emulator run share one code path.
|
|
7
|
+
*
|
|
8
|
+
* `plan` reads each template resource live with the read client
|
|
9
|
+
* (../api/read-client.ts) and compares it with the body `azApply` would PUT,
|
|
10
|
+
* normalized with the noise rules deep observation uses
|
|
11
|
+
* (../deep-observe.ts). A resource that is not there is a create, one whose
|
|
12
|
+
* claimed properties all match is a no-op, and one that differs is an update
|
|
13
|
+
* with each changed path before and after. With `prune: true`, each
|
|
14
|
+
* chant-owned resource in the group the template no longer declares is a
|
|
15
|
+
* delete, the set `pruneArmOrphans` would delete.
|
|
16
|
+
*
|
|
17
|
+
* ARM's PUT is a create-or-update with no preview, so the plan cannot ask
|
|
18
|
+
* Azure what would change the way `cfn-deploy` asks for a change set. What
|
|
19
|
+
* holds the apply to the approved plan is the digest: `run` plans again and
|
|
20
|
+
* applies only when it gets the approved one.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { MASKED, UNRESOLVED, deepPathSet, flattenDeepProperties, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
25
|
+
import { diffDeep } from "@intentius/chant/lifecycle/deep-diff";
|
|
26
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
27
|
+
import { AzureReadError, getResource, isNotFound, listResources } from "../api/read-client.js";
|
|
28
|
+
import { azureDeepNormalizationHooks, buildLiveProperties } from "../deep-observe.js";
|
|
29
|
+
import { classifyArmFailure } from "../describe-resources.js";
|
|
30
|
+
import { armDependencies, azApply, evalArm, evalArmString, isChantOwned, orderArmResources, rememberDeployed, } from "../op/activities/az-apply.js";
|
|
31
|
+
/** floci-az's local subscription, the applier's default too. */
|
|
32
|
+
const DEFAULT_SUBSCRIPTION = "00000000-0000-0000-0000-000000000001";
|
|
33
|
+
const DEFAULT_ENDPOINT = "https://management.azure.com";
|
|
34
|
+
const DEFAULT_LOCATION = "eastus";
|
|
35
|
+
function isArmDeployPlanArtifact(value) {
|
|
36
|
+
const v = value;
|
|
37
|
+
return v?.kind === "arm-deploy-plan" && typeof v.resourceGroup === "string" && typeof v.planDigest === "string";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown by `run` when planning again gives another digest than the approved
|
|
41
|
+
* plan: the group, the template or the endpoint moved since. Nothing is
|
|
42
|
+
* applied. `planMoved` is core's mark for a moved plan, so the command exits 3.
|
|
43
|
+
*/
|
|
44
|
+
export class ArmDeployPlanMovedError extends Error {
|
|
45
|
+
constructor(resourceGroup, planned, now) {
|
|
46
|
+
super(`arm-deploy "${resourceGroup}": refusing to apply. Planning again gives ${now}, the approved plan was ${planned}; ` +
|
|
47
|
+
`the resource group or its template changed since. Plan it again.`);
|
|
48
|
+
this.resourceGroup = resourceGroup;
|
|
49
|
+
this.planned = planned;
|
|
50
|
+
this.now = now;
|
|
51
|
+
this.planMoved = true;
|
|
52
|
+
this.name = "ArmDeployPlanMovedError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function targetOf(ctx, input) {
|
|
56
|
+
return {
|
|
57
|
+
resourceGroup: input.resourceGroup ?? ctx.component,
|
|
58
|
+
location: input.location ?? DEFAULT_LOCATION,
|
|
59
|
+
endpoint: (input.endpoint ?? process.env.AZURE_ENDPOINT_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
60
|
+
subscriptionId: input.subscriptionId ?? process.env.AZURE_SUBSCRIPTION_ID ?? DEFAULT_SUBSCRIPTION,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function evalCtxOf(target, http) {
|
|
64
|
+
return {
|
|
65
|
+
subscriptionId: target.subscriptionId,
|
|
66
|
+
resourceGroup: target.resourceGroup,
|
|
67
|
+
location: target.location,
|
|
68
|
+
deployed: new Map(),
|
|
69
|
+
http,
|
|
70
|
+
base: target.endpoint,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function clientOf(target, http) {
|
|
74
|
+
return { endpoint: target.endpoint, subscriptionId: target.subscriptionId, resourceGroup: target.resourceGroup, http };
|
|
75
|
+
}
|
|
76
|
+
function readTemplate(path) {
|
|
77
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
function templateDigest(path) {
|
|
80
|
+
try {
|
|
81
|
+
return `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A 404 that means the resource is not there, as opposed to one that means the
|
|
89
|
+
* subscription is not: ARM answers `SubscriptionNotFound` with a 404 too, and
|
|
90
|
+
* planning every resource as a create on that answer would be wrong. A missing
|
|
91
|
+
* resource group is absence, since `azApply` creates the group first.
|
|
92
|
+
*/
|
|
93
|
+
function isAbsent(err) {
|
|
94
|
+
return isNotFound(err) && !(err instanceof AzureReadError && /^SubscriptionNotFound$/i.test(err.code ?? ""));
|
|
95
|
+
}
|
|
96
|
+
// ── The declared side ────────────────────────────────────────────────────────
|
|
97
|
+
/** An expression that reads a resource's runtime state, which only an apply knows. */
|
|
98
|
+
const RUNTIME_READ = /\b(reference|list[A-Za-z]*)\s*\(/;
|
|
99
|
+
/**
|
|
100
|
+
* Evaluate one string the way `azApply` would, leaving an expression that
|
|
101
|
+
* reads runtime state (`reference()`, `listKeys()`) or uses a function the
|
|
102
|
+
* evaluator lacks as written. The noise rules read a bracket expression on
|
|
103
|
+
* the declared side as unresolved, so it is never compared, and the plan
|
|
104
|
+
* makes no `listKeys` call.
|
|
105
|
+
*/
|
|
106
|
+
async function evalStaticString(s, ctx) {
|
|
107
|
+
if (!(s.startsWith("[") && s.endsWith("]")) || s.startsWith("[["))
|
|
108
|
+
return evalArmString(s, ctx);
|
|
109
|
+
if (RUNTIME_READ.test(s))
|
|
110
|
+
return s;
|
|
111
|
+
try {
|
|
112
|
+
return await evalArmString(s, ctx);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function evalStatic(value, ctx) {
|
|
119
|
+
if (typeof value === "string")
|
|
120
|
+
return evalStaticString(value, ctx);
|
|
121
|
+
if (Array.isArray(value))
|
|
122
|
+
return Promise.all(value.map((v) => evalStatic(v, ctx)));
|
|
123
|
+
if (value && typeof value === "object") {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const [k, v] of Object.entries(value))
|
|
126
|
+
out[String(await evalStaticString(k, ctx))] = await evalStatic(v, ctx);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The fields `azApply` PUTs for a resource, in the flat shape deep
|
|
133
|
+
* observation reads a live resource into. The ownership tags `azApply` adds
|
|
134
|
+
* are left out: the noise rules drop them from the live side too.
|
|
135
|
+
*/
|
|
136
|
+
async function declaredTree(resource, name, ctx) {
|
|
137
|
+
const body = { name };
|
|
138
|
+
if (resource.location !== undefined)
|
|
139
|
+
body.location = (await evalStatic(resource.location, ctx));
|
|
140
|
+
for (const field of ["sku", "kind", "identity", "tags", "zones", "plan", "properties"]) {
|
|
141
|
+
if (resource[field] !== undefined)
|
|
142
|
+
body[field] = await evalStatic(resource[field], ctx);
|
|
143
|
+
}
|
|
144
|
+
return buildLiveProperties(body, name);
|
|
145
|
+
}
|
|
146
|
+
function attributeOf(path, before, after, hasBefore, hasAfter) {
|
|
147
|
+
if (before === MASKED || after === MASKED)
|
|
148
|
+
return { path, sensitive: true };
|
|
149
|
+
if (after === UNRESOLVED)
|
|
150
|
+
return { path, ...(hasBefore ? { before } : {}), unknown: true };
|
|
151
|
+
return { path, ...(hasBefore ? { before } : {}), ...(hasAfter ? { after } : {}) };
|
|
152
|
+
}
|
|
153
|
+
/** Read the group and compare it with the template. Pure apart from GETs. */
|
|
154
|
+
async function readPlan(member, target, input, http) {
|
|
155
|
+
const template = readTemplate(input.template);
|
|
156
|
+
const resources = template.resources ?? [];
|
|
157
|
+
const ctx = evalCtxOf(target, http);
|
|
158
|
+
const client = clientOf(target, http);
|
|
159
|
+
const entries = [];
|
|
160
|
+
const holes = [];
|
|
161
|
+
const planned = [];
|
|
162
|
+
const base = (type, name) => ({
|
|
163
|
+
member,
|
|
164
|
+
lexicon: "azure",
|
|
165
|
+
planner: "chant",
|
|
166
|
+
address: `${type}/${name}`,
|
|
167
|
+
type,
|
|
168
|
+
name,
|
|
169
|
+
});
|
|
170
|
+
for (const resource of orderArmResources(resources)) {
|
|
171
|
+
const name = String(await evalStaticString(resource.name, ctx));
|
|
172
|
+
const address = `${resource.type}/${name}`;
|
|
173
|
+
if (name.startsWith("[")) {
|
|
174
|
+
holes.push({ address, type: resource.type, reason: "the resource's name reads runtime state; known only at apply" });
|
|
175
|
+
planned.push({ resource, name, action: "unread" });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const declaredRaw = await declaredTree(resource, name, ctx);
|
|
179
|
+
let live;
|
|
180
|
+
try {
|
|
181
|
+
live = (await getResource(client, resource.type, name, resource.apiVersion));
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (!isAbsent(err)) {
|
|
185
|
+
const failure = classifyArmFailure(err);
|
|
186
|
+
holes.push({ address, type: resource.type, reason: `${failure.reason}: ${failure.detail}` });
|
|
187
|
+
planned.push({ resource, name, action: "unread" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const opts = { entityType: resource.type, hooks: azureDeepNormalizationHooks };
|
|
192
|
+
if (!live) {
|
|
193
|
+
const declared = normalizeDeepProperties(declaredRaw, { ...opts, side: "declared" });
|
|
194
|
+
const attributes = [...flattenDeepProperties(declared, { ...opts, side: "declared" })]
|
|
195
|
+
.map(([path, value]) => attributeOf(path, undefined, value, false, true))
|
|
196
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
197
|
+
entries.push({ ...base(resource.type, name), action: "create", attributes });
|
|
198
|
+
planned.push({ resource, name, action: "create" });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
rememberDeployed(ctx, resource.type, name, live);
|
|
202
|
+
const liveRaw = buildLiveProperties(live, name);
|
|
203
|
+
const declared = normalizeDeepProperties(declaredRaw, { ...opts, side: "declared", counterpartPaths: deepPathSet(liveRaw) });
|
|
204
|
+
const liveTree = normalizeDeepProperties(liveRaw, { ...opts, side: "live", counterpartPaths: deepPathSet(declaredRaw) });
|
|
205
|
+
const diff = diffDeep({
|
|
206
|
+
declared: { [address]: { type: resource.type, properties: declared } },
|
|
207
|
+
live: { resources: { [address]: { type: resource.type, properties: liveTree } }, unobserved: {} },
|
|
208
|
+
hooks: azureDeepNormalizationHooks,
|
|
209
|
+
});
|
|
210
|
+
// A masked value ARM does not return (a password) is write-only, not a change.
|
|
211
|
+
const changes = (diff.drifted[0]?.changes ?? []).filter((c) => !(c.kind === "absent" && c.declared === MASKED));
|
|
212
|
+
const id = typeof live.id === "string" ? { id: live.id } : {};
|
|
213
|
+
if (changes.length === 0) {
|
|
214
|
+
entries.push({ ...base(resource.type, name), ...id, action: "no-op", attributes: [] });
|
|
215
|
+
planned.push({ resource, name, action: "no-op" });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const attributes = changes
|
|
219
|
+
.map((c) => attributeOf(c.path, c.live, c.declared, c.kind === "changed", true))
|
|
220
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
221
|
+
entries.push({ ...base(resource.type, name), ...id, action: "update", disruption: "in-place", attributes });
|
|
222
|
+
planned.push({ resource, name, action: "update" });
|
|
223
|
+
}
|
|
224
|
+
if (input.prune) {
|
|
225
|
+
// The set `pruneArmOrphans` deletes: chant-owned, of any type, and not declared by (type, name).
|
|
226
|
+
const keep = new Set(planned.map((p) => `${p.resource.type}\0${p.name}`));
|
|
227
|
+
try {
|
|
228
|
+
for (const item of await listResources(client)) {
|
|
229
|
+
if (typeof item.type !== "string" || typeof item.name !== "string")
|
|
230
|
+
continue;
|
|
231
|
+
if (!isChantOwned(item.tags))
|
|
232
|
+
continue;
|
|
233
|
+
if (keep.has(`${item.type}\0${item.name}`))
|
|
234
|
+
continue;
|
|
235
|
+
entries.push({
|
|
236
|
+
...base(item.type, item.name),
|
|
237
|
+
...(typeof item.id === "string" ? { id: item.id } : {}),
|
|
238
|
+
action: "delete",
|
|
239
|
+
disruption: "destroy",
|
|
240
|
+
attributes: [],
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (!isAbsent(err)) {
|
|
246
|
+
const failure = classifyArmFailure(err);
|
|
247
|
+
holes.push({ address: target.resourceGroup, type: "Microsoft.Resources/resourceGroups", reason: `prune not read: ${failure.reason}: ${failure.detail}` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
entries.sort((a, b) => a.address.localeCompare(b.address));
|
|
252
|
+
holes.sort((a, b) => a.address.localeCompare(b.address));
|
|
253
|
+
return { entries, holes, resources: planned, live: ctx, template };
|
|
254
|
+
}
|
|
255
|
+
function armPlanDigest(ctx, target, input, unresolved, plan) {
|
|
256
|
+
return computePlanDigest("arm-deploy-plan", {
|
|
257
|
+
env: ctx.env,
|
|
258
|
+
...target,
|
|
259
|
+
template: input.template,
|
|
260
|
+
templateDigest: templateDigest(input.template),
|
|
261
|
+
prune: input.prune ?? false,
|
|
262
|
+
unresolved,
|
|
263
|
+
...(plan ? { entries: plan.entries, holes: plan.holes } : {}),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** The template's outputs evaluated against `ctx`, leaving out each one that evaluates to nothing. */
|
|
267
|
+
async function evaluateOutputs(outputs, ctx, only) {
|
|
268
|
+
const known = {};
|
|
269
|
+
const unknown = [];
|
|
270
|
+
for (const [name, output] of Object.entries(outputs ?? {})) {
|
|
271
|
+
if (only && !only(name)) {
|
|
272
|
+
unknown.push(name);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let value;
|
|
276
|
+
try {
|
|
277
|
+
value = await evalArm(output?.value, ctx);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
value = undefined;
|
|
281
|
+
}
|
|
282
|
+
if (value === undefined)
|
|
283
|
+
unknown.push(name);
|
|
284
|
+
else
|
|
285
|
+
known[name] = value;
|
|
286
|
+
}
|
|
287
|
+
return { known, unknown: unknown.sort() };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* What the plan says of the template's outputs: an output whose value reads
|
|
291
|
+
* only resources the plan leaves as they are is known from the live read.
|
|
292
|
+
* Every other one is known only after apply.
|
|
293
|
+
*/
|
|
294
|
+
async function plannedOutputs(plan) {
|
|
295
|
+
const names = new Set(plan.resources.map((r) => r.resource.name));
|
|
296
|
+
const changing = new Set(plan.resources.filter((r) => r.action !== "no-op").map((r) => r.resource.name));
|
|
297
|
+
return evaluateOutputs(plan.template.outputs, plan.live, (name) => {
|
|
298
|
+
const value = plan.template.outputs?.[name]?.value;
|
|
299
|
+
const reads = armDependencies({ type: "", apiVersion: "", name: "", properties: value }, names);
|
|
300
|
+
return reads.every((r) => !changing.has(r));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Read every template resource live and evaluate the template's outputs against them. */
|
|
304
|
+
async function liveOutputs(target, input, http) {
|
|
305
|
+
const template = readTemplate(input.template);
|
|
306
|
+
if (Object.keys(template.outputs ?? {}).length === 0)
|
|
307
|
+
return {};
|
|
308
|
+
const ctx = evalCtxOf(target, http);
|
|
309
|
+
const client = clientOf(target, http);
|
|
310
|
+
for (const resource of orderArmResources(template.resources ?? [])) {
|
|
311
|
+
const name = String(await evalStaticString(resource.name, ctx));
|
|
312
|
+
if (name.startsWith("["))
|
|
313
|
+
continue;
|
|
314
|
+
try {
|
|
315
|
+
rememberDeployed(ctx, resource.type, name, await getResource(client, resource.type, name, resource.apiVersion));
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (!isAbsent(err))
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return (await evaluateOutputs(template.outputs, ctx)).known;
|
|
323
|
+
}
|
|
324
|
+
async function planArmDeploy(http, ctx, input) {
|
|
325
|
+
const target = targetOf(ctx, input);
|
|
326
|
+
const member = target.resourceGroup;
|
|
327
|
+
const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
|
|
328
|
+
const memberOf = (planDigest, holes) => ({
|
|
329
|
+
member,
|
|
330
|
+
lexicon: "azure",
|
|
331
|
+
planner: "chant",
|
|
332
|
+
status: "planned",
|
|
333
|
+
planDigest,
|
|
334
|
+
holes,
|
|
335
|
+
});
|
|
336
|
+
if (unresolved.length > 0) {
|
|
337
|
+
const planDigest = armPlanDigest(ctx, target, input, unresolved);
|
|
338
|
+
const hole = { address: member, type: "Microsoft.Resources/resourceGroups", reason: `inputs from ${unresolved.join(", ")}; re-checked on apply` };
|
|
339
|
+
return { member, planDigest, changeSet: { member: memberOf(planDigest, [hole]), entries: [] } };
|
|
340
|
+
}
|
|
341
|
+
const plan = await readPlan(member, target, input, http);
|
|
342
|
+
const planDigest = armPlanDigest(ctx, target, input, unresolved, plan);
|
|
343
|
+
const outputs = await plannedOutputs(plan);
|
|
344
|
+
// A plan with a hole hands `run` nothing to hold it to, as `cfn-deploy`'s does not.
|
|
345
|
+
const artifact = plan.holes.length === 0 ? { kind: "arm-deploy-plan", resourceGroup: member, planDigest } : undefined;
|
|
346
|
+
return {
|
|
347
|
+
member,
|
|
348
|
+
planDigest,
|
|
349
|
+
...(artifact ? { artifact } : {}),
|
|
350
|
+
changeSet: { member: memberOf(planDigest, plan.holes), entries: plan.entries },
|
|
351
|
+
outputs,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The `arm-deploy` capability over an injectable HTTP transport (the
|
|
356
|
+
* `AzHttp` seam `azApply` and the read client share). Tests pass a fake;
|
|
357
|
+
* the default is `fetch`.
|
|
358
|
+
*/
|
|
359
|
+
export function createArmDeployCapability(options = {}) {
|
|
360
|
+
const http = options.http ?? (async (method, url, body, signal) => {
|
|
361
|
+
const res = await fetch(url, {
|
|
362
|
+
method,
|
|
363
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
364
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
365
|
+
signal,
|
|
366
|
+
});
|
|
367
|
+
return { status: res.status, text: await res.text() };
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
kind: "arm-deploy",
|
|
371
|
+
plan(ctx, input) {
|
|
372
|
+
return planArmDeploy(http, ctx, input);
|
|
373
|
+
},
|
|
374
|
+
outputs(ctx, input) {
|
|
375
|
+
return liveOutputs(targetOf(ctx, input), input, http);
|
|
376
|
+
},
|
|
377
|
+
async run(ctx, input) {
|
|
378
|
+
const target = targetOf(ctx, input);
|
|
379
|
+
const approved = ctx.plans?.[target.resourceGroup];
|
|
380
|
+
if (isArmDeployPlanArtifact(approved)) {
|
|
381
|
+
const now = await planArmDeploy(http, { env: ctx.env, component: ctx.component }, input);
|
|
382
|
+
if (now.planDigest !== approved.planDigest)
|
|
383
|
+
throw new ArmDeployPlanMovedError(target.resourceGroup, approved.planDigest, now.planDigest);
|
|
384
|
+
}
|
|
385
|
+
const result = await azApply({
|
|
386
|
+
templatePath: input.template,
|
|
387
|
+
resourceGroup: target.resourceGroup,
|
|
388
|
+
location: target.location,
|
|
389
|
+
endpoint: target.endpoint,
|
|
390
|
+
subscriptionId: target.subscriptionId,
|
|
391
|
+
prune: input.prune ?? false,
|
|
392
|
+
}, undefined, http);
|
|
393
|
+
return { ...result, outputs: await liveOutputs(target, input, http) };
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** The default `arm-deploy` capability, over `fetch`. */
|
|
398
|
+
export const armDeployCapability = createArmDeployCapability();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
2
|
+
export declare const AZURE_VERB_FAMILIES: {
|
|
3
|
+
readonly apply: readonly ["arm-deploy"];
|
|
4
|
+
};
|
|
5
|
+
export declare const azureCapabilityPlugin: CapabilityPlugin;
|
|
6
|
+
//# sourceMappingURL=capability-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAGzG,eAAO,MAAM,mBAAmB;aAC9B,KAAK,YAAG,YAAY;CACZ,CAAC;AAEX,eAAO,MAAM,qBAAqB,EAAE,gBAWnC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ownPackageVersion } from "@intentius/chant/components/capability-plugin";
|
|
2
|
+
import { armDeployCapability } from "./arm-deploy.js";
|
|
3
|
+
export const AZURE_VERB_FAMILIES = {
|
|
4
|
+
apply: ["arm-deploy"],
|
|
5
|
+
};
|
|
6
|
+
export const azureCapabilityPlugin = {
|
|
7
|
+
name: "azure",
|
|
8
|
+
get version() {
|
|
9
|
+
return ownPackageVersion(import.meta.url);
|
|
10
|
+
},
|
|
11
|
+
capabilities() {
|
|
12
|
+
return [armDeployCapability];
|
|
13
|
+
},
|
|
14
|
+
families() {
|
|
15
|
+
return AZURE_VERB_FAMILIES;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { azureCapabilityPlugin, AZURE_VERB_FAMILIES } from "./capability-plugin.js";
|
|
2
|
+
export { armDeployCapability, createArmDeployCapability, ArmDeployPlanMovedError, type ArmDeployInput, type ArmDeployOutput, type ArmDeployPlanArtifact, } from "./arm-deploy.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-app.d.ts","sourceRoot":"","sources":["../../src/composites/container-app.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,cAAc,EACd,MAAM,EACN,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACd,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE;QACT,cAAc,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC3D,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;IACpD,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB;;;
|
|
1
|
+
{"version":3,"file":"container-app.d.ts","sourceRoot":"","sources":["../../src/composites/container-app.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,cAAc,EACd,MAAM,EACN,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACd,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE;QACT,cAAc,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC3D,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;IACpD,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB;;;EAyCP,CAAC;AAa1B,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACrE,WAAW,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5E,YAAY,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACxE,CAAC;CACH;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,YAAY,CAAC,OAAO,qBAAqB,CAAC,CAAC;IACtD,QAAQ,EAAE,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC;IAC/C,WAAW,EAAE,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;IACtD,YAAY,EAAE,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB;;;;;EAsEP,CAAC"}
|
|
@@ -24,22 +24,39 @@ export const ContainerAppNetwork = Composite((props) => {
|
|
|
24
24
|
const mergedTags = { "managed-by": "chant", ...tags };
|
|
25
25
|
const vnetName = `${name}-vnet`;
|
|
26
26
|
const delegations = [{ name: "apps", properties: { serviceName: "Microsoft.App/environments" } }];
|
|
27
|
+
const subnet = new Subnet(mergeDefaults({
|
|
28
|
+
name: `${vnetName}/apps`,
|
|
29
|
+
addressPrefix: subnetPrefix,
|
|
30
|
+
delegations,
|
|
31
|
+
}, defaults?.subnet), { apiVersion: "2023-05-01" });
|
|
27
32
|
// The subnet is also listed inline, as VnetDefault does, so a later
|
|
28
|
-
// deployment of the VNet does not try to remove it.
|
|
33
|
+
// deployment of the VNet does not try to remove it. The inline entry is
|
|
34
|
+
// built from the Subnet resource's merged props, so `defaults.subnet`
|
|
35
|
+
// reaches both copies. The apps entry always comes first; a caller's
|
|
36
|
+
// `defaults.virtualNetwork.subnets` adds the other subnets after it, and
|
|
37
|
+
// an entry of theirs named "apps" is replaced by this one.
|
|
38
|
+
const { subnets: extraSubnets, ...vnetDefaults } = (defaults?.virtualNetwork ?? {});
|
|
29
39
|
const virtualNetwork = new VirtualNetwork(mergeDefaults({
|
|
30
40
|
name: vnetName,
|
|
31
41
|
location,
|
|
32
42
|
tags: mergedTags,
|
|
33
43
|
addressSpace: { addressPrefixes: [addressSpace] },
|
|
34
|
-
subnets: [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
delegations,
|
|
40
|
-
}, defaults?.subnet), { apiVersion: "2023-05-01" });
|
|
44
|
+
subnets: [
|
|
45
|
+
inlineSubnet("apps", subnetProps(subnet)),
|
|
46
|
+
...(extraSubnets ?? []).filter((s) => s?.name !== "apps"),
|
|
47
|
+
],
|
|
48
|
+
}, vnetDefaults), { apiVersion: "2023-05-01" });
|
|
41
49
|
return { virtualNetwork, subnet };
|
|
42
50
|
}, "ContainerAppNetwork");
|
|
51
|
+
/** A Subnet resource's props, flat or under `properties`. */
|
|
52
|
+
function subnetProps(subnet) {
|
|
53
|
+
return subnet.props;
|
|
54
|
+
}
|
|
55
|
+
/** The VNet's inline entry for a subnet, from that Subnet resource's props. */
|
|
56
|
+
function inlineSubnet(name, props) {
|
|
57
|
+
const { name: _name, id: _id, properties, ...flat } = props;
|
|
58
|
+
return { name, properties: { ...flat, ...properties } };
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* A Container Apps environment in a subnet, shipping logs to its own Log
|
|
45
62
|
* Analytics workspace, with one app running as a user-assigned identity.
|
package/dist/deep-observe.d.ts
CHANGED
|
@@ -49,6 +49,36 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { type AzHttp } from "./op/activities/az-apply.js";
|
|
51
51
|
import type { DeepNormalizationHooks, DeepObservationResult } from "@intentius/chant/lexicon";
|
|
52
|
+
/** The full `az resource show -o json` shape this reader reads (a superset of the thin path's `AzResourceShowResponse`). */
|
|
53
|
+
export interface ArmResourceShowResponse {
|
|
54
|
+
id?: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
type?: string;
|
|
57
|
+
location?: string;
|
|
58
|
+
kind?: string;
|
|
59
|
+
sku?: unknown;
|
|
60
|
+
identity?: unknown;
|
|
61
|
+
tags?: Record<string, string>;
|
|
62
|
+
zones?: string[];
|
|
63
|
+
plan?: unknown;
|
|
64
|
+
etag?: string;
|
|
65
|
+
systemData?: unknown;
|
|
66
|
+
properties?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reshape one `az resource show` response into chant's own flat declared
|
|
70
|
+
* vocabulary: the resource-level fields ARM returns as siblings of
|
|
71
|
+
* `properties`, plus every `properties.*` field spread onto that same top
|
|
72
|
+
* level (see the module doc). `id`/`type`/`etag`/`systemData` are envelope
|
|
73
|
+
* bookkeeping with no declared counterpart and are never read in at all.
|
|
74
|
+
*
|
|
75
|
+
* `queriedName` is the declared name the GET was addressed by. A child
|
|
76
|
+
* resource is declared as `vnet/subnet` while ARM answers with the leaf
|
|
77
|
+
* (`name: "subnet"`) — the same identity in two spellings, since the URL that
|
|
78
|
+
* returned 200 is the declared name. The leaf is folded back to the declared
|
|
79
|
+
* spelling; a name that is not the leaf of the query is kept as returned.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildLiveProperties(obj: ArmResourceShowResponse, queriedName?: string): Record<string, unknown>;
|
|
52
82
|
/**
|
|
53
83
|
* Server-populated wherever they appear — matched on the final path segment,
|
|
54
84
|
* the same technique AWS's `AWS_READ_ONLY_NAMES` uses, because ARM repeats
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,KAAK,EAGV,sBAAsB,EACtB,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,KAAK,EAGV,sBAAsB,EACtB,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAQlC,4HAA4H;AAC5H,MAAM,WAAW,uBAAuB;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,uBAAuB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqB/G;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CASpD,CAAC;AAUH;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAI1D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI1E,CAAC;AA+DF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAyBD;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,EAAE,sBA2EzC,CAAC;AAIF,MAAM,WAAW,uBAAuB;IACtC,4FAA4F;IAC5F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,qBAAqB,CAAC,CAkEhC"}
|
package/dist/deep-observe.js
CHANGED
|
@@ -68,7 +68,7 @@ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
|
|
|
68
68
|
* returned 200 is the declared name. The leaf is folded back to the declared
|
|
69
69
|
* spelling; a name that is not the leaf of the query is kept as returned.
|
|
70
70
|
*/
|
|
71
|
-
function buildLiveProperties(obj, queriedName) {
|
|
71
|
+
export function buildLiveProperties(obj, queriedName) {
|
|
72
72
|
const out = {};
|
|
73
73
|
if (obj.name !== undefined) {
|
|
74
74
|
out.name =
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { defaultTags, type TagEntry, type DefaultTags } from "./default-tags.js"
|
|
|
8
8
|
export { deploymentScope, isDeploymentScope, type DeploymentScope, type DeployScope } from "./deploy-scopes.js";
|
|
9
9
|
export { azureSerializer } from "./serializer.js";
|
|
10
10
|
export { azurePlugin } from "./plugin.js";
|
|
11
|
+
export { azureCapabilityPlugin } from "./components/capability-plugin.js";
|
|
11
12
|
export { Parameter } from "./parameter.js";
|
|
12
13
|
export { observeResourcesDeepAzure, azureDeepNormalizationHooks, AZURE_READ_ONLY_NAMES, AZURE_SERVER_COMPUTED_NAMES, AZURE_SERVICE_DEFAULTS, } from "./deep-observe.js";
|
|
13
14
|
export { ResourceId, ResourceIdIntrinsic, Reference, ReferenceIntrinsic, Concat, ConcatIntrinsic, ResourceGroup, ResourceGroupIntrinsic, Subscription, SubscriptionIntrinsic, UniqueString, UniqueStringIntrinsic, Format, FormatIntrinsic, If, IfIntrinsic, ListKeys, ListKeysIntrinsic, } from "./intrinsics.js";
|