@intentius/chant-lexicon-azure 0.124.31 → 0.124.33
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/actions/appservice.js +7 -0
- package/dist/actions/compute.js +9 -0
- package/dist/actions/container.js +14 -0
- package/dist/actions/identity.js +11 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/keyvault.js +12 -0
- package/dist/actions/network.js +9 -0
- package/dist/actions/sql.js +9 -0
- package/dist/actions/storage.js +15 -0
- package/dist/api/read-client.js +146 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +83 -0
- package/dist/codegen/generate-cli.js +11 -0
- package/dist/codegen/generate-lexicon.js +77 -0
- package/dist/codegen/generate-typescript.js +163 -0
- package/dist/codegen/generate.js +106 -0
- package/dist/codegen/naming.js +139 -0
- package/dist/codegen/package.js +64 -0
- package/dist/composites/aks-cluster.js +68 -0
- package/dist/composites/app-service.js +62 -0
- package/dist/composites/application-gateway.js +107 -0
- package/dist/composites/catalog.js +856 -0
- package/dist/composites/container-instance.js +36 -0
- package/dist/composites/container-registry.js +62 -0
- package/dist/composites/cosmos-database.js +56 -0
- package/dist/composites/from-arm.js +47 -0
- package/dist/composites/function-app.js +57 -0
- package/dist/composites/index.js +16 -0
- package/dist/composites/keyvault.js +69 -0
- package/dist/composites/landing-zone.js +119 -0
- package/dist/composites/private-endpoint.js +61 -0
- package/dist/composites/redis-cache.js +21 -0
- package/dist/composites/service-bus-pipeline.js +54 -0
- package/dist/composites/sql-database.js +62 -0
- package/dist/composites/storage-account.js +59 -0
- package/dist/composites/vm-linux.js +139 -0
- package/dist/composites/vnet-default.js +102 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe.js +356 -0
- package/dist/default-tags.js +50 -0
- package/dist/deploy-scopes.d.ts.map +1 -1
- package/dist/deploy-scopes.js +157 -0
- package/dist/describe-resources.js +187 -0
- package/dist/detect.js +17 -0
- package/dist/export-resources.js +108 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +16598 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +144 -0
- package/dist/health.d.ts +42 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +137 -0
- package/dist/import/generator.js +140 -0
- package/dist/import/live-export.js +39 -0
- package/dist/import/parser.js +192 -0
- package/dist/index.js +44 -0
- package/dist/integrity.json +5 -5
- package/dist/intrinsics.js +179 -0
- package/dist/lint/audit-catalog.js +36 -0
- package/dist/lint/audit-lineage.js +71 -0
- package/dist/lint/post-synth/arm-refs.d.ts +1 -0
- package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/arm-refs.js +96 -0
- package/dist/lint/post-synth/azr010.js +45 -0
- package/dist/lint/post-synth/azr011.js +43 -0
- package/dist/lint/post-synth/azr012.js +41 -0
- package/dist/lint/post-synth/azr013.js +50 -0
- package/dist/lint/post-synth/azr014.js +49 -0
- package/dist/lint/post-synth/azr015.js +66 -0
- package/dist/lint/post-synth/azr016.js +37 -0
- package/dist/lint/post-synth/azr017.js +37 -0
- package/dist/lint/post-synth/azr018.js +42 -0
- package/dist/lint/post-synth/azr019.js +41 -0
- package/dist/lint/post-synth/azr020.js +37 -0
- package/dist/lint/post-synth/azr021.js +36 -0
- package/dist/lint/post-synth/azr022.js +39 -0
- package/dist/lint/post-synth/azr023.js +39 -0
- package/dist/lint/post-synth/azr024.js +39 -0
- package/dist/lint/post-synth/azr025.js +37 -0
- package/dist/lint/post-synth/azr026.js +38 -0
- package/dist/lint/post-synth/azr027.js +37 -0
- package/dist/lint/post-synth/azr028.js +37 -0
- package/dist/lint/post-synth/azr029.js +38 -0
- package/dist/lint/post-synth/azr030.js +44 -0
- package/dist/lint/post-synth/azr031.js +36 -0
- package/dist/lint/post-synth/azr032.js +53 -0
- package/dist/lint/post-synth/azr033.js +35 -0
- package/dist/lint/post-synth/index.js +50 -0
- package/dist/lint/rules/hardcoded-location.js +82 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/nsg-wildcard.js +44 -0
- package/dist/lint/rules/storage-https.js +62 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +44 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +6 -1
- package/dist/okf/types/containerApps.md +5 -0
- package/dist/op/activities/az-apply.js +563 -0
- package/dist/op/activities/azure.js +48 -0
- package/dist/op/activities/floci-az.js +37 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +70 -0
- package/dist/ownership.js +13 -0
- package/dist/package-cli.js +21 -0
- package/dist/parameter.js +13 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +478 -0
- package/dist/pseudo.js +40 -0
- package/dist/rules/arm-refs.ts +5 -3
- package/dist/serializer.d.ts +7 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +543 -0
- package/dist/spec/api-versions.js +130 -0
- package/dist/spec/fetch.js +196 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +318 -0
- package/dist/taggable.js +34 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +53 -0
- package/package.json +8 -8
- package/src/attributes.test.ts +17 -0
- package/src/codegen/generate-typescript.ts +1 -1
- package/src/codegen/generate.ts +1 -1
- package/src/codegen/package.ts +2 -2
- package/src/deploy-scopes.ts +2 -1
- package/src/generated/index.d.ts +5 -0
- package/src/generated/index.ts +1 -1
- package/src/generated/lexicon-azure.json +6 -1
- package/src/health.test.ts +66 -0
- package/src/health.ts +162 -0
- package/src/lint/post-synth/arm-refs.ts +5 -3
- package/src/lint/post-synth/post-synth.test.ts +1 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/plugin.ts +11 -2
- package/src/serializer-names.test.ts +83 -0
- package/src/serializer-outputs.test.ts +65 -0
- package/src/serializer.ts +112 -30
- package/src/spec/parse.test.ts +15 -0
- package/src/spec/parse.ts +10 -0
- package/src/taggable.ts +2 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StorageAccountSecure composite — Storage Account with security hardening.
|
|
3
|
+
*
|
|
4
|
+
* A higher-level construct that creates a Storage Account with
|
|
5
|
+
* HTTPS-only, encryption at rest, default-deny network rules, and TLS 1.2.
|
|
6
|
+
*/
|
|
7
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
8
|
+
import { StorageAccount } from "../generated/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Create a StorageAccountSecure composite — returns a Storage Account
|
|
11
|
+
* resource with security best practices.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { StorageAccountSecure } from "@intentius/chant-lexicon-azure";
|
|
16
|
+
*
|
|
17
|
+
* const { storageAccount } = StorageAccountSecure({
|
|
18
|
+
* name: "myappstorage01",
|
|
19
|
+
* sku: "Standard_GRS",
|
|
20
|
+
* tags: { environment: "production" },
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* export { storageAccount };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const StorageAccountSecure = Composite((props) => {
|
|
27
|
+
const { name, location = "[resourceGroup().location]", sku = "Standard_LRS", tags = {}, defaults, } = props;
|
|
28
|
+
const storageAccount = new StorageAccount(mergeDefaults({
|
|
29
|
+
name,
|
|
30
|
+
location,
|
|
31
|
+
tags: {
|
|
32
|
+
"managed-by": "chant",
|
|
33
|
+
...tags,
|
|
34
|
+
},
|
|
35
|
+
sku: {
|
|
36
|
+
name: sku,
|
|
37
|
+
},
|
|
38
|
+
kind: "StorageV2",
|
|
39
|
+
supportsHttpsTrafficOnly: true,
|
|
40
|
+
minimumTlsVersion: "TLS1_2",
|
|
41
|
+
allowBlobPublicAccess: false,
|
|
42
|
+
encryption: {
|
|
43
|
+
services: {
|
|
44
|
+
blob: { enabled: true, keyType: "Account" },
|
|
45
|
+
file: { enabled: true, keyType: "Account" },
|
|
46
|
+
table: { enabled: true, keyType: "Account" },
|
|
47
|
+
queue: { enabled: true, keyType: "Account" },
|
|
48
|
+
},
|
|
49
|
+
keySource: "Microsoft.Storage",
|
|
50
|
+
},
|
|
51
|
+
networkAcls: {
|
|
52
|
+
bypass: "AzureServices",
|
|
53
|
+
defaultAction: "Deny",
|
|
54
|
+
ipRules: [],
|
|
55
|
+
virtualNetworkRules: [],
|
|
56
|
+
},
|
|
57
|
+
}, defaults?.storageAccount), { apiVersion: "2023-01-01" });
|
|
58
|
+
return { storageAccount };
|
|
59
|
+
}, "StorageAccountSecure");
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VmLinux composite — Linux Virtual Machine + NIC + NSG + optional Public IP.
|
|
3
|
+
*
|
|
4
|
+
* A higher-level construct for deploying a Linux VM with SSH access,
|
|
5
|
+
* managed disk, and a Network Security Group.
|
|
6
|
+
*/
|
|
7
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
8
|
+
import { VirtualMachine, NetworkInterface, NetworkSecurityGroup, PublicIPAddress, } from "../generated/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Create a VmLinux composite — returns a Linux VM, NIC, NSG,
|
|
11
|
+
* and optional Public IP.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { VmLinux } from "@intentius/chant-lexicon-azure";
|
|
16
|
+
*
|
|
17
|
+
* const { virtualMachine, nic, nsg, publicIpAddress } = VmLinux({
|
|
18
|
+
* name: "my-vm",
|
|
19
|
+
* vmSize: "Standard_B2s",
|
|
20
|
+
* adminUsername: "azureuser",
|
|
21
|
+
* sshPublicKey: "ssh-rsa AAAA...",
|
|
22
|
+
* subnetId: "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'subnet-1')]",
|
|
23
|
+
* publicIp: true,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* export { virtualMachine, nic, nsg, publicIpAddress };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const VmLinux = Composite((props) => {
|
|
30
|
+
const { name, vmSize, adminUsername, sshPublicKey, subnetId, location = "[resourceGroup().location]", publicIp = false, tags = {}, defaults, } = props;
|
|
31
|
+
const commonTags = {
|
|
32
|
+
"managed-by": "chant",
|
|
33
|
+
...tags,
|
|
34
|
+
};
|
|
35
|
+
const nsgName = `${name}-nsg`;
|
|
36
|
+
const nicName = `${name}-nic`;
|
|
37
|
+
const pipName = `${name}-pip`;
|
|
38
|
+
const nsg = new NetworkSecurityGroup(mergeDefaults({
|
|
39
|
+
name: nsgName,
|
|
40
|
+
location,
|
|
41
|
+
tags: commonTags,
|
|
42
|
+
securityRules: [
|
|
43
|
+
{
|
|
44
|
+
name: "AllowSSH",
|
|
45
|
+
properties: {
|
|
46
|
+
priority: 1000,
|
|
47
|
+
direction: "Inbound",
|
|
48
|
+
access: "Allow",
|
|
49
|
+
protocol: "Tcp",
|
|
50
|
+
sourcePortRange: "*",
|
|
51
|
+
destinationPortRange: "22",
|
|
52
|
+
sourceAddressPrefix: "*",
|
|
53
|
+
destinationAddressPrefix: "*",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}, defaults?.nsg), { apiVersion: "2023-05-01" });
|
|
58
|
+
const ipConfiguration = {
|
|
59
|
+
name: "ipconfig1",
|
|
60
|
+
properties: {
|
|
61
|
+
privateIPAllocationMethod: "Dynamic",
|
|
62
|
+
subnet: {
|
|
63
|
+
id: subnetId,
|
|
64
|
+
},
|
|
65
|
+
...(publicIp && {
|
|
66
|
+
publicIPAddress: {
|
|
67
|
+
id: `[resourceId('Microsoft.Network/publicIPAddresses', '${pipName}')]`,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const nic = new NetworkInterface(mergeDefaults({
|
|
73
|
+
name: nicName,
|
|
74
|
+
location,
|
|
75
|
+
tags: commonTags,
|
|
76
|
+
ipConfigurations: [ipConfiguration],
|
|
77
|
+
networkSecurityGroup: {
|
|
78
|
+
id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
|
|
79
|
+
},
|
|
80
|
+
}, defaults?.nic), { apiVersion: "2023-05-01" });
|
|
81
|
+
const virtualMachine = new VirtualMachine(mergeDefaults({
|
|
82
|
+
name,
|
|
83
|
+
location,
|
|
84
|
+
tags: commonTags,
|
|
85
|
+
hardwareProfile: {
|
|
86
|
+
vmSize,
|
|
87
|
+
},
|
|
88
|
+
osProfile: {
|
|
89
|
+
computerName: name,
|
|
90
|
+
adminUsername,
|
|
91
|
+
linuxConfiguration: {
|
|
92
|
+
disablePasswordAuthentication: true,
|
|
93
|
+
ssh: {
|
|
94
|
+
publicKeys: [
|
|
95
|
+
{
|
|
96
|
+
path: `/home/${adminUsername}/.ssh/authorized_keys`,
|
|
97
|
+
keyData: sshPublicKey,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
storageProfile: {
|
|
104
|
+
imageReference: {
|
|
105
|
+
publisher: "Canonical",
|
|
106
|
+
offer: "0001-com-ubuntu-server-jammy",
|
|
107
|
+
sku: "22_04-lts-gen2",
|
|
108
|
+
version: "latest",
|
|
109
|
+
},
|
|
110
|
+
osDisk: {
|
|
111
|
+
createOption: "FromImage",
|
|
112
|
+
managedDisk: {
|
|
113
|
+
storageAccountType: "Premium_LRS",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
networkProfile: {
|
|
118
|
+
networkInterfaces: [
|
|
119
|
+
{
|
|
120
|
+
id: `[resourceId('Microsoft.Network/networkInterfaces', '${nicName}')]`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
}, defaults?.virtualMachine), { apiVersion: "2023-07-01" });
|
|
125
|
+
const result = { virtualMachine, nic, nsg };
|
|
126
|
+
if (publicIp) {
|
|
127
|
+
result.publicIpAddress = new PublicIPAddress(mergeDefaults({
|
|
128
|
+
name: pipName,
|
|
129
|
+
location,
|
|
130
|
+
tags: commonTags,
|
|
131
|
+
sku: {
|
|
132
|
+
name: "Standard",
|
|
133
|
+
},
|
|
134
|
+
publicIPAllocationMethod: "Static",
|
|
135
|
+
publicIPAddressVersion: "IPv4",
|
|
136
|
+
}, defaults?.publicIpAddress), { apiVersion: "2023-05-01" });
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}, "VmLinux");
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VnetDefault composite — Virtual Network + Subnets + NSG + Route Table.
|
|
3
|
+
*
|
|
4
|
+
* A higher-level construct for a standard Azure virtual network with
|
|
5
|
+
* two subnets, a Network Security Group, and a Route Table.
|
|
6
|
+
*/
|
|
7
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
8
|
+
import { VirtualNetwork, Subnet, NetworkSecurityGroup, RouteTable, } from "../generated/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Create a VnetDefault composite — returns a Virtual Network,
|
|
11
|
+
* two Subnets, an NSG, and a Route Table.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { VnetDefault } from "@intentius/chant-lexicon-azure";
|
|
16
|
+
*
|
|
17
|
+
* const { virtualNetwork, subnet1, subnet2, nsg, routeTable } = VnetDefault({
|
|
18
|
+
* name: "my-vnet",
|
|
19
|
+
* addressPrefix: "10.0.0.0/16",
|
|
20
|
+
* subnetPrefixes: ["10.0.1.0/24", "10.0.2.0/24"],
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* export { virtualNetwork, subnet1, subnet2, nsg, routeTable };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const VnetDefault = Composite((props) => {
|
|
27
|
+
const { name, addressPrefix = "10.0.0.0/16", subnetPrefixes = ["10.0.1.0/24", "10.0.2.0/24"], location = "[resourceGroup().location]", tags = {}, defaults, } = props;
|
|
28
|
+
const commonTags = {
|
|
29
|
+
"managed-by": "chant",
|
|
30
|
+
...tags,
|
|
31
|
+
};
|
|
32
|
+
const nsgName = `${name}-nsg`;
|
|
33
|
+
const routeTableName = `${name}-rt`;
|
|
34
|
+
const nsg = new NetworkSecurityGroup(mergeDefaults({
|
|
35
|
+
name: nsgName,
|
|
36
|
+
location,
|
|
37
|
+
tags: commonTags,
|
|
38
|
+
securityRules: [],
|
|
39
|
+
}, defaults?.nsg), { apiVersion: "2023-05-01" });
|
|
40
|
+
const routeTable = new RouteTable(mergeDefaults({
|
|
41
|
+
name: routeTableName,
|
|
42
|
+
location,
|
|
43
|
+
tags: commonTags,
|
|
44
|
+
disableBgpRoutePropagation: false,
|
|
45
|
+
routes: [],
|
|
46
|
+
}, defaults?.routeTable), { apiVersion: "2023-05-01" });
|
|
47
|
+
const subnet1 = new Subnet(mergeDefaults({
|
|
48
|
+
name: `${name}/subnet-1`,
|
|
49
|
+
addressPrefix: subnetPrefixes[0],
|
|
50
|
+
networkSecurityGroup: {
|
|
51
|
+
id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
|
|
52
|
+
},
|
|
53
|
+
routeTable: {
|
|
54
|
+
id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
|
|
55
|
+
},
|
|
56
|
+
}, defaults?.subnet1), { apiVersion: "2023-05-01" });
|
|
57
|
+
const subnet2 = new Subnet(mergeDefaults({
|
|
58
|
+
name: `${name}/subnet-2`,
|
|
59
|
+
addressPrefix: subnetPrefixes[1],
|
|
60
|
+
networkSecurityGroup: {
|
|
61
|
+
id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
|
|
62
|
+
},
|
|
63
|
+
routeTable: {
|
|
64
|
+
id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
|
|
65
|
+
},
|
|
66
|
+
}, defaults?.subnet2), { apiVersion: "2023-05-01" });
|
|
67
|
+
const virtualNetwork = new VirtualNetwork(mergeDefaults({
|
|
68
|
+
name,
|
|
69
|
+
location,
|
|
70
|
+
tags: commonTags,
|
|
71
|
+
addressSpace: {
|
|
72
|
+
addressPrefixes: [addressPrefix],
|
|
73
|
+
},
|
|
74
|
+
subnets: [
|
|
75
|
+
{
|
|
76
|
+
name: "subnet-1",
|
|
77
|
+
properties: {
|
|
78
|
+
addressPrefix: subnetPrefixes[0],
|
|
79
|
+
networkSecurityGroup: {
|
|
80
|
+
id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
|
|
81
|
+
},
|
|
82
|
+
routeTable: {
|
|
83
|
+
id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "subnet-2",
|
|
89
|
+
properties: {
|
|
90
|
+
addressPrefix: subnetPrefixes[1],
|
|
91
|
+
networkSecurityGroup: {
|
|
92
|
+
id: `[resourceId('Microsoft.Network/networkSecurityGroups', '${nsgName}')]`,
|
|
93
|
+
},
|
|
94
|
+
routeTable: {
|
|
95
|
+
id: `[resourceId('Microsoft.Network/routeTables', '${routeTableName}')]`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}, defaults?.virtualNetwork), { apiVersion: "2023-05-01" });
|
|
101
|
+
return { virtualNetwork, subnet1, subnet2, nsg, routeTable };
|
|
102
|
+
}, "VnetDefault");
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure lexicon coverage analysis.
|
|
3
|
+
*/
|
|
4
|
+
import { computeCoverage, formatSummary, formatVerbose, checkThresholds, } from "@intentius/chant/codegen/coverage";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
export { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds, } from "@intentius/chant/codegen/coverage";
|
|
9
|
+
export async function analyzeAzureCoverage(opts) {
|
|
10
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-azure.json");
|
|
12
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
13
|
+
const report = computeCoverage(content);
|
|
14
|
+
if (opts?.verbose) {
|
|
15
|
+
console.log(formatVerbose(report));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(formatSummary(report));
|
|
19
|
+
}
|
|
20
|
+
if (typeof opts?.minOverall === "number") {
|
|
21
|
+
const result = checkThresholds(report, { minOverallPct: opts.minOverall });
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
for (const v of result.violations)
|
|
24
|
+
console.error(` FAIL: ${v}`);
|
|
25
|
+
throw new Error("Coverage below threshold");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure deep observation (#1086) — the azure row of the deep-observe contract
|
|
3
|
+
* (#1014).
|
|
4
|
+
*
|
|
5
|
+
* `az resource show --resource-group <env> --name <name> --resource-type
|
|
6
|
+
* <type> -o json` already returns the full ARM resource — the same call the
|
|
7
|
+
* thin path (./describe-resources.ts) makes, which keeps `provisioningState`
|
|
8
|
+
* (as `status`) plus `location`/`tags`. The depth is free; the work is
|
|
9
|
+
* entirely normalization, per the issue.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the live tree is flattened, not passed through raw
|
|
12
|
+
*
|
|
13
|
+
* Unlike AWS (whose declared props mirror CloudFormation's vocabulary 1:1, so
|
|
14
|
+
* the Cloud Control payload normalizes and diffs as-is), chant's Azure
|
|
15
|
+
* resource classes do not mirror the ARM response shape verbatim: codegen
|
|
16
|
+
* (`spec/parse.ts`'s `RESOURCE_LEVEL_FIELDS` pass) flattens ARM's `properties`
|
|
17
|
+
* wrapper onto the constructor's top level, so a storage account's declared
|
|
18
|
+
* `minimumTlsVersion` is a sibling of `location`/`tags`, not nested under a
|
|
19
|
+
* `properties` key. Passing the raw `az resource show` response through
|
|
20
|
+
* unflattened would make `properties.minimumTlsVersion` a path with no
|
|
21
|
+
* declared counterpart, ever — the same permanent-noise problem AWS avoids by
|
|
22
|
+
* having a vocabulary match to begin with. So this reader flattens
|
|
23
|
+
* `properties.*` up to the same top level chant's generated classes use,
|
|
24
|
+
* alongside the resource-level fields (`name`, `location`, `sku`, `kind`,
|
|
25
|
+
* `identity`, `tags`, `zones`, `plan`) ARM returns as siblings of `properties`.
|
|
26
|
+
*
|
|
27
|
+
* `id`, `type`, `etag` and `systemData` are the one thing that's *not*
|
|
28
|
+
* flattened in — they have no declared counterpart of any kind (chant's
|
|
29
|
+
* codegen skips `type`/`apiVersion` explicitly, and `id`/`etag`/`systemData`
|
|
30
|
+
* are pure ARM-envelope bookkeeping), so they are simply never read into the
|
|
31
|
+
* tree, the same way `type`/`physicalId` live outside `properties` on {@link
|
|
32
|
+
* DeepResourceObservation} rather than inside it.
|
|
33
|
+
*
|
|
34
|
+
* ## Scope
|
|
35
|
+
*
|
|
36
|
+
* Same as the thin path: any top-level ARM type (`Microsoft.<provider>/<kind>`,
|
|
37
|
+
* exactly one `/`) is readable; a nested compound type
|
|
38
|
+
* (`Microsoft.Storage/storageAccounts/blobServices`) is `unsupported-kind`,
|
|
39
|
+
* because `az resource show` does not accept a compound type name either.
|
|
40
|
+
* Deferred, not fixed, here — same as the issue's own "worth fixing here or
|
|
41
|
+
* explicitly deferring" — widening it is additive and needs no contract change.
|
|
42
|
+
*
|
|
43
|
+
* ## Nothing here talks to real Azure on its own terms
|
|
44
|
+
*
|
|
45
|
+
* ARM over the applier's own transport (`./api/read-client.ts`), exactly like
|
|
46
|
+
* the thin path (#1212) — no CLI, no ARM SDK, no ambient token. The payload is
|
|
47
|
+
* the same ARM JSON `az resource show` was relaying, so the normalization below
|
|
48
|
+
* is untouched by the move.
|
|
49
|
+
*/
|
|
50
|
+
import { boundedConcurrently } from "@intentius/chant/observation";
|
|
51
|
+
import { getResource, isNotFound } from "./api/read-client.js";
|
|
52
|
+
import { LEGACY_OWNERSHIP_TAG_KEY } from "./op/activities/az-apply.js";
|
|
53
|
+
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
54
|
+
import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
55
|
+
import { classifyArmFailure, isTopLevelType } from "./describe-resources.js";
|
|
56
|
+
import { isBracketExpression } from "./lint/post-synth/arm-refs.js";
|
|
57
|
+
import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
|
|
58
|
+
/**
|
|
59
|
+
* Reshape one `az resource show` response into chant's own flat declared
|
|
60
|
+
* vocabulary: the resource-level fields ARM returns as siblings of
|
|
61
|
+
* `properties`, plus every `properties.*` field spread onto that same top
|
|
62
|
+
* level (see the module doc). `id`/`type`/`etag`/`systemData` are envelope
|
|
63
|
+
* bookkeeping with no declared counterpart and are never read in at all.
|
|
64
|
+
*
|
|
65
|
+
* `queriedName` is the declared name the GET was addressed by. A child
|
|
66
|
+
* resource is declared as `vnet/subnet` while ARM answers with the leaf
|
|
67
|
+
* (`name: "subnet"`) — the same identity in two spellings, since the URL that
|
|
68
|
+
* returned 200 is the declared name. The leaf is folded back to the declared
|
|
69
|
+
* spelling; a name that is not the leaf of the query is kept as returned.
|
|
70
|
+
*/
|
|
71
|
+
function buildLiveProperties(obj, queriedName) {
|
|
72
|
+
const out = {};
|
|
73
|
+
if (obj.name !== undefined) {
|
|
74
|
+
out.name =
|
|
75
|
+
queriedName !== undefined && queriedName !== obj.name && queriedName.endsWith(`/${obj.name}`)
|
|
76
|
+
? queriedName
|
|
77
|
+
: obj.name;
|
|
78
|
+
}
|
|
79
|
+
if (obj.location !== undefined)
|
|
80
|
+
out.location = obj.location;
|
|
81
|
+
if (obj.sku !== undefined)
|
|
82
|
+
out.sku = obj.sku;
|
|
83
|
+
if (obj.kind !== undefined)
|
|
84
|
+
out.kind = obj.kind;
|
|
85
|
+
if (obj.identity !== undefined)
|
|
86
|
+
out.identity = obj.identity;
|
|
87
|
+
if (obj.tags !== undefined)
|
|
88
|
+
out.tags = obj.tags;
|
|
89
|
+
if (obj.zones !== undefined)
|
|
90
|
+
out.zones = obj.zones;
|
|
91
|
+
if (obj.plan !== undefined)
|
|
92
|
+
out.plan = obj.plan;
|
|
93
|
+
if (obj.properties && typeof obj.properties === "object") {
|
|
94
|
+
for (const [k, v] of Object.entries(obj.properties)) {
|
|
95
|
+
if (v !== undefined)
|
|
96
|
+
out[k] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
// ── Noise rules (#1086) ──────────────────────────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Server-populated wherever they appear — matched on the final path segment,
|
|
104
|
+
* the same technique AWS's `AWS_READ_ONLY_NAMES` uses, because ARM repeats
|
|
105
|
+
* `provisioningState` at every resource-properties depth and a per-type list
|
|
106
|
+
* of full paths would be a maintenance trap. Deliberately excludes ambiguous
|
|
107
|
+
* names like `id`/`name`: a subnet's `properties.routeTable.id` is a declared
|
|
108
|
+
* cross-reference, and pruning it is how a normalization pass starts hiding
|
|
109
|
+
* real drift. (The resource's *own* envelope `id` is handled separately —
|
|
110
|
+
* see the module doc — precisely so this list never has to touch `id` at all.)
|
|
111
|
+
*/
|
|
112
|
+
export const AZURE_READ_ONLY_NAMES = new Set([
|
|
113
|
+
"provisioningState",
|
|
114
|
+
"resourceGuid",
|
|
115
|
+
"principalId",
|
|
116
|
+
"tenantId",
|
|
117
|
+
// The resource's own envelope etag never reaches the tree (see the module
|
|
118
|
+
// doc), but ARM also stamps one on every *nested* child — each security rule,
|
|
119
|
+
// each subnet — and nobody declares those.
|
|
120
|
+
"etag",
|
|
121
|
+
]);
|
|
122
|
+
/**
|
|
123
|
+
* Server-injected boilerplate no chant user ever declares nor could
|
|
124
|
+
* meaningfully override — Azure stamps every network security group with
|
|
125
|
+
* these regardless of what was requested, so unlike `AZURE_READ_ONLY_NAMES`
|
|
126
|
+
* this is dropped outright rather than compared.
|
|
127
|
+
*/
|
|
128
|
+
const AZURE_UNCONDITIONAL_PRUNE_NAMES = new Set(["defaultSecurityRules"]);
|
|
129
|
+
/**
|
|
130
|
+
* Server-computed surfaces ARM fills in on its own (#1214, the AKS row of the
|
|
131
|
+
* CC round-trip): a managed cluster always comes back with `fqdn`,
|
|
132
|
+
* `currentKubernetesVersion` and `nodeResourceGroup` whether or not the
|
|
133
|
+
* declaration said anything about them. Unlike `AZURE_READ_ONLY_NAMES` these
|
|
134
|
+
* are counterpart-gated rather than pruned outright: `nodeResourceGroup` IS
|
|
135
|
+
* declarable at create time, so a declared value is still compared — only the
|
|
136
|
+
* purely server-filled appearance is noise. Sparse and evidence-based, like
|
|
137
|
+
* `AZURE_SERVICE_DEFAULTS`.
|
|
138
|
+
*/
|
|
139
|
+
export const AZURE_SERVER_COMPUTED_NAMES = new Set([
|
|
140
|
+
"currentKubernetesVersion",
|
|
141
|
+
"fqdn",
|
|
142
|
+
"nodeResourceGroup",
|
|
143
|
+
]);
|
|
144
|
+
/**
|
|
145
|
+
* ARM service defaults, per type, as index-erased property paths. Subtracted
|
|
146
|
+
* only where source never declared the property — cdk-real-drift's default
|
|
147
|
+
* subtraction, same as AWS's `AWS_SERVICE_DEFAULTS`. Sparse and evidence-based
|
|
148
|
+
* rather than exhaustive across 1900+ ARM types: widening this table is
|
|
149
|
+
* additive and needs no contract change.
|
|
150
|
+
*/
|
|
151
|
+
export const AZURE_SERVICE_DEFAULTS = {
|
|
152
|
+
"Microsoft.Storage/storageAccounts": {
|
|
153
|
+
minimumTlsVersion: "TLS1_2",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
/** Arrays known to be ARM sets (server order is not authoring order), keyed by well-known ARM naming conventions. */
|
|
157
|
+
const AZURE_SET_ARRAY_NAMES = new Set([
|
|
158
|
+
"securityRules",
|
|
159
|
+
"subnets",
|
|
160
|
+
"ipRules",
|
|
161
|
+
"virtualNetworkRules",
|
|
162
|
+
"addressPrefixes",
|
|
163
|
+
"dnsServers",
|
|
164
|
+
"zones",
|
|
165
|
+
]);
|
|
166
|
+
/**
|
|
167
|
+
* chant's own ownership marker, as tag paths (#1213 — the azure edition of
|
|
168
|
+
* AWS's #1301 correction). The serializer stamps `chant-stack`/`chant-env` from
|
|
169
|
+
* project config and `azApply` stamps the managed-by pair on every resource it
|
|
170
|
+
* PUTs, so a managed estate always carries them live while the declared
|
|
171
|
+
* *properties* the diff compares do not — chant reading its own signature back
|
|
172
|
+
* as drift. Counterpart-gated at the call site, so a source that declares one
|
|
173
|
+
* of these tags itself is still compared.
|
|
174
|
+
*/
|
|
175
|
+
const AZURE_OWNERSHIP_TAG_PATTERNS = new Set([
|
|
176
|
+
`tags.${AZURE_TAG_OWNERSHIP_KEYS.stack}`,
|
|
177
|
+
`tags.${AZURE_TAG_OWNERSHIP_KEYS.env}`,
|
|
178
|
+
]);
|
|
179
|
+
/**
|
|
180
|
+
* The managed-by pair is gated on the value as well: both keys always carry
|
|
181
|
+
* `"chant"` when chant wrote them, so `managed-by: terraform` appearing out of
|
|
182
|
+
* band still surfaces as the drift it is.
|
|
183
|
+
*/
|
|
184
|
+
const AZURE_MANAGED_BY_TAG_PATTERNS = new Set([
|
|
185
|
+
`tags.${AZURE_TAG_OWNERSHIP_KEYS.managedBy}`,
|
|
186
|
+
`tags.${LEGACY_OWNERSHIP_TAG_KEY}`,
|
|
187
|
+
]);
|
|
188
|
+
function isRecord(value) {
|
|
189
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
190
|
+
}
|
|
191
|
+
function isEmptyRecord(value) {
|
|
192
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
193
|
+
}
|
|
194
|
+
/** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a `name` field. */
|
|
195
|
+
function canonicalJson(value) {
|
|
196
|
+
return (JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v)
|
|
197
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
198
|
+
: v) ?? "");
|
|
199
|
+
}
|
|
200
|
+
/** The final segment of an index-erased pattern (`properties.networkAcls.ipRules[]` → `ipRules`). */
|
|
201
|
+
function lastSegment(pattern) {
|
|
202
|
+
const withoutIndex = pattern.replace(/\[\]$/, "");
|
|
203
|
+
const dot = withoutIndex.lastIndexOf(".");
|
|
204
|
+
return dot === -1 ? withoutIndex : withoutIndex.slice(dot + 1);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The azure lexicon's noise rules. Three classes, same as AWS: server-
|
|
208
|
+
* populated fields (by name, unconditional), controller-injected boilerplate
|
|
209
|
+
* (by name, unconditional), and ARM service defaults (gated on
|
|
210
|
+
* `counterpart === "absent"`) — plus tag-map defaults, which AWS has no
|
|
211
|
+
* equivalent for since its `Tags` is an array rather than a plain object (an
|
|
212
|
+
* ARM tag map's key order is already canonicalized unconditionally by core,
|
|
213
|
+
* since it is a plain object; only the *default empty map* needs a rule here).
|
|
214
|
+
*/
|
|
215
|
+
export const azureDeepNormalizationHooks = {
|
|
216
|
+
prune(node) {
|
|
217
|
+
const name = lastSegment(node.pattern);
|
|
218
|
+
if (AZURE_READ_ONLY_NAMES.has(name) || AZURE_UNCONDITIONAL_PRUNE_NAMES.has(name))
|
|
219
|
+
return true;
|
|
220
|
+
if (node.side !== "live" || node.counterpart !== "absent")
|
|
221
|
+
return false;
|
|
222
|
+
// ARM always returns *some* tag map; an empty one when nobody declared any
|
|
223
|
+
// tag at all is the default, not a finding.
|
|
224
|
+
if (node.pattern === "tags" && isEmptyRecord(node.value))
|
|
225
|
+
return true;
|
|
226
|
+
// A live `location` nobody declared is the effective placement, not drift:
|
|
227
|
+
// a child resource (a subnet) has no location property of its own to
|
|
228
|
+
// declare, and every top-level resource that CAN take one is declared with
|
|
229
|
+
// one (or with `[resourceGroup().location]`, which is a declared
|
|
230
|
+
// counterpart). Resource-level only — `pattern` is the exact top-level
|
|
231
|
+
// path, so a nested `.location` inside a declared property is untouched.
|
|
232
|
+
if (node.pattern === "location")
|
|
233
|
+
return true;
|
|
234
|
+
// A server-computed surface nobody declared is ARM doing its job, not
|
|
235
|
+
// drift (see the set's doc for why this is counterpart-gated).
|
|
236
|
+
if (AZURE_SERVER_COMPUTED_NAMES.has(name))
|
|
237
|
+
return true;
|
|
238
|
+
// chant's own ownership marker is not drift (see the sets' docs).
|
|
239
|
+
if (AZURE_OWNERSHIP_TAG_PATTERNS.has(node.pattern))
|
|
240
|
+
return true;
|
|
241
|
+
if (AZURE_MANAGED_BY_TAG_PATTERNS.has(node.pattern) && node.value === OWNERSHIP_MANAGED_BY_VALUE)
|
|
242
|
+
return true;
|
|
243
|
+
// ARM stamps a self-`id` on every element of a nested child collection —
|
|
244
|
+
// each security rule, each inline subnet. Only the element's *own* id
|
|
245
|
+
// (`securityRules[].id`, directly on the element): a declared
|
|
246
|
+
// cross-reference like `virtualNetworkRules[].id` has a counterpart and is
|
|
247
|
+
// still compared, and object-valued references (`routeTable.id`) never
|
|
248
|
+
// match the array-element shape.
|
|
249
|
+
if (node.pattern.endsWith("[].id"))
|
|
250
|
+
return true;
|
|
251
|
+
const defaults = AZURE_SERVICE_DEFAULTS[node.entityType];
|
|
252
|
+
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern))
|
|
253
|
+
return false;
|
|
254
|
+
return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* An ARM bracket-expression string (`"[resourceId(...)]"`) is an unevaluated
|
|
258
|
+
* reference: the applier resolves it at deploy time, so the live side holds
|
|
259
|
+
* the evaluated value and comparing the two is comparing a formula to its
|
|
260
|
+
* result. Collapsed to UNRESOLVED on the declared side only — a live payload
|
|
261
|
+
* cannot carry an unevaluated expression, and `[[`-escaped strings are ARM's
|
|
262
|
+
* own literal-`[` spelling, not expressions.
|
|
263
|
+
*/
|
|
264
|
+
unresolved(node) {
|
|
265
|
+
return (node.side === "declared" &&
|
|
266
|
+
isBracketExpression(node.value) &&
|
|
267
|
+
!node.value.startsWith("[["));
|
|
268
|
+
},
|
|
269
|
+
/**
|
|
270
|
+
* The key doubles as a path segment (`securityRules[#allow-ssh]`), so a
|
|
271
|
+
* `name` field is the element's own identity where ARM gives one — which it
|
|
272
|
+
* does for essentially every nested rule/subnet/range collection — and
|
|
273
|
+
* canonical JSON only as a fallback for primitive-valued sets
|
|
274
|
+
* (`addressPrefixes`, `dnsServers`).
|
|
275
|
+
*/
|
|
276
|
+
orderKey(element) {
|
|
277
|
+
const name = lastSegment(element.pattern);
|
|
278
|
+
if (!AZURE_SET_ARRAY_NAMES.has(name))
|
|
279
|
+
return undefined;
|
|
280
|
+
const el = element.element;
|
|
281
|
+
if (isRecord(el)) {
|
|
282
|
+
const key = typeof el.name === "string" ? el.name : undefined;
|
|
283
|
+
return key ?? canonicalJson(el);
|
|
284
|
+
}
|
|
285
|
+
return typeof el === "string" ? el : canonicalJson(el);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Read the live property tree for each declared entity via `az resource
|
|
290
|
+
* show`. One call per entity, same as the thin path — ARM has no bulk
|
|
291
|
+
* "describe everything in this group" call with per-resource depth the way
|
|
292
|
+
* CloudFormation's `describe-stack-resources` does, so there is no cheap
|
|
293
|
+
* list-then-describe split to make here the way AWS's own reader does.
|
|
294
|
+
*/
|
|
295
|
+
export async function observeResourcesDeepAzure(options) {
|
|
296
|
+
const resources = {};
|
|
297
|
+
const unobserved = {};
|
|
298
|
+
const readable = [];
|
|
299
|
+
const client = {
|
|
300
|
+
resourceGroup: options.environment,
|
|
301
|
+
...(process.env.AZURE_ENDPOINT_URL ? { endpoint: process.env.AZURE_ENDPOINT_URL } : {}),
|
|
302
|
+
...(process.env.AZURE_SUBSCRIPTION_ID ? { subscriptionId: process.env.AZURE_SUBSCRIPTION_ID } : {}),
|
|
303
|
+
...(options.http ? { http: options.http } : {}),
|
|
304
|
+
};
|
|
305
|
+
for (const [entityName, { entityType, props }] of options.entities) {
|
|
306
|
+
if (!entityType.startsWith("Microsoft.")) {
|
|
307
|
+
unobserved[entityName] = {
|
|
308
|
+
type: entityType,
|
|
309
|
+
reason: "unsupported-kind",
|
|
310
|
+
detail: "not an ARM resource type (expected Microsoft.<provider>/<kind>)",
|
|
311
|
+
};
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!isTopLevelType(entityType)) {
|
|
315
|
+
unobserved[entityName] = {
|
|
316
|
+
type: entityType,
|
|
317
|
+
reason: "unsupported-kind",
|
|
318
|
+
detail: "a nested ARM type needs a different read path; chant never queried this resource",
|
|
319
|
+
};
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const name = props.name;
|
|
323
|
+
if (!name) {
|
|
324
|
+
unobserved[entityName] = {
|
|
325
|
+
type: entityType,
|
|
326
|
+
reason: "read-failed",
|
|
327
|
+
detail: "declared entity has no name to query by",
|
|
328
|
+
};
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
readable.push({ entityName, entityType, name });
|
|
332
|
+
}
|
|
333
|
+
await boundedConcurrently(readable, async ({ entityName, entityType, name }) => {
|
|
334
|
+
try {
|
|
335
|
+
const obj = (await getResource(client, entityType, name));
|
|
336
|
+
resources[entityName] = {
|
|
337
|
+
type: entityType,
|
|
338
|
+
physicalId: obj.id,
|
|
339
|
+
properties: normalizeDeepProperties(buildLiveProperties(obj, name), {
|
|
340
|
+
entityType,
|
|
341
|
+
side: "live",
|
|
342
|
+
hooks: azureDeepNormalizationHooks,
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
// Not-found leaves the entity out (absence, same as the thin path).
|
|
348
|
+
// Auth/binding/other failures are holes so they can't become creates.
|
|
349
|
+
if (isNotFound(err))
|
|
350
|
+
return;
|
|
351
|
+
const outcome = classifyArmFailure(err);
|
|
352
|
+
unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return deepObservation(resources, unobserved);
|
|
356
|
+
}
|