Cannot concat two arrays in ARM template - azure

Given two variables of type array in an ARM template, using concat gives me:
The template variable 'thirdArray' is not valid: Unable to evaluate language function 'concat': al
l function arguments must be string literals, integer values, boolean values or arrays.
All function arguments are arrays, so I don't see what's wrong.
"variables": {
"firstArray": {
"type": "array",
"value": [
"1-1",
"1-2",
"1-3"
]
},
"secondArray": {
"type": "array",
"value": [
"2-1",
"2-2",
"2-3"
]
},
"thirdArray": {
"type": "array",
"value": "[concat(variables('firstArray'), variables('secondArray'))]"
}
}

Looking at the documentation, your syntax is incorrect:
When defining a variable, you don't specify a data type for the variable. Instead provide a value or template expression. The variable type is inferred from the resolved value.
This works for me:
"variables": {
"firstArray": [
"1-1",
"1-2",
"1-3"
],
"secondArray": [
"2-1",
"2-2",
"2-3"
],
"thirdArray": "[concat(variables('firstArray'), variables('secondArray'))]"
}

Related

How to convert an array into properties of one object in an ARM template?

I am looking for a way to convert an array (e.g. of strings) into one object, where the properties are generated from the array values.
Use case: I want to generate a tags object with links to resources, based on a list of resource names. I need to do this, to link App Service resources to an Application Insights resource.
The list of resources could be supplied using a parameter:
"parameters": {
"appServices": {
"type": "array",
"metadata": {
"description": "Names of app services to link this application insights resource to via hidden tags"
}
}
}
Sample input:
['appName1', 'appName2', 'appName3']
Sample output:
"tags":
{
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName1'))]": "Resource",
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName2'))]": "Resource",
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName3'))]": "Resource"
}
I know you can use copy to loop over arrays but that will create an array of objects and not a single object (which is required for tags), for example:
[
{
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName1'))]": "Resource"
},
{
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName2'))]": "Resource"
},
{
"[concat('hidden-link:', resourceId('Microsoft.Web/sites/', 'appName3'))]": "Resource"
}
]
It would be possible to use union to merge those objects again, but that function requires you to hardcode the objects you want to merge, so it does not work when you have an input with variable length.
What I am looking for is a way to do this in a dynamic way.
There is no direct option to convert array to object.
But here's a hack to achieve what you need. This will work for array of any length.
Steps:
append hidden-link text to service names
convert array to string
replace necessary symbols and make it a valid json string.
use json() to convert string to object
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appServices": {
"type": "array",
"metadata": {
"description": "Names of app services to link this application insights resource to via hidden tags"
},
"defaultValue": [ "appName1", "appName2", "appName3" ]
}
},
"functions": [],
"variables": {
"copy": [
{
"name": "as",
"count": "[length(parameters('appServices'))]",
"input": "[concat('hidden-link:', resourceId('Microsoft.Web/sites/', parameters('appServices')[copyIndex('as')]))]"
}
],
"0": "[string(variables('as'))]",
"1": "[replace(variables('0'), '[', '{')]",
"2": "[replace(variables('1'), '\",', '\":\"Resource\",')]",
"3": "[replace(variables('2'), '\"]', '\":\"Resource\"}')]"
},
"resources": [],
"outputs": {
"op1": {
"type": "object",
"value": "[json(variables('3'))]"
}
}
}
Now that lambdas have been added to bicep you can convert arrays to objects using reduce. Note that the array you reduce must consist of object items. If it doesn't you can convert it to an object using map.
// With array of objects
var names = [
{
name: 'foo'
id: 'foo-id'
}
{
name: 'bar'
id: 'bar-id'
}
]
var nameIds = reduce(names, {}, (cur, next) => union(cur, {
'${next.name}': next.id
}))
output test object = nameIds
// With array of strings
var names = [
'foo'
'bar'
]
var nameMaps = map(names, (name) => {name: name})
var nameIds = reduce(nameMaps, {}, (cur, next) => union(cur, {
'${next.name}': next.name
}))
output test object = nameIds
I'm not sure if this is the best approach to this problem.
Tags are supposed to be metadata about a specific object/service. Wouldn't it make more sense to apply a tag (say your system name, environment, etc..) and then run a query against azure on that tag?
This should achieve the same result pulling back all related resources.
I don't know if it is still relevant, but since 2021 it is possible to do with items() function

ARM template transform array of strings into array of objects

Not sure if this functionality exists. I'm trying to transform a list of comma separated IP addresses from the Azure DevOps build parameters into an array of objects. So far it's only splitting a comma separated list into an array of strings, but the template needs an array of objects.
The parameter value is a comma separated list of IP Addresses.
e.g. "192.168.0.1,192.168.0.2/32,127.0.0.1"
The ARM template would look like:
"variables": {
"ipaddresses": "[split(parameters('ipaddresses'), ',')]"
},
"resources": [
...
"ipRestrictions": "[stringArrToObjArr(variables('ipaddresses'))]" <--
...
]
And ideally function with the arrow above would yield a value for ipRestictions would be something like:
[
{
"ipAddress": "192.168.0.1"
},
{
"ipAddress": "192.168.0.2/32"
},
{
"ipAddress": "127.0.0.1"
},
]
you can use copy() function to do that:
"variables": {
"ipaddresses": "[split(parameters('ipaddresses'), ',')]"
"copy": [
{
"name": "myVariable",
"count": "[length(variables('ipaddresses'))]",
"input": {
"ipAddress": "[variables('ipaddresses')[copyIndex('myVariable')]]"
}
}
]
},
this would return the desired object into a variable called myVariable. if you want to rename it >> don't forget to rename it inside copyIndex() as well

