Last active
June 3, 2024 06:50
-
-
Save Basssiiie/a1140a21f853a6196776eae3e01d1205 to your computer and use it in GitHub Desktop.
az deployment what-if workaround for skipping reference()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Workaround for issue: | |
* https://github.com/Azure/arm-template-whatif/issues/157 | |
* | |
* This script will evaluate various ARM functions in your Bicep/ARM template and replace them with hardcoded values where possible. | |
* | |
* 1. Converts Bicep templates to a single ARM template using `az bicep decompile`. | |
* 2. Parses ARM template as JSON. | |
* 3. Uses `eval()` to attempt to evaluate ARM functions in fields that have them. | |
* 4. References will either: | |
* - reference local properties within the template; | |
* - other deployments in the templates; | |
* - call `az resource show` to retrieve existing resource details from Azure. | |
* 5. The script can't and doesn't need to solve all ARM functions. If a property evaluation errors or fails, the script | |
* will keep it as-is so az what-if can try solving it. | |
* 6. Script will output both Bicep decompile and final template before the what-if to the output folder for personal review. | |
* | |
* Requires NodeJS and Azure CLI installed. | |
* | |
* Run using: | |
* `node what-if.mjs --in=<input bicep/arm template> --out=<output arm template> -g=<resource group> --subscription=<azure subscription id>` | |
*/ | |
import { execSync } from 'node:child_process'; | |
import { readFileSync, writeFileSync } from 'node:fs' | |
const args = {} | |
const params = {} | |
for (const arg of process.argv) | |
{ | |
const [key, value] = arg.split("=") | |
switch (key) | |
{ | |
case "--in": args.in = value; break; | |
case "--out": args.out = value; break; | |
case "--resource-group": | |
case "-g": args.resourceGroup = value; break; | |
case "--subscription": args.subscription = value; break; | |
default: params[key] = value; break; | |
} | |
} | |
let json | |
if (args.in.endsWith(".bicep")) | |
{ | |
json = execSync(`az bicep build -f "${args.in}" --stdout`, { encoding: 'utf8' }) | |
} | |
else if (args.in.endsWith(".json")) | |
{ | |
json = readFileSync(args.in, 'utf8') | |
} | |
else throw Error(`Invalid input file: ${args.in}`) | |
function findReferences(entry, key, parent, path) | |
{ | |
if (typeof entry == "string") | |
{ | |
if (entry[0] == '[') | |
{ | |
parent[key] = evaluateFunctionsSafe(entry, path) | |
} | |
} | |
else if (typeof entry == "object") | |
{ | |
const isDeployment = (entry["type"] == "Microsoft.Resources/deployments") | |
if (isDeployment) | |
{ | |
const name = evaluateFunctionsSafe(entry["name"]) | |
deploymentTemplates[name] ||= entry.properties.template // todo: only registers the first if deployments have duplicate names | |
deploymentsStack.push(entry) | |
} | |
const isTemplate = (key == "template" && /^https:\/\/schema\.management\.azure\.com\/schemas\/[\d-]+\/deploymentTemplate\.json#$/.test(entry["$schema"])) | |
if (isTemplate) | |
{ | |
templatesStack.push(parent) | |
} | |
for (const key in entry) | |
{ | |
findReferences(entry[key], key, entry, `${path}.${key}`) | |
} | |
isDeployment && deploymentsStack.pop(); | |
isTemplate && templatesStack.pop() | |
} | |
} | |
function evaluateFunctionsSafe(value, path) | |
{ | |
try | |
{ | |
const result = evaluateFunctions(value) | |
console.log(`Parsed: ${value}\n to ${result}\n at ${path || '?'}`) | |
return result; | |
} | |
catch (error) | |
{ | |
console.warn(`Failed to parse: ${value}\n at ${path || '?'}\n`, error) | |
return value; | |
} | |
} | |
function evaluateFunctions(value) | |
{ | |
if (value[0] != '[') | |
{ | |
return value; | |
} | |
const result = eval(value)[0] | |
return (result instanceof ResourceId) ? result.toString() : result; | |
} | |
function reference(resource, version) | |
{ | |
if (typeof resource == 'string') // local reference | |
{ | |
const resources = templatesStack[templatesStack.length - 1].template.resources; | |
if (resource in resources) | |
{ | |
return resources[resource].properties.template; | |
} | |
} | |
if (resource.type == "Microsoft.Resources/deployments") // deployment reference | |
{ | |
return deploymentTemplates[resource.name] | |
} | |
// existing reference | |
const result = execSync(`az resource show --api-version "${version}" --ids "${resource}"`, { encoding: 'utf8' }) | |
return JSON.parse(result).properties | |
} | |
function resourceId(type, name) | |
{ | |
return new ResourceId(type, name) | |
} | |
function deployment() | |
{ | |
return deploymentsStack[deploymentsStack.length - 1]; | |
} | |
function parameters(name) | |
{ | |
for (let idx = templatesStack.length - 1; idx >= 0; idx--) | |
{ | |
const params = templatesStack[idx].parameters?.[name]?.value; | |
if (params !== undefined) | |
{ | |
return params; | |
} | |
} | |
if (name in params) | |
{ | |
return params[name] | |
} | |
throw Error(`Parameter not found: ${name}`) | |
} | |
function variables(name) | |
{ | |
for (let idx = templatesStack.length - 1; idx >= 0; idx--) | |
{ | |
const vars = templatesStack[idx].template.variables?.[name]; | |
if (vars) | |
{ | |
return vars; | |
} | |
} | |
throw Error(`Variable not found: ${name}`) | |
} | |
function format(string, ...args) | |
{ | |
return string.replace(/{(\d+)}/g, (match, number) => evaluateFunctions(args[number]) ?? match) | |
} | |
function split(string, separator) | |
{ | |
return string.split(separator) | |
} | |
class ResourceId | |
{ | |
constructor(type, name) | |
{ | |
this.type = type; | |
this.name = name; | |
} | |
toString() | |
{ | |
return `/subscriptions/${args.subscription}/resourceGroups/${args.resourceGroup}/providers/${this.type}/${this.name}` | |
} | |
} | |
const template = JSON.parse(json); | |
const deploymentTemplates = {}; | |
const deploymentsStack = []; | |
const templatesStack = [ { template }]; | |
writeFileSync(args.out.replace('.json', '.tmp.json'), JSON.stringify(template, null, 2), "utf8") | |
findReferences(template, null, null, "") | |
writeFileSync(args.out, JSON.stringify(template, null, 2), "utf8") | |
execSync(`az deployment group what-if -f "${args.out}" -g "${args.resourceGroup}" --subscription "${args.subscription}"`, { stdio: 'inherit' }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment