nexarch 0.12.14 → 0.12.16
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.
|
@@ -3,7 +3,7 @@ import { execFileSync } from "child_process";
|
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import { callMcpTool } from "../lib/mcp.js";
|
|
5
5
|
import { parseTerraformStateBuffer, projectTerraformState, inferEnvironmentFromTags, environmentSubtypeFor, } from "../lib/terraform-projection.js";
|
|
6
|
-
import {
|
|
6
|
+
import { loadIacCatalogue, describeCatalogue } from "../lib/iac-catalogue.js";
|
|
7
7
|
import { discoverRootModules, environmentFromPath } from "../lib/terraform-detect.js";
|
|
8
8
|
const ENTITY_BATCH = 50;
|
|
9
9
|
function readResult(raw) {
|
|
@@ -88,8 +88,28 @@ function loadState(statePath, dir) {
|
|
|
88
88
|
return { document: parseTerraformStateBuffer(stdout), source: "terraform show -json" };
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// `error.message` is only ever "Command failed: terraform show -json",
|
|
92
|
+
// which names the command that failed and not one thing about why. The
|
|
93
|
+
// reason — no state, a backend that needs credentials, a lock, a directory
|
|
94
|
+
// owned by another user — is on stderr, and reporting the wrapper instead
|
|
95
|
+
// of the cause leaves the caller with nothing to act on. That matters more
|
|
96
|
+
// here than usual: the caller is normally an agent, and an agent given
|
|
97
|
+
// "Command failed" can only report failure, while an agent given
|
|
98
|
+
// "Error: Backend initialization required" can run terraform init.
|
|
99
|
+
const failure = error;
|
|
100
|
+
const stderr = (typeof failure.stderr === "string" ? failure.stderr : failure.stderr?.toString("utf8") ?? "").trim();
|
|
101
|
+
const reason = stderr || failure.message || String(error);
|
|
102
|
+
// Terraform's own errors are already formatted for a reader; keep enough
|
|
103
|
+
// lines to carry the explanation without pasting an entire trace.
|
|
104
|
+
const detail = reason
|
|
105
|
+
.split("\n")
|
|
106
|
+
.filter((line) => line.trim().length > 0)
|
|
107
|
+
.slice(0, 8)
|
|
108
|
+
.map((line) => ` ${line.trim()}`)
|
|
109
|
+
.join("\n");
|
|
110
|
+
throw new Error(`Could not read Terraform state in ${dir}.\n` +
|
|
111
|
+
` Run inside the infrastructure repo with terraform available, or pass --state <file>.\n` +
|
|
112
|
+
`\n terraform said:\n${detail}`);
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
function resolveEnvironment(projection, override, dir, rootModuleDir) {
|
|
@@ -301,7 +321,7 @@ export async function ingestInfra(args) {
|
|
|
301
321
|
// Fetched once and used for both projection and payload: two different
|
|
302
322
|
// catalogues within one ingest would produce entities whose attributes and
|
|
303
323
|
// whose type mapping disagreed about what a resource is.
|
|
304
|
-
const catalogueResult = await
|
|
324
|
+
const catalogueResult = await loadIacCatalogue();
|
|
305
325
|
if (!asJson)
|
|
306
326
|
console.log(` ${describeCatalogue(catalogueResult)}`);
|
|
307
327
|
const catalogue = catalogueResult.entries;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback catalogue for Terraform resource types.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED FILE — DO NOT EDIT BY HAND.
|
|
5
|
+
* Regenerate with: node web/scripts/export-iac-catalogue.mjs
|
|
6
|
+
*
|
|
7
|
+
* The catalogue proper is platform reference data, curated in
|
|
8
|
+
* `iac_catalogue_entry` and served to every tenant, so supporting a new
|
|
9
|
+
* service is a data change rather than a CLI release. This file is what the CLI
|
|
10
|
+
* falls back to when it cannot reach the platform — offline, or a CI runner with
|
|
11
|
+
* no route to the gateway. It is generated from that table so it can never get
|
|
12
|
+
* ahead of it: editing it by hand recreates the two-sources-of-truth problem
|
|
13
|
+
* moving the catalogue out of the CLI was meant to end.
|
|
14
|
+
*
|
|
15
|
+
* 25 entries, exported 2026-08-21.
|
|
16
|
+
*/
|
|
17
|
+
export const IAC_CATALOGUE_SEED = [
|
|
18
|
+
{
|
|
19
|
+
resourceType: "azurerm_container_app_environment",
|
|
20
|
+
entityTypeCode: "platform_component",
|
|
21
|
+
entitySubtypeCode: "platform_runtime",
|
|
22
|
+
role: "compute_environment",
|
|
23
|
+
attributes: ["name", "location", "resource_group_name", "default_domain", "infrastructure_subnet_id", "internal_load_balancer_enabled", "static_ip_address", "zone_redundancy_enabled"],
|
|
24
|
+
references: { "infrastructure_subnet_id": "azurerm_subnet" },
|
|
25
|
+
cloudResourceType: "Microsoft.App/managedEnvironments",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
resourceType: "azurerm_service_plan",
|
|
29
|
+
entityTypeCode: "platform_component",
|
|
30
|
+
entitySubtypeCode: "platform_runtime",
|
|
31
|
+
role: "compute_environment",
|
|
32
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "os_type", "worker_count"],
|
|
33
|
+
cloudResourceType: "Microsoft.Web/serverfarms",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
resourceType: "azurerm_cognitive_account",
|
|
37
|
+
entityTypeCode: "platform_component",
|
|
38
|
+
entitySubtypeCode: "platform_service",
|
|
39
|
+
role: "compute_host",
|
|
40
|
+
attributes: ["name", "location", "resource_group_name", "kind", "sku_name", "endpoint", "custom_subdomain_name", "public_network_access_enabled", "local_auth_enabled"],
|
|
41
|
+
cloudResourceType: "Microsoft.CognitiveServices/accounts",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
resourceType: "azurerm_container_app",
|
|
45
|
+
entityTypeCode: "platform_component",
|
|
46
|
+
entitySubtypeCode: "platform_runtime",
|
|
47
|
+
role: "compute_host",
|
|
48
|
+
bindable: true,
|
|
49
|
+
attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "revision_mode", "workload_profile_name"],
|
|
50
|
+
references: { "container_app_environment_id": "azurerm_container_app_environment" },
|
|
51
|
+
cloudResourceType: "Microsoft.App/containerApps",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
resourceType: "azurerm_container_app_job",
|
|
55
|
+
entityTypeCode: "platform_component",
|
|
56
|
+
entitySubtypeCode: "platform_runtime",
|
|
57
|
+
role: "compute_host",
|
|
58
|
+
bindable: true,
|
|
59
|
+
attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "replica_timeout_in_seconds", "workload_profile_name"],
|
|
60
|
+
references: { "container_app_environment_id": "azurerm_container_app_environment" },
|
|
61
|
+
cloudResourceType: "Microsoft.App/jobs",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
resourceType: "azurerm_linux_web_app",
|
|
65
|
+
entityTypeCode: "platform_component",
|
|
66
|
+
entitySubtypeCode: "platform_runtime",
|
|
67
|
+
role: "compute_host",
|
|
68
|
+
bindable: true,
|
|
69
|
+
attributes: ["name", "location", "resource_group_name", "service_plan_id", "https_only", "default_hostname"],
|
|
70
|
+
references: { "service_plan_id": "azurerm_service_plan" },
|
|
71
|
+
cloudResourceType: "Microsoft.Web/sites",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
resourceType: "azurerm_cosmosdb_account",
|
|
75
|
+
entityTypeCode: "data_store",
|
|
76
|
+
entitySubtypeCode: "store_document",
|
|
77
|
+
role: "data_store",
|
|
78
|
+
attributes: ["name", "location", "resource_group_name", "kind", "offer_type", "public_network_access_enabled", "endpoint"],
|
|
79
|
+
cloudResourceType: "Microsoft.DocumentDB/databaseAccounts",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
resourceType: "azurerm_postgresql_flexible_server",
|
|
83
|
+
entityTypeCode: "data_store",
|
|
84
|
+
entitySubtypeCode: "store_relational",
|
|
85
|
+
role: "data_store",
|
|
86
|
+
attributes: ["name", "location", "resource_group_name", "fqdn", "version", "sku_name", "storage_mb", "storage_tier", "public_network_access_enabled", "delegated_subnet_id", "private_dns_zone_id", "backup_retention_days", "geo_redundant_backup_enabled", "zone"],
|
|
87
|
+
cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
resourceType: "azurerm_postgresql_flexible_server_database",
|
|
91
|
+
entityTypeCode: "data_store",
|
|
92
|
+
entitySubtypeCode: "store_relational",
|
|
93
|
+
role: "data_store",
|
|
94
|
+
attributes: ["name", "server_id", "charset", "collation"],
|
|
95
|
+
cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers/databases",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
resourceType: "azurerm_storage_account",
|
|
99
|
+
entityTypeCode: "data_store",
|
|
100
|
+
entitySubtypeCode: "store_object",
|
|
101
|
+
role: "data_store",
|
|
102
|
+
attributes: ["name", "location", "resource_group_name", "account_kind", "account_tier", "account_replication_type", "access_tier", "https_traffic_only_enabled", "min_tls_version", "public_network_access_enabled", "allow_nested_items_to_be_public", "shared_access_key_enabled", "primary_blob_endpoint", "primary_location", "secondary_location"],
|
|
103
|
+
cloudResourceType: "Microsoft.Storage/storageAccounts",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
resourceType: "azurerm_storage_container",
|
|
107
|
+
entityTypeCode: "data_store",
|
|
108
|
+
entitySubtypeCode: "store_object",
|
|
109
|
+
role: "data_store",
|
|
110
|
+
attributes: ["name", "container_access_type", "storage_account_id", "has_immutability_policy"],
|
|
111
|
+
cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
resourceType: "azurerm_cdn_frontdoor_endpoint",
|
|
115
|
+
entityTypeCode: "platform_component",
|
|
116
|
+
entitySubtypeCode: "platform_service",
|
|
117
|
+
role: "edge",
|
|
118
|
+
attributes: ["name", "host_name", "enabled", "cdn_frontdoor_profile_id"],
|
|
119
|
+
references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
|
|
120
|
+
cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
resourceType: "azurerm_cdn_frontdoor_firewall_policy",
|
|
124
|
+
entityTypeCode: "policy_control",
|
|
125
|
+
role: "edge",
|
|
126
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "mode", "enabled"],
|
|
127
|
+
cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
resourceType: "azurerm_cdn_frontdoor_profile",
|
|
131
|
+
entityTypeCode: "platform_component",
|
|
132
|
+
entitySubtypeCode: "platform_service",
|
|
133
|
+
role: "edge",
|
|
134
|
+
attributes: ["name", "location", "resource_group_name", "sku_name", "response_timeout_seconds"],
|
|
135
|
+
cloudResourceType: "Microsoft.Cdn/profiles",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
resourceType: "azurerm_role_assignment",
|
|
139
|
+
entityTypeCode: "policy_control",
|
|
140
|
+
role: "identity",
|
|
141
|
+
attributes: ["name", "principal_id", "principal_type", "role_definition_name", "scope"],
|
|
142
|
+
cloudResourceType: "Microsoft.Authorization/roleAssignments",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
resourceType: "azurerm_user_assigned_identity",
|
|
146
|
+
entityTypeCode: "platform_component",
|
|
147
|
+
entitySubtypeCode: "platform_control_plane",
|
|
148
|
+
role: "identity",
|
|
149
|
+
attributes: ["name", "location", "resource_group_name", "client_id", "principal_id", "tenant_id"],
|
|
150
|
+
cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
resourceType: "azurerm_servicebus_namespace",
|
|
154
|
+
entityTypeCode: "platform_component",
|
|
155
|
+
entitySubtypeCode: "platform_service",
|
|
156
|
+
role: "messaging",
|
|
157
|
+
attributes: ["name", "location", "resource_group_name", "sku", "public_network_access_enabled"],
|
|
158
|
+
cloudResourceType: "Microsoft.ServiceBus/namespaces",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
resourceType: "azurerm_servicebus_queue",
|
|
162
|
+
entityTypeCode: "platform_component",
|
|
163
|
+
entitySubtypeCode: "platform_service",
|
|
164
|
+
role: "messaging",
|
|
165
|
+
attributes: ["name", "namespace_id", "max_delivery_count", "requires_session"],
|
|
166
|
+
references: { "namespace_id": "azurerm_servicebus_namespace" },
|
|
167
|
+
cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
resourceType: "azurerm_private_dns_zone",
|
|
171
|
+
entityTypeCode: "platform_component",
|
|
172
|
+
entitySubtypeCode: "platform_service",
|
|
173
|
+
role: "network_boundary",
|
|
174
|
+
attributes: ["name", "resource_group_name", "number_of_record_sets"],
|
|
175
|
+
cloudResourceType: "Microsoft.Network/privateDnsZones",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
resourceType: "azurerm_private_endpoint",
|
|
179
|
+
entityTypeCode: "platform_component",
|
|
180
|
+
entitySubtypeCode: "platform_service",
|
|
181
|
+
role: "network_boundary",
|
|
182
|
+
attributes: ["name", "location", "resource_group_name", "subnet_id"],
|
|
183
|
+
references: { "subnet_id": "azurerm_subnet" },
|
|
184
|
+
cloudResourceType: "Microsoft.Network/privateEndpoints",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
resourceType: "azurerm_subnet",
|
|
188
|
+
entityTypeCode: "platform_component",
|
|
189
|
+
entitySubtypeCode: "platform_service",
|
|
190
|
+
role: "network_boundary",
|
|
191
|
+
attributes: ["name", "resource_group_name", "virtual_network_name", "address_prefixes", "default_outbound_access_enabled", "private_endpoint_network_policies"],
|
|
192
|
+
nameReferences: { "virtual_network_name": "azurerm_virtual_network" },
|
|
193
|
+
cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
resourceType: "azurerm_virtual_network",
|
|
197
|
+
entityTypeCode: "platform_component",
|
|
198
|
+
entitySubtypeCode: "platform_service",
|
|
199
|
+
role: "network_boundary",
|
|
200
|
+
attributes: ["name", "location", "resource_group_name", "address_space", "guid"],
|
|
201
|
+
cloudResourceType: "Microsoft.Network/virtualNetworks",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
resourceType: "azurerm_log_analytics_workspace",
|
|
205
|
+
entityTypeCode: "platform_component",
|
|
206
|
+
entitySubtypeCode: "platform_service",
|
|
207
|
+
role: "observability",
|
|
208
|
+
attributes: ["name", "location", "resource_group_name", "sku", "retention_in_days", "workspace_id"],
|
|
209
|
+
cloudResourceType: "Microsoft.OperationalInsights/workspaces",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
resourceType: "azurerm_container_registry",
|
|
213
|
+
entityTypeCode: "platform_component",
|
|
214
|
+
entitySubtypeCode: "platform_service",
|
|
215
|
+
role: "registry",
|
|
216
|
+
attributes: ["name", "location", "resource_group_name", "login_server", "sku", "admin_enabled", "public_network_access_enabled"],
|
|
217
|
+
cloudResourceType: "Microsoft.ContainerRegistry/registries",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
resourceType: "azurerm_key_vault",
|
|
221
|
+
entityTypeCode: "platform_component",
|
|
222
|
+
entitySubtypeCode: "platform_service",
|
|
223
|
+
role: "secret_store",
|
|
224
|
+
attributes: ["name", "location", "resource_group_name", "vault_uri", "sku_name", "enable_rbac_authorization", "rbac_authorization_enabled", "public_network_access_enabled", "purge_protection_enabled", "soft_delete_retention_days"],
|
|
225
|
+
cloudResourceType: "Microsoft.KeyVault/vaults",
|
|
226
|
+
},
|
|
227
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { callMcpTool } from "./mcp.js";
|
|
2
|
+
import { IAC_CATALOGUE_SEED } from "./iac-catalogue-seed.js";
|
|
3
|
+
/**
|
|
4
|
+
* Rejects a payload that would widen what may be read.
|
|
5
|
+
*
|
|
6
|
+
* A served catalogue is a network response, and this one decides what leaves
|
|
7
|
+
* the machine. An entry with no attributes is meaningless — it would project a
|
|
8
|
+
* resource stripped of all evidence — and an entry missing its type mapping
|
|
9
|
+
* cannot be applied at all, so neither is accepted quietly. Terraform's own
|
|
10
|
+
* `sensitive_values` gate still runs behind whatever survives here.
|
|
11
|
+
*/
|
|
12
|
+
function usableEntries(payload) {
|
|
13
|
+
return (payload.entries ?? []).filter((entry) => typeof entry?.resourceType === "string" &&
|
|
14
|
+
typeof entry?.entityTypeCode === "string" &&
|
|
15
|
+
Array.isArray(entry?.attributes) &&
|
|
16
|
+
entry.attributes.length > 0 &&
|
|
17
|
+
entry.attributes.every((attribute) => typeof attribute === "string"));
|
|
18
|
+
}
|
|
19
|
+
export async function loadIacCatalogue() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await callMcpTool("nexarch_get_iac_catalogue", {});
|
|
22
|
+
const payload = JSON.parse(raw.content?.[0]?.text ?? "{}");
|
|
23
|
+
const entries = usableEntries(payload);
|
|
24
|
+
// An empty or unparseable response is not a reason to ingest nothing: the
|
|
25
|
+
// bundled copy is known-good and was generated from this same table.
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: "the platform returned no usable entries" };
|
|
28
|
+
}
|
|
29
|
+
return { entries, source: "platform", reason: null };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One line describing which catalogue was used, so a stale ingest is never silent. */
|
|
37
|
+
export function describeCatalogue(result) {
|
|
38
|
+
if (result.source === "platform")
|
|
39
|
+
return `Catalogue: ${result.entries.length} resource types from the platform.`;
|
|
40
|
+
return `Catalogue: ${result.entries.length} resource types from the bundled copy — ${result.reason}.\n Types added to the platform since this CLI was published will not be recognised.`;
|
|
41
|
+
}
|
|
@@ -183,7 +183,14 @@ export function discoverRootModules(repoDir) {
|
|
|
183
183
|
if (/^\s*provider\s+"[a-z0-9_]+"\s*\{/m.test(content))
|
|
184
184
|
hasProvider = true;
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
// A `.terraform` directory only proves provider plugins were downloaded.
|
|
187
|
+
// The backend — the thing `terraform show` needs — is initialised when
|
|
188
|
+
// `.terraform/terraform.tfstate` records its configuration. Treating the
|
|
189
|
+
// directory as proof reports a root as ready, the caller runs ingest, and
|
|
190
|
+
// Terraform answers "Backend initialization required" from somewhere the
|
|
191
|
+
// caller was told not to expect it.
|
|
192
|
+
const hasPluginDir = existsSync(join(dir, ".terraform"));
|
|
193
|
+
const initialised = existsSync(join(dir, ".terraform", "terraform.tfstate"));
|
|
187
194
|
const hasTfvars = entries.some((entry) => entry.endsWith(".tfvars") || entry.endsWith(".tfvars.json"));
|
|
188
195
|
const environmentHint = environmentFromPath(relative);
|
|
189
196
|
const underStacksDir = /(^|[\\/])(environments|envs|stacks|live)([\\/]|$)/i.test(relative);
|
|
@@ -193,11 +200,13 @@ export function discoverRootModules(repoDir) {
|
|
|
193
200
|
signals.push("provider configuration");
|
|
194
201
|
if (initialised)
|
|
195
202
|
signals.push("initialised");
|
|
203
|
+
else if (hasPluginDir)
|
|
204
|
+
signals.push("providers downloaded, backend not initialised");
|
|
196
205
|
if (hasTfvars)
|
|
197
206
|
signals.push("tfvars");
|
|
198
207
|
if (underStacksDir)
|
|
199
208
|
signals.push("stacks directory");
|
|
200
|
-
if (hasBackend || hasProvider || initialised || (underStacksDir && hasTfvars) || (underStacksDir && environmentHint)) {
|
|
209
|
+
if (hasBackend || hasProvider || initialised || hasPluginDir || (underStacksDir && hasTfvars) || (underStacksDir && environmentHint)) {
|
|
201
210
|
found.push({ dir, relative: relative || ".", signals, environmentHint, initialised, usesLocalBackend: hasLocalBackend });
|
|
202
211
|
// A root module's subdirectories are its own modules, not further roots.
|
|
203
212
|
return;
|