David Gard
David Gard

Reputation: 12047

ARM resource iteration failing with an empty array

It seems that all expressions in an ARM template are evaluated up-front, so even if you have condition false for a resource, expressions within that resource are evaluated.

This appears to be the case regardless of whether condition is explicitly set to false or if it's an expression that evaluates to false.

This behavior is problematic in the case resource iteration, because expressions within the resource may refer to the copyIndex() of a parameter or variable. However, that parameter of variable is an empty array, evaluation of those expressions will fail with a message similar to the following -

The language expression property array index '0' is out of bounds.. Please see https://aka.ms/arm-template-expressions for usage details.

Is there any way I can amend my template to work regardless of whether or not there are any resources to add?

Example template

Note that I've already had to 'hack' the count parameter of the copy object. If it's 0 (as the expression length(variables('productsJArray')) may evaluation to), template validation fails with the following error -

The copy count must be a postive integer value and cannot exceed '800'.

I feel that it's a reasonable expectation that if count is 0, then no resource is added - but that's a whole different subject!

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "apiManagementServiceName": {
            "type": "string",
            "metadata": {
                "description": "The name of the API Management instance."
            }
        },
        "productsJson": {
            "type": "string",
            "metadata": {
                "description": "A JSON representation of the Products to add."
            }
        }
    },
    "variables": {
        "productsJArray": "[json(parameters('productsJson'))]"
    },
    "resources": [
        {
            "condition": "[greater(length(variables('productsJArray')), 0)]",
            "type": "Microsoft.ApiManagement/service/products",
            "name": "[concat(parameters('apiManagementServiceName'), '/', variables('productsJArray')[copyIndex()].name)]",
            "apiVersion": "2018-06-01-preview",
            "properties": {
                "displayName": "[variables('productsJArray')[copyIndex()].displayName]",
                "description": "[variables('productsJArray')[copyIndex()].description]",
                "state": "[variables('productsJArray')[copyIndex()].state]",
                "subscriptionRequired": "[variables('productsJArray')[copyIndex()].subscriptionRequired]",
                "approvalRequired": "[variables('productsJArray')[copyIndex()].approvalRequired]"
            },
            "copy": {
                "name": "productscopy",
                "count": "[if(greater(length(variables('productsJArray')), 0), length(variables('productsJArray')), 1)]"
            }
        }
    ]
}

Example parameters file that will work

Note that while this may seem fine, there may be instances in which the productsJson parameter value is any empty array, [], and this is where my issue has arisen.

{
        "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
                "apiManagementServiceName": {
                        "value": "my-api-management"
                },
                "productsJson": {
                        "value": "[{\"name\":\"my-product\",\"displayName\":\"My Product\",\"description\":\"My product is awesome.\",\"state\":\"published\",\"subscriptionRequired\":true,\"approvalRequired\":false}]"
                }
        }
}

Example parameters file that will fail

{
        "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
                "apiManagementServiceName": {
                        "value": "lmk-bvt-conveyorbot"
                },
                "productsJson": {
                        "value": "[]"
                }
        }
}

Updated template

Derived from one of the suggestions by user '4c74356b41'.

This template is generating the following error -

Unable to process template language expressions for resource '/subscriptions/**********/resourceGroups/**********/providers/Microsoft.Resources/deployments/api-management-products' at line '1' and column '839'. 'The template function 'copyIndex' is not expected at this location. The function can only be used in a resource with copy specified.

