@meshxdata/fops 0.1.49 → 0.1.51

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/package.json +1 -1
  3. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +347 -6
  4. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-data-bootstrap.js +421 -0
  5. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +5 -179
  6. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +14 -4
  7. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +171 -4
  8. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +303 -8
  9. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +2 -0
  10. package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +1 -1
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet-swarm.js +936 -0
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +10 -918
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +5 -0
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault-keys.js +413 -0
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +14 -399
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-config.js +754 -0
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-knock.js +527 -0
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-ssh.js +427 -0
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +99 -1686
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-health.js +279 -0
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +186 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +66 -444
  23. package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +11 -0
  24. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-lifecycle.js +5 -540
  25. package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-terraform.js +544 -0
  26. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +75 -3
  27. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +227 -11
  28. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +2 -1
  29. package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -0
  30. package/src/plugins/bundled/fops-plugin-foundation/index.js +371 -44
@@ -13,6 +13,10 @@ import {
13
13
  ensureOpenAiNetworkAccess, handleAzSessionExpired,
14
14
  } from "./azure-helpers.js";
15
15
  import { reconcileVm, provisionVm, configureVm, postStartChecks, cleanupStaleVmResources } from "./azure-provision.js";
16
+ import { vmTerraform } from "./azure-vm-terraform.js";
17
+
18
+ // Re-export for backwards compatibility
19
+ export { vmTerraform } from "./azure-vm-terraform.js";
16
20
 
17
21
  // ── Output ──────────────────────────────────────────────────────────────────
18
22
 
@@ -801,544 +805,5 @@ export async function azureStart(opts = {}) {
801
805
  printInfo(vmName, ip, adminUser, publicUrl);
802
806
  }
803
807
 
804
- // ── terraform ────────────────────────────────────────────────────────────────
805
-
806
- export async function vmTerraform(opts = {}) {
807
- const fs = await import("node:fs");
808
- const path = await import("node:path");
809
- const execa = await lazyExeca();
810
- const sub = opts.profile;
811
- await ensureAzCli(execa);
812
- await ensureAzAuth(execa, { subscription: sub });
813
- const state = requireVmState(opts.vmName);
814
- const { vmName, resourceGroup: rg } = state;
815
-
816
- banner(`VM Terraform: ${vmName}`);
817
- hint("Fetching VM and networking details from Azure…");
818
-
819
- const { stdout: vmJson } = await execa("az", [
820
- "vm", "show", "-g", rg, "-n", vmName, "--output", "json", ...subArgs(sub),
821
- ], { timeout: 30000 });
822
- const vm = JSON.parse(vmJson);
823
-
824
- let rgLocation = vm.location;
825
- try {
826
- const { stdout: rgJson } = await execa("az", [
827
- "group", "show", "--name", rg, "--output", "json", ...subArgs(sub),
828
- ], { timeout: 15000 });
829
- rgLocation = JSON.parse(rgJson).location || rgLocation;
830
- } catch { /* use vm location */ }
831
-
832
- // Resolve NIC
833
- const nicId = vm.networkProfile?.networkInterfaces?.[0]?.id || "";
834
- const nicName = nicId.split("/").pop();
835
- let nic = null;
836
- if (nicName) {
837
- try {
838
- const { stdout } = await execa("az", [
839
- "network", "nic", "show", "-g", rg, "-n", nicName, "--output", "json", ...subArgs(sub),
840
- ], { timeout: 15000 });
841
- nic = JSON.parse(stdout);
842
- } catch { /* no nic */ }
843
- }
844
-
845
- let pip = null;
846
- let nsg = null;
847
- let nsgRules = [];
848
- let subnet = null;
849
- let vnet = null;
850
-
851
- if (nic) {
852
- const ipConfig = nic.ipConfigurations?.[0] || {};
853
- const pipId = ipConfig.publicIPAddress?.id || "";
854
- const pipName = pipId.split("/").pop();
855
- const subnetId = ipConfig.subnet?.id || "";
856
- const nsgId = nic.networkSecurityGroup?.id || "";
857
- const nsgName = nsgId.split("/").pop();
858
-
859
- hint("Discovering networking resources…");
860
- const fetches = [];
861
-
862
- if (pipName) {
863
- fetches.push(
864
- execa("az", ["network", "public-ip", "show", "-g", rg, "-n", pipName, "--output", "json", ...subArgs(sub)],
865
- { reject: false, timeout: 15000 }).then(r => { if (r.exitCode === 0) pip = JSON.parse(r.stdout); })
866
- );
867
- }
868
- if (nsgName) {
869
- fetches.push(
870
- execa("az", ["network", "nsg", "show", "-g", rg, "-n", nsgName, "--output", "json", ...subArgs(sub)],
871
- { reject: false, timeout: 15000 }).then(r => {
872
- if (r.exitCode === 0) {
873
- nsg = JSON.parse(r.stdout);
874
- nsgRules = (nsg.securityRules || []).filter(rule => rule.direction === "Inbound" && rule.access === "Allow");
875
- }
876
- })
877
- );
878
- }
879
- if (subnetId) {
880
- const parts = subnetId.split("/");
881
- const vnetName = parts[parts.indexOf("virtualNetworks") + 1];
882
- const subnetName = parts[parts.indexOf("subnets") + 1];
883
- const vnetRg = parts[parts.indexOf("resourceGroups") + 1] || rg;
884
- fetches.push(
885
- execa("az", ["network", "vnet", "show", "-g", vnetRg, "-n", vnetName, "--output", "json", ...subArgs(sub)],
886
- { reject: false, timeout: 15000 }).then(r => {
887
- if (r.exitCode === 0) {
888
- vnet = JSON.parse(r.stdout);
889
- subnet = (vnet.subnets || []).find(s => s.name === subnetName) || null;
890
- }
891
- })
892
- );
893
- }
894
-
895
- await Promise.allSettled(fetches);
896
- }
897
-
898
- // Check for ADE Key Vault
899
- let adeKeyVault = null;
900
- const adeVaultName = `fops-ade-kv-${vm.location}`;
901
- try {
902
- const { stdout, exitCode } = await execa("az", [
903
- "keyvault", "show", "--name", adeVaultName, "--output", "json", ...subArgs(sub),
904
- ], { reject: false, timeout: 15000 });
905
- if (exitCode === 0 && stdout?.trim()) adeKeyVault = JSON.parse(stdout);
906
- } catch { /* no vault */ }
907
-
908
- const hcl = generateVmTerraform(vm, {
909
- rg, rgLocation, nic, pip, nsg, nsgRules, vnet, subnet, adeKeyVault, state,
910
- });
911
-
912
- console.log(OK(" ✓ Terraform HCL generated from live VM state\n"));
913
-
914
- if (opts.output) {
915
- fs.mkdirSync(path.dirname(path.resolve(opts.output)), { recursive: true });
916
- fs.writeFileSync(path.resolve(opts.output), hcl, "utf8");
917
- console.log(OK(` ✓ Written to ${opts.output}\n`));
918
- } else {
919
- console.log(hcl);
920
- }
921
- }
922
-
923
- function generateVmTerraform(vm, { rg, rgLocation, nic, pip, nsg, nsgRules, vnet, subnet, adeKeyVault, state }) {
924
- const tfName = (vm.name || "vm").replace(/[^a-zA-Z0-9_]/g, "_");
925
- const adminUser = vm.osProfile?.adminUsername || DEFAULTS.adminUser;
926
- const vmSize = vm.hardwareProfile?.vmSize || DEFAULTS.vmSize;
927
- const osDisk = vm.storageProfile?.osDisk || {};
928
- const imageRef = vm.storageProfile?.imageReference || {};
929
- const secProfile = vm.securityProfile || {};
930
- const isTrustedLaunch = secProfile.securityType === "TrustedLaunch";
931
- const linuxConfig = vm.osProfile?.linuxConfiguration || {};
932
-
933
- const lines = [];
934
- const w = (s = "") => lines.push(s);
935
-
936
- // ── variables ──────────────────────────────────────────────────────────────
937
-
938
- w(`# ─── Variables ────────────────────────────────────────────────────────────────`);
939
- w();
940
- w(`variable "resource_group_name" {`);
941
- w(` description = "Name of the Azure resource group"`);
942
- w(` type = string`);
943
- w(` default = "${rg}"`);
944
- w(`}`);
945
- w();
946
- w(`variable "location" {`);
947
- w(` description = "Azure region"`);
948
- w(` type = string`);
949
- w(` default = "${rgLocation}"`);
950
- w(`}`);
951
- w();
952
- w(`variable "vm_name" {`);
953
- w(` description = "Virtual machine name"`);
954
- w(` type = string`);
955
- w(` default = "${vm.name}"`);
956
- w(`}`);
957
- w();
958
- w(`variable "vm_size" {`);
959
- w(` description = "VM size / SKU"`);
960
- w(` type = string`);
961
- w(` default = "${vmSize}"`);
962
- w(`}`);
963
- w();
964
- w(`variable "admin_username" {`);
965
- w(` description = "SSH admin username"`);
966
- w(` type = string`);
967
- w(` default = "${adminUser}"`);
968
- w(`}`);
969
- w();
970
- w(`variable "os_disk_size_gb" {`);
971
- w(` description = "OS disk size in GB"`);
972
- w(` type = number`);
973
- w(` default = ${osDisk.diskSizeGb || DEFAULTS.osDiskSizeGb}`);
974
- w(`}`);
975
- w();
976
- w(`variable "ssh_public_key_path" {`);
977
- w(` description = "Path to SSH public key for admin access"`);
978
- w(` type = string`);
979
- w(` default = "~/.ssh/id_rsa.pub"`);
980
- w(`}`);
981
- w();
982
-
983
- if (state?.publicUrl) {
984
- w(`variable "public_url" {`);
985
- w(` description = "Public URL for Foundation platform"`);
986
- w(` type = string`);
987
- w(` default = "${state.publicUrl}"`);
988
- w(`}`);
989
- w();
990
- }
991
-
992
- // ── provider ───────────────────────────────────────────────────────────────
993
-
994
- w(`# ─── Provider ─────────────────────────────────────────────────────────────────`);
995
- w();
996
- w(`terraform {`);
997
- w(` required_providers {`);
998
- w(` azurerm = {`);
999
- w(` source = "hashicorp/azurerm"`);
1000
- w(` version = "~> 4.0"`);
1001
- w(` }`);
1002
- w(` }`);
1003
- w(`}`);
1004
- w();
1005
- w(`provider "azurerm" {`);
1006
- w(` features {}`);
1007
- w(`}`);
1008
- w();
1009
-
1010
- // ── resource group ─────────────────────────────────────────────────────────
1011
-
1012
- w(`# ─── Resource Group ──────────────────────────────────────────────────────────`);
1013
- w();
1014
- w(`resource "azurerm_resource_group" "vm" {`);
1015
- w(` name = var.resource_group_name`);
1016
- w(` location = var.location`);
1017
- w(`}`);
1018
- w();
1019
-
1020
- // ── VNet + Subnet ──────────────────────────────────────────────────────────
1021
-
1022
- if (vnet) {
1023
- const vnetTf = (vnet.name || "vnet").replace(/[^a-zA-Z0-9_]/g, "_");
1024
- const addrSpace = vnet.addressSpace?.addressPrefixes?.[0] || "10.0.0.0/16";
1025
- w(`# ─── Network ──────────────────────────────────────────────────────────────────`);
1026
- w();
1027
- w(`resource "azurerm_virtual_network" "${vnetTf}" {`);
1028
- w(` name = "${vnet.name}"`);
1029
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1030
- w(` location = azurerm_resource_group.vm.location`);
1031
- w(` address_space = ["${addrSpace}"]`);
1032
- w(`}`);
1033
- w();
1034
-
1035
- if (subnet) {
1036
- const subnetTf = (subnet.name || "subnet").replace(/[^a-zA-Z0-9_]/g, "_");
1037
- const subnetPrefix = subnet.addressPrefix || "10.0.0.0/24";
1038
- w(`resource "azurerm_subnet" "${subnetTf}" {`);
1039
- w(` name = "${subnet.name}"`);
1040
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1041
- w(` virtual_network_name = azurerm_virtual_network.${vnetTf}.name`);
1042
- w(` address_prefixes = ["${subnetPrefix}"]`);
1043
- w(`}`);
1044
- w();
1045
- }
1046
- }
1047
-
1048
- // ── NSG ────────────────────────────────────────────────────────────────────
1049
-
1050
- if (nsg) {
1051
- const nsgTf = (nsg.name || "nsg").replace(/[^a-zA-Z0-9_]/g, "_");
1052
- w(`# ─── Network Security Group ───────────────────────────────────────────────────`);
1053
- w();
1054
- w(`resource "azurerm_network_security_group" "${nsgTf}" {`);
1055
- w(` name = "${nsg.name}"`);
1056
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1057
- w(` location = azurerm_resource_group.vm.location`);
1058
-
1059
- for (const rule of nsgRules) {
1060
- w();
1061
- w(` security_rule {`);
1062
- w(` name = "${rule.name}"`);
1063
- w(` priority = ${rule.priority}`);
1064
- w(` direction = "${rule.direction}"`);
1065
- w(` access = "${rule.access}"`);
1066
- w(` protocol = "${rule.protocol}"`);
1067
- w(` source_port_range = "${rule.sourcePortRange || "*"}"`);
1068
- w(` destination_port_range = "${rule.destinationPortRange || "*"}"`);
1069
- w(` source_address_prefix = "${rule.sourceAddressPrefix || "*"}"`);
1070
- w(` destination_address_prefix = "${rule.destinationAddressPrefix || "*"}"`);
1071
- w(` }`);
1072
- }
1073
-
1074
- w(`}`);
1075
- w();
1076
- }
1077
808
 
1078
- // ── Public IP ──────────────────────────────────────────────────────────────
1079
-
1080
- if (pip) {
1081
- const pipTf = (pip.name || "pip").replace(/[^a-zA-Z0-9_]/g, "_");
1082
- w(`# ─── Public IP ────────────────────────────────────────────────────────────────`);
1083
- w();
1084
- w(`resource "azurerm_public_ip" "${pipTf}" {`);
1085
- w(` name = "${pip.name}"`);
1086
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1087
- w(` location = azurerm_resource_group.vm.location`);
1088
- w(` allocation_method = "${pip.publicIPAllocationMethod || "Static"}"`);
1089
- w(` sku = "${pip.sku?.name || "Standard"}"`);
1090
- const pipTags = pip.tags || {};
1091
- const pipTagEntries = Object.entries(pipTags);
1092
- if (pipTagEntries.length) {
1093
- w();
1094
- w(` tags = {`);
1095
- for (const [k, v] of pipTagEntries) {
1096
- w(` ${JSON.stringify(k)} = ${JSON.stringify(v)}`);
1097
- }
1098
- w(` }`);
1099
- }
1100
- w(`}`);
1101
- w();
1102
- }
1103
-
1104
- // ── NIC ────────────────────────────────────────────────────────────────────
1105
-
1106
- if (nic) {
1107
- const nicTf = (nic.name || "nic").replace(/[^a-zA-Z0-9_]/g, "_");
1108
- const vnetTf = vnet ? (vnet.name || "vnet").replace(/[^a-zA-Z0-9_]/g, "_") : null;
1109
- const subnetTf = subnet ? (subnet.name || "subnet").replace(/[^a-zA-Z0-9_]/g, "_") : null;
1110
- const nsgTf = nsg ? (nsg.name || "nsg").replace(/[^a-zA-Z0-9_]/g, "_") : null;
1111
- const pipTf = pip ? (pip.name || "pip").replace(/[^a-zA-Z0-9_]/g, "_") : null;
1112
-
1113
- w(`# ─── Network Interface ────────────────────────────────────────────────────────`);
1114
- w();
1115
- w(`resource "azurerm_network_interface" "${nicTf}" {`);
1116
- w(` name = "${nic.name}"`);
1117
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1118
- w(` location = azurerm_resource_group.vm.location`);
1119
- if (nic.enableAcceleratedNetworking) {
1120
- w(` accelerated_networking_enabled = true`);
1121
- }
1122
- w();
1123
- w(` ip_configuration {`);
1124
- w(` name = "${nic.ipConfigurations?.[0]?.name || "ipconfig1"}"`);
1125
- if (subnetTf) {
1126
- w(` subnet_id = azurerm_subnet.${subnetTf}.id`);
1127
- }
1128
- w(` private_ip_address_allocation = "${nic.ipConfigurations?.[0]?.privateIPAllocationMethod || "Dynamic"}"`);
1129
- if (pipTf) {
1130
- w(` public_ip_address_id = azurerm_public_ip.${pipTf}.id`);
1131
- }
1132
- w(` }`);
1133
- w(`}`);
1134
- w();
1135
-
1136
- if (nsgTf) {
1137
- w(`resource "azurerm_network_interface_security_group_association" "${nicTf}_nsg" {`);
1138
- w(` network_interface_id = azurerm_network_interface.${nicTf}.id`);
1139
- w(` network_security_group_id = azurerm_network_security_group.${nsgTf}.id`);
1140
- w(`}`);
1141
- w();
1142
- }
1143
- }
1144
-
1145
- // ── ADE Key Vault ──────────────────────────────────────────────────────────
1146
-
1147
- if (adeKeyVault) {
1148
- const kvTf = (adeKeyVault.name || "ade_kv").replace(/[^a-zA-Z0-9_]/g, "_");
1149
- w(`# ─── Disk Encryption Key Vault ────────────────────────────────────────────────`);
1150
- w();
1151
- w(`resource "azurerm_key_vault" "${kvTf}" {`);
1152
- w(` name = "${adeKeyVault.name}"`);
1153
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1154
- w(` location = azurerm_resource_group.vm.location`);
1155
- w(` tenant_id = "${adeKeyVault.properties?.tenantId || ""}"`);
1156
- w(` sku_name = "${adeKeyVault.properties?.sku?.name || "standard"}"`);
1157
- w(` enabled_for_disk_encryption = true`);
1158
- if (adeKeyVault.properties?.enableRbacAuthorization) {
1159
- w(` enable_rbac_authorization = true`);
1160
- }
1161
- if (adeKeyVault.properties?.softDeleteRetentionInDays) {
1162
- w(` soft_delete_retention_days = ${adeKeyVault.properties.softDeleteRetentionInDays}`);
1163
- }
1164
- if (adeKeyVault.properties?.enablePurgeProtection) {
1165
- w(` purge_protection_enabled = true`);
1166
- }
1167
- w(`}`);
1168
- w();
1169
- }
1170
-
1171
- // ── Virtual Machine ────────────────────────────────────────────────────────
1172
-
1173
- const nicTf = nic ? (nic.name || "nic").replace(/[^a-zA-Z0-9_]/g, "_") : null;
1174
-
1175
- w(`# ─── Virtual Machine ─────────────────────────────────────────────────────────`);
1176
- w();
1177
- w(`resource "azurerm_linux_virtual_machine" "${tfName}" {`);
1178
- w(` name = var.vm_name`);
1179
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1180
- w(` location = azurerm_resource_group.vm.location`);
1181
- w(` size = var.vm_size`);
1182
- w(` admin_username = var.admin_username`);
1183
- if (linuxConfig.disablePasswordAuthentication !== false) {
1184
- w(` disable_password_authentication = true`);
1185
- }
1186
- if (nicTf) {
1187
- w(` network_interface_ids = [azurerm_network_interface.${nicTf}.id]`);
1188
- }
1189
- w();
1190
-
1191
- w(` admin_ssh_key {`);
1192
- w(` username = var.admin_username`);
1193
- w(` public_key = file(var.ssh_public_key_path)`);
1194
- w(` }`);
1195
- w();
1196
-
1197
- w(` os_disk {`);
1198
- w(` name = "${osDisk.name || vm.name + "-osdisk"}"`);
1199
- w(` caching = "${osDisk.caching || "ReadWrite"}"`);
1200
- w(` storage_account_type = "${osDisk.managedDisk?.storageAccountType || "Premium_LRS"}"`);
1201
- w(` disk_size_gb = var.os_disk_size_gb`);
1202
- w(` }`);
1203
- w();
1204
-
1205
- if (imageRef.publisher) {
1206
- w(` source_image_reference {`);
1207
- w(` publisher = "${imageRef.publisher}"`);
1208
- w(` offer = "${imageRef.offer}"`);
1209
- w(` sku = "${imageRef.sku}"`);
1210
- w(` version = "${imageRef.exactVersion || imageRef.version || "latest"}"`);
1211
- w(` }`);
1212
- w();
1213
- } else if (imageRef.id) {
1214
- w(` source_image_id = "${imageRef.id}"`);
1215
- w();
1216
- }
1217
-
1218
- if (isTrustedLaunch) {
1219
- w(` secure_boot_enabled = ${secProfile.uefiSettings?.secureBootEnabled ?? true}`);
1220
- w(` vtpm_enabled = ${secProfile.uefiSettings?.vTpmEnabled ?? true}`);
1221
- w();
1222
- }
1223
-
1224
- if (vm.diagnosticsProfile?.bootDiagnostics?.enabled) {
1225
- w(` boot_diagnostics {}`);
1226
- w();
1227
- }
1228
-
1229
- if (vm.identity?.type && vm.identity.type !== "None") {
1230
- w(` identity {`);
1231
- w(` type = "${vm.identity.type}"`);
1232
- w(` }`);
1233
- w();
1234
- }
1235
-
1236
- const patchMode = linuxConfig.patchSettings?.patchMode;
1237
- if (patchMode) {
1238
- w(` patch_mode = "${patchMode}"`);
1239
- if (patchMode === "AutomaticByPlatform") {
1240
- w(` patch_assessment_mode = "AutomaticByPlatform"`);
1241
- }
1242
- w();
1243
- }
1244
-
1245
- w(` # Foundation provisioning script — uncomment and point to your cloud-init`);
1246
- w(` # custom_data = filebase64("cloud-init-foundation.sh")`);
1247
- w();
1248
-
1249
- const tags = vm.tags || {};
1250
- const tagEntries = Object.entries(tags);
1251
- if (tagEntries.length) {
1252
- w(` tags = {`);
1253
- for (const [k, v] of tagEntries) {
1254
- w(` ${JSON.stringify(k)} = ${JSON.stringify(v)}`);
1255
- }
1256
- w(` }`);
1257
- w();
1258
- }
1259
-
1260
- w(`}`);
1261
- w();
1262
-
1263
- // ── Data disks ─────────────────────────────────────────────────────────────
1264
-
1265
- for (const dd of vm.storageProfile?.dataDisks || []) {
1266
- const ddTf = (dd.name || `datadisk_lun${dd.lun}`).replace(/[^a-zA-Z0-9_]/g, "_");
1267
- w(`resource "azurerm_managed_disk" "${ddTf}" {`);
1268
- w(` name = "${dd.name}"`);
1269
- w(` resource_group_name = azurerm_resource_group.vm.name`);
1270
- w(` location = azurerm_resource_group.vm.location`);
1271
- w(` storage_account_type = "${dd.managedDisk?.storageAccountType || "Premium_LRS"}"`);
1272
- w(` create_option = "Empty"`);
1273
- w(` disk_size_gb = ${dd.diskSizeGb || 128}`);
1274
- w(`}`);
1275
- w();
1276
- w(`resource "azurerm_virtual_machine_data_disk_attachment" "${ddTf}" {`);
1277
- w(` managed_disk_id = azurerm_managed_disk.${ddTf}.id`);
1278
- w(` virtual_machine_id = azurerm_linux_virtual_machine.${tfName}.id`);
1279
- w(` lun = ${dd.lun}`);
1280
- w(` caching = "${dd.caching || "ReadWrite"}"`);
1281
- w(`}`);
1282
- w();
1283
- }
1284
-
1285
- // ── Resource lock ──────────────────────────────────────────────────────────
1286
-
1287
- w(`resource "azurerm_management_lock" "${tfName}_lock" {`);
1288
- w(` name = "${vm.name}-lock"`);
1289
- w(` scope = azurerm_linux_virtual_machine.${tfName}.id`);
1290
- w(` lock_level = "CanNotDelete"`);
1291
- w(` notes = "Managed by fops — prevents accidental deletion"`);
1292
- w(`}`);
1293
- w();
1294
-
1295
- // ── VM extension: ADE ──────────────────────────────────────────────────────
1296
-
1297
- const adeExt = (vm.resources || []).find(r => r.id?.toLowerCase().includes("azurediskencryption"));
1298
- if (adeExt && adeKeyVault) {
1299
- const kvTf = (adeKeyVault.name || "ade_kv").replace(/[^a-zA-Z0-9_]/g, "_");
1300
- w(`resource "azurerm_virtual_machine_extension" "ade" {`);
1301
- w(` name = "AzureDiskEncryption"`);
1302
- w(` virtual_machine_id = azurerm_linux_virtual_machine.${tfName}.id`);
1303
- w(` publisher = "Microsoft.Azure.Security"`);
1304
- w(` type = "AzureDiskEncryptionForLinux"`);
1305
- w(` type_handler_version = "1.4"`);
1306
- w();
1307
- w(` settings = jsonencode({`);
1308
- w(` EncryptionOperation = "EnableEncryption"`);
1309
- w(` KeyVaultURL = azurerm_key_vault.${kvTf}.vault_uri`);
1310
- w(` KeyVaultResourceId = azurerm_key_vault.${kvTf}.id`);
1311
- w(` VolumeType = "All"`);
1312
- w(` })`);
1313
- w(`}`);
1314
- w();
1315
- }
1316
-
1317
- // ── outputs ────────────────────────────────────────────────────────────────
1318
-
1319
- w(`# ─── Outputs ─────────────────────────────────────────────────────────────────`);
1320
- w();
1321
- w(`output "vm_name" {`);
1322
- w(` value = azurerm_linux_virtual_machine.${tfName}.name`);
1323
- w(`}`);
1324
- w();
1325
- if (pip) {
1326
- const pipTf = (pip.name || "pip").replace(/[^a-zA-Z0-9_]/g, "_");
1327
- w(`output "public_ip" {`);
1328
- w(` value = azurerm_public_ip.${pipTf}.ip_address`);
1329
- w(`}`);
1330
- w();
1331
- }
1332
- w(`output "admin_username" {`);
1333
- w(` value = var.admin_username`);
1334
- w(`}`);
1335
- w();
1336
- if (state?.publicUrl) {
1337
- w(`output "public_url" {`);
1338
- w(` value = var.public_url`);
1339
- w(`}`);
1340
- w();
1341
- }
1342
-
1343
- return lines.join("\n");
1344
- }
809
+ // vmTerraform is now in azure-vm-terraform.js and re-exported above for backwards compatibility