JSON objects as ARM template function argument

Does the ARM template offers a way to define a JSON object inline, as a template function parameter?
Something that could look like this, where I am mixing references, Azure template functions and JSON object.
"value": "[concat(reference('ArrayMaker').outputs.fooBarArray.value],
[{ "cat": "Tom", "mouse" : "Jerry"}, { "cat":"Garfield", "mouse":"[reference('MouseTrap').outputs.mouseTrap.value]"} ] )]"
Using variables would seem to be a natural fit for this, but since the value is constructed from a reference, variables can't be used.
well, not natively. you can hack around with nested deployments, something like this:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"garfiled": {
"type": "string"
},
"catData": {
"type": "object",
"defaultValue": {
"cat": "Tom"
}
}
},
"variables": {
"cat": { <<< if you can\need to construct whole variable in the nested template
"cat": "Garfield",
"mouse": "[parameters('garfiled')]"
},
"t&j": { <<< if you need to pass in part of the variable to the nested template, you can also create another variable to create an object of a proper structure to union with existing object
"mouse": "Jerry"
}
},
"resources": [],
"outputs": {
"garfiled": {
"type": "object",
"value": "[variables('cat')]"
},
"t&j": {
"type": "object",
"value": "[union(variables('t&j'), parameters('catData'))]"
}
}
}
You would then use nested template to pass reference to this template and output the result.

Recursive data type like a tree as Avro schema