{
        "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
                "apiManagementServiceName": {
                        "type": "string",
                        "metadata": {
                                "description": "The name of the API Management instance."
                        }
                },
                "productsJson": {
                        "type": "string",
                        "metadata": {
                                "description": "A JSON representation of the Products to add."
                        }
                }
        },
        "variables": {
                "productsJArray": "[json(parameters('productsJson'))]"
        },
        "resources": [
                {
                        "condition": "[greater(length(variables('productsJArray')), 0)]",
                        "type": "Microsoft.Resources/deployments",
                        "name": "api-management-products",
                        "apiVersion": "2017-05-10",
                        "properties": {
                                "mode": "Incremental",
                                "template": {
                                        "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
                                        "contentVersion": "1.0.0.0",
                                        "resources": [
                                                {
                                                        "type": "Microsoft.ApiManagement/service/products",
                                                        "name": "[concat(parameters('apiManagementServiceName'), '/', variables('productsJArray')[copyIndex('productscopy')].name)]",
                                                        "apiVersion": "2018-06-01-preview",
                                                        "properties": {
                                                                "displayName": "[variables('productsJArray')[copyIndex('productscopy')].displayName]",
                                                                "description": "[variables('productsJArray')[copyIndex('productscopy')].description]",
                                                                "state": "[variables('productsJArray')[copyIndex('productscopy')].state]",
                                                                "subscriptionRequired": "[variables('productsJArray')[copyIndex('productscopy')].subscriptionRequired]",
                                                                "approvalRequired": "[variables('productsJArray')[copyIndex('productscopy')].approvalRequired]"
                                                        },
                                                        "copy": {
                                                                "name": "productscopy",
                                                                "count": "[if(greater(length(variables('productsJArray')), 0), length(variables('productsJArray')), 1)]"
                                                        }
                                                }
                                        ]
                                }
                        }
                }
        ]
}

Upvotes: 5

Views: 9322

Answers (3)

user2309432
user2309432

Reputation: 11

You can also have a placeholder element in the empty array and use condition to ignore the placeholder element. like this

{
"parameters": {
    "yourArray": {
        "defaultValue": [
            {
                "name": "placeholder"
            }
        ],
        "type": "array"
    }
},
"resources": [
    {
        "condition": "[not(equals(parameters('yourArray')[copyIndex()].name,'placeholder'))]",
        "copy": {
            "name": "yourArray",
            "count": "[length(parameters('yourArray'))]"
        }
    }
]
}

Upvotes: 1

David Gard
David Gard

Reputation: 12047

Here is the final template that I will use. This allows me to pass either an array of objects or an empty array to the productsJson parameter.

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "apiManagementServiceName": {
            "type": "string",
            "metadata": {
                "description": "The name of the API Management instance."
            }
        },
        "productsJson": {
            "type": "string",
            "metadata": {
                "description": "A JSON representation of the Products to add."
            }
        }
    },
    "variables": {
        "productsJArray": "[json(parameters('productsJson'))]"
    },
    "resources": [
        {
            "condition": "[greater(length(variables('productsJArray')), 0)]",
            "type": "Microsoft.Resources/deployments",
            "name": "[concat('api-management-products-', copyIndex())]",
            "apiVersion": "2017-05-10",
            "copy": {
                "name": "productscopy",
                "count": "[if(greater(length(variables('productsJArray')), 0), length(variables('productsJArray')), 1)]",
                "mode": "Serial"
            },
            "properties": {
                "mode": "Incremental",
                "template": {
                    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "resources": [
                        {
                            "type": "Microsoft.ApiManagement/service/products",
                            "name": "[concat(parameters('apiManagementServiceName'), '/', variables('productsJArray')[copyIndex()].name)]",
                            "apiVersion": "2018-06-01-preview",
                            "properties": {
                                "displayName": "[variables('productsJArray')[copyIndex()].displayName]",
                                "description": "[variables('productsJArray')[copyIndex()].description]",
                                "state": "[variables('productsJArray')[copyIndex()].state]",
                                "subscriptionRequired": "[variables('productsJArray')[copyIndex()].subscriptionRequired]",
                                "approvalRequired": "[variables('productsJArray')[copyIndex()].approvalRequired]"
                            }
                        }
                    ]
                }
            }
        }
    ]
}

Upvotes: 2

4c74356b41
4c74356b41

Reputation: 72171

two ways to work around this:

  1. if() hack, so if length == 0, length = 1. Using this approach, you need to have a proxy object you will reference, as referencing a non existing object wont work.
  2. Use nested deployments, you can put a condition on a deployment so it doesnt start if length == 0 (and you run your loop inside the nested deployment). this way you dont care about length of zero.

Upvotes: 4

Related Questions