@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
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGxG,OAAO,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC7G,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ export { defaultTags } from "./default-tags.js";
|
|
|
15
15
|
export { deploymentScope, isDeploymentScope } from "./deploy-scopes.js";
|
|
16
16
|
export { azureSerializer } from "./serializer.js";
|
|
17
17
|
export { azurePlugin } from "./plugin.js";
|
|
18
|
+
// Core's capability-plugin loader scans this package's exports and loads
|
|
19
|
+
// `azureCapabilityPlugin` when a project's chant.config lists this lexicon (#4273).
|
|
20
|
+
export { azureCapabilityPlugin } from "./components/capability-plugin.js";
|
|
18
21
|
export { Parameter } from "./parameter.js";
|
|
19
22
|
// Deep observation (#1086): the `az resource show` reader and the noise
|
|
20
23
|
// rules it shares with core's normalization pass.
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "bc610c4143712ac195d56822a922054f5f94b9251a30403488a71c273bae7278",
|
|
5
5
|
"meta.json": "d2521221ed616d882ded4296f0071685bd74e4b645d55dabc9685733c37dc419",
|
|
6
6
|
"types/index.d.ts": "29631032bda9533849178968290e65d72250eab0b8bab424f2d9ef9b4e8546a4",
|
|
7
7
|
"rules/hardcoded-location.ts": "0b895449c8c94066e81721c46be24249231029fe13b471477456df5c4e45b8bf",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"skills/chant-azure-patterns.md": "2d8cb8bbeda73b695856cec4ddaf5b3a0efce76e4ee6e3cf14b90ca69dee5757",
|
|
38
38
|
"skills/chant-azure-aks.md": "2d4e0098c1a22b54ffadd410564d9df0f3a04cb4eb7c6261f20ae33106e7aca8"
|
|
39
39
|
},
|
|
40
|
-
"composite": "
|
|
40
|
+
"composite": "f5d39f472b9337b9f6729b1a6adf719e59ad3dada43de344bdf112c2f0b3b409"
|
|
41
41
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-azure",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.40",
|
|
4
4
|
"description": "Azure lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
"types": "./dist/*.d.ts",
|
|
40
40
|
"default": "./dist/*.js"
|
|
41
41
|
},
|
|
42
|
+
"./components": {
|
|
43
|
+
"development": "./src/components/index.ts",
|
|
44
|
+
"types": "./dist/components/index.d.ts",
|
|
45
|
+
"default": "./dist/components/index.js"
|
|
46
|
+
},
|
|
42
47
|
"./detect": {
|
|
43
48
|
"development": "./src/detect.ts",
|
|
44
49
|
"types": "./dist/detect.d.ts",
|
|
@@ -74,6 +79,6 @@
|
|
|
74
79
|
"typescript": "^5.9.3"
|
|
75
80
|
},
|
|
76
81
|
"peerDependencies": {
|
|
77
|
-
"@intentius/chant": "^0.124.
|
|
82
|
+
"@intentius/chant": "^0.124.40"
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `arm-deploy`'s plan and apply against floci-az (#4273), over `fetch`. Set
|
|
3
|
+
* CHANT_FLOCI_AZ_ENDPOINT to a running floci-az, for example
|
|
4
|
+
* `docker run -d --name chant-floci-az-e2e -p 4577:4577 floci/floci-az:0.10.0`
|
|
5
|
+
* and `CHANT_FLOCI_AZ_ENDPOINT=http://localhost:4577`. Skipped without it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { azDelete } from "../op/activities/az-apply";
|
|
13
|
+
import { ArmDeployPlanMovedError, createArmDeployCapability } from "./arm-deploy";
|
|
14
|
+
|
|
15
|
+
const endpoint = process.env.CHANT_FLOCI_AZ_ENDPOINT;
|
|
16
|
+
|
|
17
|
+
describe.skipIf(!endpoint)("arm-deploy plan against floci-az (#4273)", () => {
|
|
18
|
+
const resourceGroup = `chant-arm-plan-${process.pid}`;
|
|
19
|
+
const ctx = { env: "local", component: resourceGroup };
|
|
20
|
+
const saved = { ...process.env };
|
|
21
|
+
let dir: string;
|
|
22
|
+
let template: string;
|
|
23
|
+
const account = (accessTier: string) => ({
|
|
24
|
+
type: "Microsoft.Storage/storageAccounts",
|
|
25
|
+
apiVersion: "2023-01-01",
|
|
26
|
+
name: `chantplan${process.pid}`,
|
|
27
|
+
location: "[resourceGroup().location]",
|
|
28
|
+
sku: { name: "Standard_LRS" },
|
|
29
|
+
kind: "StorageV2",
|
|
30
|
+
properties: { accessTier },
|
|
31
|
+
});
|
|
32
|
+
const write = (accessTier: string): void =>
|
|
33
|
+
writeFileSync(
|
|
34
|
+
template,
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
resources: [account(accessTier)],
|
|
37
|
+
outputs: { group: { type: "string", value: "[resourceGroup().name]" } },
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
process.env.AZURE_ENDPOINT_URL = endpoint;
|
|
43
|
+
dir = mkdtempSync(join(tmpdir(), "arm-plan-floci-az-"));
|
|
44
|
+
template = join(dir, "deploymentTemplate.json");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
try {
|
|
49
|
+
await azDelete({ templatePath: template, resourceGroup, endpoint });
|
|
50
|
+
} catch { /* never created */ }
|
|
51
|
+
process.env = saved;
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("plans a create, applies it, plans no change, plans an update and refuses a plan the template moved past", async () => {
|
|
56
|
+
const capability = createArmDeployCapability();
|
|
57
|
+
const input = { template, resourceGroup };
|
|
58
|
+
|
|
59
|
+
write("Hot");
|
|
60
|
+
const created = await capability.plan!(ctx, input);
|
|
61
|
+
expect(created.changeSet!.entries.map((e) => e.action)).toEqual(["create"]);
|
|
62
|
+
const first = await capability.run({ ...ctx, plans: { [resourceGroup]: created.artifact } }, input);
|
|
63
|
+
expect(first.applied).toHaveLength(1);
|
|
64
|
+
expect(first.outputs).toEqual({ group: resourceGroup });
|
|
65
|
+
|
|
66
|
+
const same = await capability.plan!(ctx, input);
|
|
67
|
+
expect(same.changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
|
|
68
|
+
|
|
69
|
+
write("Cool");
|
|
70
|
+
const updated = await capability.plan!(ctx, input);
|
|
71
|
+
expect(updated.changeSet!.entries.map((e) => [e.action, e.attributes])).toEqual([
|
|
72
|
+
["update", [{ path: "accessTier", before: "Hot", after: "Cool" }]],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// The template moves after the plan: run refuses and changes nothing.
|
|
76
|
+
write("Hot");
|
|
77
|
+
await expect(capability.run({ ...ctx, plans: { [resourceGroup]: updated.artifact } }, input)).rejects.toBeInstanceOf(ArmDeployPlanMovedError);
|
|
78
|
+
expect((await capability.plan!(ctx, input)).changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
|
|
79
|
+
}, 120_000);
|
|
80
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { isPlanMovedError, type DeployContext } from "@intentius/chant/components/capability";
|
|
6
|
+
import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
7
|
+
import { deployUnits } from "@intentius/chant/components/deploy-units";
|
|
8
|
+
import type { AzHttp } from "../op/activities/az-apply";
|
|
9
|
+
import { ArmDeployPlanMovedError, createArmDeployCapability, type ArmDeployInput } from "./arm-deploy";
|
|
10
|
+
import { azureCapabilityPlugin } from "./capability-plugin";
|
|
11
|
+
|
|
12
|
+
const ENDPOINT = "http://floci-az.test";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An in-memory ARM resource group behind the `AzHttp` seam: PUT stores a
|
|
16
|
+
* resource the way ARM answers (id, leaf name, type, provisioningState),
|
|
17
|
+
* GET reads it or answers 404, the group listing lists it and DELETE removes it.
|
|
18
|
+
*/
|
|
19
|
+
function fakeArm(options: { dropTags?: boolean; fail?: (method: string, url: string) => number | undefined } = {}) {
|
|
20
|
+
const store = new Map<string, Record<string, unknown>>();
|
|
21
|
+
const calls: Array<{ method: string; url: string }> = [];
|
|
22
|
+
const groups = new Set<string>();
|
|
23
|
+
const http: AzHttp = async (method, url, body) => {
|
|
24
|
+
calls.push({ method, url });
|
|
25
|
+
const failed = options.fail?.(method, url);
|
|
26
|
+
if (failed) return { status: failed, text: JSON.stringify({ error: { code: "AuthorizationFailed", message: "denied" } }) };
|
|
27
|
+
const path = new URL(url).pathname;
|
|
28
|
+
const key = path.toLowerCase();
|
|
29
|
+
if (/\/resourcegroups\/[^/]+$/.test(key)) {
|
|
30
|
+
if (method === "PUT") groups.add(key);
|
|
31
|
+
return { status: 200, text: "{}" };
|
|
32
|
+
}
|
|
33
|
+
if (key.endsWith("/resources")) {
|
|
34
|
+
const prefix = key.slice(0, -"/resources".length);
|
|
35
|
+
if (!groups.has(prefix)) return { status: 404, text: JSON.stringify({ error: { code: "ResourceGroupNotFound" } }) };
|
|
36
|
+
return { status: 200, text: JSON.stringify({ value: [...store.entries()].filter(([k]) => k.startsWith(prefix)).map(([, v]) => v) }) };
|
|
37
|
+
}
|
|
38
|
+
if (method === "PUT") {
|
|
39
|
+
const b = body as Record<string, unknown>;
|
|
40
|
+
const m = /\/providers\/([^/]+\/[^/]+)\/([^/]+)$/.exec(path)!;
|
|
41
|
+
const stored = {
|
|
42
|
+
id: path,
|
|
43
|
+
name: m[2],
|
|
44
|
+
type: m[1],
|
|
45
|
+
...b,
|
|
46
|
+
...(options.dropTags ? { tags: {} } : {}),
|
|
47
|
+
properties: { ...((b.properties as Record<string, unknown>) ?? {}), provisioningState: "Succeeded" },
|
|
48
|
+
};
|
|
49
|
+
store.set(key, stored);
|
|
50
|
+
return { status: 200, text: JSON.stringify(stored) };
|
|
51
|
+
}
|
|
52
|
+
if (method === "DELETE") return { status: store.delete(key) ? 200 : 404, text: "" };
|
|
53
|
+
const found = store.get(key);
|
|
54
|
+
return found ? { status: 200, text: JSON.stringify(found) } : { status: 404, text: JSON.stringify({ error: { code: "ResourceNotFound" } }) };
|
|
55
|
+
};
|
|
56
|
+
return { http, calls, store };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const storage = (accessTier: string) => ({
|
|
60
|
+
type: "Microsoft.Storage/storageAccounts",
|
|
61
|
+
apiVersion: "2023-01-01",
|
|
62
|
+
name: "chantst1",
|
|
63
|
+
location: "[resourceGroup().location]",
|
|
64
|
+
sku: { name: "Standard_LRS" },
|
|
65
|
+
kind: "StorageV2",
|
|
66
|
+
properties: { accessTier },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const site = {
|
|
70
|
+
type: "Microsoft.Web/sites",
|
|
71
|
+
apiVersion: "2023-01-01",
|
|
72
|
+
name: "chantsite1",
|
|
73
|
+
location: "eastus",
|
|
74
|
+
properties: {
|
|
75
|
+
serverFarmId: "[resourceId('Microsoft.Web/serverfarms', 'plan1')]",
|
|
76
|
+
storageKey: "[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'chantst1'), '2023-01-01').keys[0].value]",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe("arm-deploy (#4273)", () => {
|
|
81
|
+
let dir: string;
|
|
82
|
+
let template: string;
|
|
83
|
+
const ctx: DeployContext = { env: "local", component: "app" };
|
|
84
|
+
const saved = { ...process.env };
|
|
85
|
+
const write = (resources: unknown[], outputs?: Record<string, unknown>): void =>
|
|
86
|
+
writeFileSync(template, JSON.stringify({ resources, ...(outputs ? { outputs } : {}) }));
|
|
87
|
+
const input = (over: Partial<ArmDeployInput> = {}): ArmDeployInput => ({ template, resourceGroup: "chant-rg", endpoint: ENDPOINT, ...over });
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
dir = mkdtempSync(join(tmpdir(), "arm-deploy-"));
|
|
91
|
+
template = join(dir, "deploymentTemplate.json");
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
rmSync(dir, { recursive: true, force: true });
|
|
95
|
+
process.env = { ...saved };
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("plans a create, applies it through azApply, then plans no change and reads the outputs", async () => {
|
|
99
|
+
const arm = fakeArm();
|
|
100
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
101
|
+
write([storage("Hot")], {
|
|
102
|
+
tier: { type: "string", value: "[reference('chantst1').accessTier]" },
|
|
103
|
+
group: { type: "string", value: "[resourceGroup().name]" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const created = await cap.plan!(ctx, input());
|
|
107
|
+
expect(created.member).toBe("chant-rg");
|
|
108
|
+
expect(created.changeSet!.member).toMatchObject({ member: "chant-rg", lexicon: "azure", planner: "chant", status: "planned", holes: [] });
|
|
109
|
+
const [entry] = created.changeSet!.entries;
|
|
110
|
+
expect(entry).toMatchObject({ address: "Microsoft.Storage/storageAccounts/chantst1", action: "create", lexicon: "azure" });
|
|
111
|
+
expect(entry.attributes).toContainEqual({ path: "accessTier", after: "Hot" });
|
|
112
|
+
expect(entry.attributes).toContainEqual({ path: "location", after: "eastus" });
|
|
113
|
+
// The output reading the new account is known only after apply; the static one now.
|
|
114
|
+
expect(created.outputs).toEqual({ known: { group: "chant-rg" }, unknown: ["tier"] });
|
|
115
|
+
// Planning made no write.
|
|
116
|
+
expect(arm.calls.filter((c) => c.method !== "GET")).toEqual([]);
|
|
117
|
+
|
|
118
|
+
const applied = await cap.run({ ...ctx, plans: { "chant-rg": created.artifact } }, input());
|
|
119
|
+
expect(applied.applied).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "chantst1" }]);
|
|
120
|
+
expect(applied.outputs).toEqual({ tier: "Hot", group: "chant-rg" });
|
|
121
|
+
|
|
122
|
+
const same = await cap.plan!(ctx, input());
|
|
123
|
+
expect(same.changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
|
|
124
|
+
expect(same.outputs).toEqual({ known: { tier: "Hot", group: "chant-rg" }, unknown: [] });
|
|
125
|
+
await expect(cap.outputs!(ctx, input())).resolves.toEqual({ tier: "Hot", group: "chant-rg" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("plans an update with each changed path before and after", async () => {
|
|
129
|
+
const arm = fakeArm();
|
|
130
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
131
|
+
write([storage("Hot")]);
|
|
132
|
+
await cap.run(ctx, input());
|
|
133
|
+
|
|
134
|
+
write([storage("Cool")]);
|
|
135
|
+
const updated = await cap.plan!(ctx, input());
|
|
136
|
+
const [entry] = updated.changeSet!.entries;
|
|
137
|
+
expect(entry).toMatchObject({ action: "update", disruption: "in-place" });
|
|
138
|
+
expect(entry.id).toContain("/storageAccounts/chantst1");
|
|
139
|
+
expect(entry.attributes).toEqual([{ path: "accessTier", before: "Hot", after: "Cool" }]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("refuses to apply when the template moved after the plan, and changes nothing", async () => {
|
|
143
|
+
const arm = fakeArm();
|
|
144
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
145
|
+
write([storage("Hot")]);
|
|
146
|
+
const planned = await cap.plan!(ctx, input());
|
|
147
|
+
write([storage("Cool")]);
|
|
148
|
+
const run = cap.run({ ...ctx, plans: { "chant-rg": planned.artifact } }, input());
|
|
149
|
+
await expect(run).rejects.toBeInstanceOf(ArmDeployPlanMovedError);
|
|
150
|
+
await expect(run).rejects.toSatisfy(isPlanMovedError);
|
|
151
|
+
expect(arm.calls.filter((c) => c.method === "PUT")).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("ownership tags the applier stamps are not a change, even where the emulator drops them", async () => {
|
|
155
|
+
for (const dropTags of [false, true]) {
|
|
156
|
+
const arm = fakeArm({ dropTags });
|
|
157
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
158
|
+
write([storage("Hot")]);
|
|
159
|
+
await cap.run(ctx, input());
|
|
160
|
+
const again = await cap.plan!(ctx, input());
|
|
161
|
+
expect(again.changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a value read from runtime state is unknown, and the plan never calls listKeys", async () => {
|
|
166
|
+
const arm = fakeArm();
|
|
167
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
168
|
+
write([site]);
|
|
169
|
+
const planned = await cap.plan!(ctx, input());
|
|
170
|
+
const [entry] = planned.changeSet!.entries;
|
|
171
|
+
expect(entry.attributes).toContainEqual({ path: "serverFarmId", after: "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/chant-rg/providers/Microsoft.Web/serverfarms/plan1" });
|
|
172
|
+
expect(entry.attributes).toContainEqual({ path: "storageKey", unknown: true });
|
|
173
|
+
expect(arm.calls.some((c) => c.url.includes("listKeys"))).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("with prune, a chant-owned resource the template dropped is a delete and a foreign one is left alone", async () => {
|
|
177
|
+
const arm = fakeArm();
|
|
178
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
179
|
+
write([storage("Hot"), { ...storage("Hot"), name: "chantst2" }]);
|
|
180
|
+
await cap.run(ctx, input());
|
|
181
|
+
await arm.http("PUT", `${ENDPOINT}/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/chant-rg/providers/Microsoft.Storage/storageAccounts/foreign1?api-version=2023-01-01`, { location: "eastus" });
|
|
182
|
+
|
|
183
|
+
write([storage("Hot")]);
|
|
184
|
+
const kept = await cap.plan!(ctx, input());
|
|
185
|
+
expect(kept.changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
|
|
186
|
+
const pruning = await cap.plan!(ctx, input({ prune: true }));
|
|
187
|
+
expect(pruning.changeSet!.entries.map((e) => [e.name, e.action, e.disruption])).toEqual([
|
|
188
|
+
["chantst1", "no-op", undefined],
|
|
189
|
+
["chantst2", "delete", "destroy"],
|
|
190
|
+
]);
|
|
191
|
+
expect(pruning.planDigest).not.toBe(kept.planDigest);
|
|
192
|
+
|
|
193
|
+
const applied = await cap.run({ ...ctx, plans: { "chant-rg": pruning.artifact } }, input({ prune: true }));
|
|
194
|
+
expect(applied.pruned).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "chantst2", deleted: true }]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a resource it cannot read is a hole, and the plan hands run nothing to hold it to", async () => {
|
|
198
|
+
const arm = fakeArm({ fail: (method) => (method === "GET" ? 403 : undefined) });
|
|
199
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
200
|
+
write([storage("Hot")]);
|
|
201
|
+
const planned = await cap.plan!(ctx, input());
|
|
202
|
+
expect(planned.changeSet!.entries).toEqual([]);
|
|
203
|
+
expect(planned.changeSet!.member.holes).toEqual([
|
|
204
|
+
{ address: "Microsoft.Storage/storageAccounts/chantst1", type: "Microsoft.Storage/storageAccounts", reason: expect.stringMatching(/^no-credentials/) },
|
|
205
|
+
]);
|
|
206
|
+
expect(planned.artifact).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a subscription ARM does not know is a hole, not a create of every resource", async () => {
|
|
210
|
+
const http: AzHttp = async () => ({ status: 404, text: JSON.stringify({ error: { code: "SubscriptionNotFound", message: "no such subscription" } }) });
|
|
211
|
+
const cap = createArmDeployCapability({ http });
|
|
212
|
+
write([storage("Hot")]);
|
|
213
|
+
const planned = await cap.plan!(ctx, input());
|
|
214
|
+
expect(planned.changeSet!.entries).toEqual([]);
|
|
215
|
+
expect(planned.changeSet!.member.holes).toHaveLength(1);
|
|
216
|
+
expect(planned.artifact).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("inputs from an upstream not applied yet leave a hole and read nothing", async () => {
|
|
220
|
+
const arm = fakeArm();
|
|
221
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
222
|
+
write([storage("Hot")]);
|
|
223
|
+
const planned = await cap.plan!({ ...ctx, unresolvedInputs: ["network"] }, input());
|
|
224
|
+
expect(planned.changeSet!.member.holes).toEqual([
|
|
225
|
+
{ address: "chant-rg", type: "Microsoft.Resources/resourceGroups", reason: "inputs from network; re-checked on apply" },
|
|
226
|
+
]);
|
|
227
|
+
expect(arm.calls).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("honours AZURE_ENDPOINT_URL and AZURE_SUBSCRIPTION_ID, and defaults the group to the component", async () => {
|
|
231
|
+
process.env.AZURE_ENDPOINT_URL = "http://localhost:4577/";
|
|
232
|
+
process.env.AZURE_SUBSCRIPTION_ID = "sub-test";
|
|
233
|
+
const arm = fakeArm();
|
|
234
|
+
const cap = createArmDeployCapability({ http: arm.http });
|
|
235
|
+
write([storage("Hot")]);
|
|
236
|
+
const planned = await cap.plan!(ctx, { template });
|
|
237
|
+
expect(planned.member).toBe("app");
|
|
238
|
+
expect(arm.calls[0].url).toBe(
|
|
239
|
+
"http://localhost:4577/subscriptions/sub-test/resourceGroups/app/providers/Microsoft.Storage/storageAccounts/chantst1?api-version=2023-01-01",
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("azureCapabilityPlugin (#4273)", () => {
|
|
245
|
+
test("satisfies the CapabilityPlugin contract and registers arm-deploy with plan and outputs", () => {
|
|
246
|
+
expect(isCapabilityPlugin(azureCapabilityPlugin)).toBe(true);
|
|
247
|
+
const [cap] = azureCapabilityPlugin.capabilities();
|
|
248
|
+
expect(cap.kind).toBe("arm-deploy");
|
|
249
|
+
expect(typeof cap.plan).toBe("function");
|
|
250
|
+
expect(typeof cap.outputs).toBe("function");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("an arm-deploy step's unit is its resource group", () => {
|
|
254
|
+
expect(deployUnits([{ steps: [{ kind: "arm-deploy", template: "t.json", resourceGroup: "rg1" }] }])).toEqual([{ unit: "rg1", lexicon: "azure" }]);
|
|
255
|
+
});
|
|
256
|
+
});
|