Reading https://avro.apache.org/docs/current/spec.html it says a schema must be one of:
A JSON string, naming a defined type.
A JSON object, of the form:
{"type": "typeName" ...attributes...} where typeName is either a
primitive or derived type name, as defined below. Attributes not
defined in this document are permitted as metadata, but must not
affect the format of serialized data.
A JSON array, representing a
union of embedded types.
I want a schema that describes a tree, using the recursive definition that a tree is either:
A node with a value (say, integer) and a list of trees (the children)
A leaf with a value
My initial attempt looked like:
{
"name": "Tree",
"type": [
{
"name": "Node",
"type": "record",
"fields": [
{
"name": "value",
"type": "long"
},
{
"name": "children",
"type": { "type": "array", "items": "Tree" }
}
]
},
{
"name": "Leaf",
"type": "record",
"fields": [
{
"name": "value",
"type": "long"
}
]
}
]
}
But the Avro compiler rejects this, complaining there is nothing of type {"name":"Tree","type":[{"name":"Node".... It seems Avro doesn't like the union type at the top-level. I'm guessing this falls under the aforementioned rule "a schema must be one of .. a JSON object .. where typeName is either a primitive or derived type name." I am not sure what a "derived type name" is though. At first I thought it was the same as a "complex type" but that includes union types..
Anyways, changing it to the more convoluted definition:
{
"name": "Tree",
"type": "record",
"fields": [{
"name": "ctors",
"type": [
{
"name": "Node",
"type": "record",
"fields": [
{
"name": "value",
"type": "long"
},
{
"name": "children",
"type": { "type": "array", "items": "Tree" }
}
]
},
{
"name": "Leaf",
"type": "record",
"fields": [
{
"name": "value",
"type": "long"
}
]
}
]
}]
}
works, but now I have this weird record with just a single field whose sole purpose is to let me define the top-level union type I want.
Is this the only way to get what I want in Avro or is there a better way?
Thanks!
While this is not an answer to the actual question about representing a recursive named union (which isn't possible as of late 2022), it is possible to work around this for a tree-like data structure.
If you represent a Tree as a node, and a Leaf as a node with an empty list of children, then one recursive type is sufficient:
{
"type": "record",
"name": "TreeNode",
"fields": [
{
"name": "value",
"type": "long"
},
{
"name": "children",
"type": { "type": "array", "items": "TreeNode" }
}
]
}
Now, your three types Tree, Node, and Leaf are unified into one type TreeNode, and there is no union of Node and Leaf necessary.
I just stumbled uppon the same problem wanting to define a recursive union. I'm quite pessimistic about a cleaner solution than your convoluted one, because there is currently no way to name an union, and hence no way to recursively refer to it while constructing it, see this open ticket

using conditional IF statements in arm templates for properties,

I have a situation where I want to only add a property to a VM if a condition is met. For example if I want to add an availability set property to a machine then do this : Below I ONLY what to execute the availability set statement if a condition is TRUE, can you do this in an ARM template? eg if a value is true then do this line, if not skip?
{
"name": "[parameters('ComputerName')]",
"type": "Microsoft.Compute/virtualMachines",
"location": "[parameters('location')]",
"apiVersion": "2017-03-30",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', variables('1stNicName'))]",
"[resourceId('Microsoft.Network/networkInterfaces', variables('2ndicName'))]"
],
"tags": {
"displayName": "[parameters('ComputerName')]"
},
"properties":
{
"availabilitySet": {
"id": "[resourceId('Microsoft.Compute/availabilitySets',variables('availabilitySetName'))]"
},
"hardwareProfile": {
"vmSize": "[parameters('serverVmSize')]"
},
"osProfile": {
"computerName": "[parameters('serverName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
As of this week (1st August 2017), ARM templates now have an IF function that can be used for variables, properties and resource parameters. It works like most other ARM functions, with syntax like:
[if(condition, true value, false value)]
The example in the documentation does exactly what you are asking, adds (or doesn't add) an availbility set to a VM:
"apiVersion": "2016-03-30",
"type": "Microsoft.Compute/virtualMachines",
"properties": {
"availabilitySet": "[if(equals(parameters('availabilitySet'),'yes'), variables('availabilitySet'), json('null'))]",
...
}
Note that the True value, the availiblity set to use, is stored as a variable rather than inline in the if statement.
"variables": {
...
"availabilitySetName": "availabilitySet1",
"availabilitySet": {
"id": "[resourceId('Microsoft.Compute/availabilitySets',variables('availabilitySetName'))]"
}
},
There is no direct way of doing this. you can use conditional to choose a value, but you cannot not set a value or pass in null (unless you want to define 2 variables for `properties').
so you can just define 2 vm resources that are exactly the same except for the property in question (so availabilitySet). One would have it and the other one would not. And you would use "condition": expression in both resources. Condition should equals to false or true to work. There are several functions in ARM templates that can evaluate input and return true or false.
Reference:
https://samcogan.com/conditions-in-arm-templates-the-right-way/
"variables": {
"loadBalancerBackendAddressPools": [
{
"id": "[parameters('lbBackendAddressPool')]"
}
]
},
Believe i have found a workable way to avoid defining two entire VM definitions for an availability set. following are from my own testing in an arm template i have made with extensive conditional controls.
this works:
"availabilitySet": {
"id": "[resourceId('Microsoft.Compute/availabilitySets',parameters('availabilitySetName'))]"
}
this does not:
"availabilitySet": "[if(equals(parameters('availabilitySet'),'yes'), variables('availabilitySet'), json('null'))]",
this also does not work due to ID being 'null':
"availabilitySet": {
"id": "[if(equals(parameters('availabilitySet'),'yes'), variables('availabilitySet'), json('null'))]"
}
I have added the following variable and parameter:
"parameters": {
"SpotInstance": {
"type": "bool",
"defaultValue": false,
"allowedValues": [
true,
false
],
"metadata": {
"description": "Should the VM be deployed as a low cost Spot Instance, this will deploy without attaching the Availability Set."
}
}
},
"variables": {
"AvailibilitySet": {
"On":{
"id":"[resourceId('Microsoft.Compute/availabilitySets/',variables('ASName'))]"
},
"Off":"[json('null')]"
}
}
then used this logic on the availability set assignment:
"availabilitySet": "[if(equals(parameters('SpotInstance'),bool('true')),variables('AvailibilitySet').Off,variables('AvailibilitySet').On)]",
if you do not define id as a property of "AvailabilitySet" this can be null, if you Do however it cannot be null.
When i try the same for loadbalancer backend pool i get errors:
12:21:49 - [ERROR] "message": "Cannot deserialize the current JSON object (e.g.
12:21:49 - [ERROR] {\"name\":\"value\"}) into type 'Microsoft.WindowsAzure.Networking.Nrp.Frontend
12:21:49 - [ERROR] .Contract.Csm.Public.ResourceReferenceHashSet`2[Microsoft.WindowsAzure.Networki
12:21:49 - [ERROR] ng.Nrp.Frontend.Contract.Csm.Public.LoadBalancerBackendAddressPool,Microsoft.Wi
12:21:49 - [ERROR] ndowsAzure.Networking.Nrp.Frontend.Contract.Csm.Public.NetworkInterfaceIpConfig
12:21:49 - [ERROR] uration]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize
12:21:49 - [ERROR] correctly.\r\nTo fix this error either change the JSON to a JSON array (e.g.
12:21:49 - [ERROR] [1,2,3]) or change the deserialized type so that it is a normal .NET type
12:21:49 - [ERROR] (e.g. not a primitive type like integer, not a collection type like an array
12:21:49 - [ERROR] or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute
12:21:49 - [ERROR] can also be added to the type to force it to deserialize from a JSON
variables
"availabilitySet": {
"id": "[resourceId('Microsoft.Compute/availabilitySets',parameters('availabilitySetName'))]"
},
"loadBalancerBackendAddressPools": {
"id": "[resourceId('Microsoft.Network/loadBalancers',parameters('LoadBalancername'))]"
},
"loadBalancerBackendAddressPools": "[if(equals(parameters('DeployavailabilitySet'),'yes'), variables('loadBalancerBackendAddressPools'), json('null'))]",

Resources