2

I'm working on an Azure ARM template which will have a string parameter expecting a comma-separated list of email addresses. In the template I want to parse this and copy into an array of the object types required by the emailReceivers property of the Microsoft.Insights/ActionGroups resource type.

The input needs to be a single string because the value will be substituted by Octopus Deploy as part of our deployment pipeline.

The template I have works fine as long as at least one email address is supplied, but I want this value to be optional. Unfortunately when an empty string is supplied I get the following error:

The template 'copy' definition at line '0' and column '0' has an invalid copy count. The copy count must be postive integer value and cannot exceed '800'.

Clearly a zero-length array is not supported by these copy blocks so I'm wondering if anyone knows a workaround or cunning hack that will let me achieve what I want.

Here is a stripped down template example:

{
  "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "emailAddresses": {
      "type": "string",
      "defaultValue": "one@email.com, two@email.com",
      "metadata": {
        "description": "Comma-separated list of email recipients."
      }
    }
  },
  "variables": {
    "emailArray": "[if(equals(length(parameters('emailAddresses')), 0), json('[]'), split(parameters('emailAddresses'),','))]",
    "copy": [
      {
        "name": "emailReceivers",
        "count": "[length(variables('emailArray'))]",
        "input": {
            "name": "[concat('Email ', trim(variables('emailArray')[copyIndex('emailReceivers')]))]",
            "emailAddress": "[trim(variables('emailArray')[copyIndex('emailReceivers')])]"
        }
      }
    ]
  },
  "resources": [],
  "outputs": {
      "return": {
          "type": "array",
          "value": "[variables('emailReceivers')]"
      }
  }
}
Tom Troughton
  • 3,941
  • 2
  • 37
  • 77
  • I've spent some time on this - See here: [Is there a workaround to get an Azure ARM template copy block to accept a zero-length array?](https://stackoverflow.com/questions/45923848/can-i-have-an-arm-template-resource-with-a-copy-array-of-0-to-n) and have yet to come up with a solution that doesn't involve injecting entire JSON blocks. I use powershell to deploy, and flip between two templates. In your case, couldn't you use a default email instead of json('[]')? – Christopher G. Lewis Nov 27 '18 at 16:40
  • @ChristopherG.Lewis I _could_ but I was hoping to avoid this :) – Tom Troughton Nov 27 '18 at 17:13

2 Answers2

2

Well, not directly, but you could do this:

"copy": [
  {
    "name": "emailReceivers",
    "count": "[if(equals(length(variables('emailArray')), 0), 1, length(variables('emailArray')))]",
    "input": {
        "name": "[concat('Email ', trim(variables('emailArray')[copyIndex('emailReceivers')]))]",
        "emailAddress": "[trim(variables('emailArray')[copyIndex('emailReceivers')])]" << these need the same if to put placeholder value inside
    }
  }
]

and then,somewhere down the line you would implement a condition if length equals 0 bla-bla-bla

4c74356b41
  • 69,186
  • 6
  • 100
  • 141
  • The challenge with this is that even with a `"count":0`, the input.Name value is calculated at least once from `variables('emailArray')[copyIndex('emailReceivers')]` which calc's out to `variables('emailArray')[0]` which of course throws the error – Christopher G. Lewis Nov 27 '18 at 16:56
  • `{ "error": { "code": "InvalidTemplate", "message": "Deployment template language expression evaluation failed: 'The language expression property array index '0' is out of bounds.'. Please see https://aka.ms/arm-template-expressions for usage details." } }` Tested it - slightly different error then above: `"message": "Deployment template validation failed: 'The template 'copy' definition at line '0' and column '0' has an invalid copy count. The copy co unt must be a postive integer value and cannot exceed '800'. Please see https://aka.ms/arm-copy for usage details.'."` – Christopher G. Lewis Nov 27 '18 at 17:07
  • Yes, I'd already tried this. `variables('emailArray')[copyIndex('emailReceivers')]` throws the error when you've forced the array length to be 1. Maybe I too am missing something from your answer @4c74356b41, could you elaborate? – Tom Troughton Nov 27 '18 at 17:16
  • ... do you not see the comment on the properties? just apply the same logic to the property, put fake value on the property with an if statement (exact if statement from `count`). then you can use logic to determine if fake value is in the property and ignore that value\not deploy resource – 4c74356b41 Nov 27 '18 at 17:32
  • The problem is that with the IF statement in ARM, both TRUE and FALSE are *always* evaluated. So if you had `[if(equals(length(variables('emailArray')), 0),json('[]'),variables('emailArray')[copyIndex('emailReceivers')])]`, the False condition would be `variables('emailArray')[0]` which is not allowed. – Christopher G. Lewis Nov 27 '18 at 18:25
  • no, thats not true, they fixes it couple of months ago, it works now. you just claimed you tried it... @ChristopherG.Lewis – 4c74356b41 Nov 27 '18 at 18:35
  • hmm, you didn't expand each template item: `"name": "emailReceivers", "count": "[if(equals(length(variables('emailArray')), 0), 1, length(variables('emailArray')))]", "input": { "name": "[concat('Email ', if(equals(length(variables('emailArray')), 0),'',variables('emailArray')[copyIndex('emailReceivers')]) )]", "emailAddress": "[if(equals(length(variables('emailArray')), 0),'',variables('emailArray')[copyIndex('emailReceivers')])]" } ` Actually works – Christopher G. Lewis Nov 27 '18 at 19:06
  • yeah, i said in the answer that you need to do it, to save typing. did you read the answer? you know you can actually upvote helpful answers on this site ;) – 4c74356b41 Nov 27 '18 at 19:17
  • 1
    @ChristopherG.Lewis - we fixed the behavior of the if() function a few months back so only the side that's used is evaluated. – bmoore-msft Nov 30 '18 at 18:58
  • 3
    Oh, and we're working on "copy count of zero" fixes, but not done yet... – bmoore-msft Nov 30 '18 at 18:59
  • @bmoore-msft - That's great that you fixed the IF - I've now got working templates for that. Will the copy count zero fix work with both Resource Iterations and Property Iterations? (https://learn.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-multiple) – Christopher G. Lewis Nov 30 '18 at 19:06
  • property copy for sure - we're working through some challenges for resource copy but I think we'll be able to resolve them - so the goal is "both" – bmoore-msft Dec 04 '18 at 20:43
  • @bmoore-msft are the "copy count of zero" fixes complete? – Joe Eng Jun 05 '19 at 17:58
  • 4
    It's in prod - you need to use apiVersion 2019-05-10 to use it. PowerShell/CLI/SDKs have not yet been updated with that version. But if you call REST directly it will work. – bmoore-msft Jun 07 '19 at 22:29
0

Expanding on what 4c74356b41 posted first - this ARM template will work with zero or more emails:

{
  "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "emailAddresses": {
      "type": "string",
      "defaultValue": "one@email.com, two@email.com",
      "metadata": {
        "description": "Comma-separated list of email recipients."
      }
    }
  },
  "variables": {
    "emailArray": "[if(equals(length(parameters('emailAddresses')), 0), json('[]'), split(parameters('emailAddresses'),','))]",
    "copy": [
      {
        "name": "emailReceivers",
        "count": "[if(equals(length(variables('emailArray')), 0), 1, length(variables('emailArray')))]",
        "input": {
            "name": "[concat('Email ', if(equals(length(variables('emailArray')), 0),'',variables('emailArray')[copyIndex('emailReceivers')])     )]",
            "emailAddress": "[if(equals(length(variables('emailArray')), 0),'',variables('emailArray')[copyIndex('emailReceivers')])]" 
        }
      }

    ]
  },
  "resources": [],
  "outputs": {
      "return": {
          "type": "array",
          "value": "[variables('emailReceivers')]"
      }
  }
}

Using a parameter file either like this:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "emailAddresses": {
            "value": "testEmail@mail.com"
        }
    }
}

or this

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "emailAddresses": {
            "value": ""
        }
    }
}

Accept the answer from 4c74356b41 - this will just have better formatting than the comments.

Note - In my testing, it appears that this doesn't work with Resources:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "DataDisks": {
            "type": "array",
            "metadata": {
                "description": "Combined DataDisk and DataDiskResource array"
            },
            "defaultValue": [
                {
                    "lun": 0,
                    "createOption": "attach",
                    "caching": "None",
                    "writeAcceleratorEnabled": false,
                    "id": null,
                    "name": "cgltest01_DataDisk_0",
                    "storageAccountType": null,
                    "diskSizeGB": 31,
                    "sku": "StandardSSD_LRS",
                    "creationData": {
                        "createOption": "empty"
                    }
                }
            ]
        }
    },
    "variables": {
        "Disks": "[parameters('DataDisks')]"
    },
    "resources": [
        {
            "name": "[if(equals(length(parameters('DataDisks')),0) ,'BogusName', parameters('DataDisks')[copyIndex()].name)]",
            "type": "Microsoft.Compute/disks",
            "condition": "[greater(length(parameters('DataDisks')), 0)]",
            "apiVersion": "2018-06-01",
            "location": "centralus",
            "properties": {
                "diskSizeGB": "[if(equals(length(parameters('DataDisks')),0) ,'123',   parameters('DataDisks')[copyIndex()].diskSizeGB)]",
                "creationData": "[if(equals(length(parameters('DataDisks')),0) , json('null'), parameters('DataDisks')[copyIndex()].creationData)]"
            },
            "sku": {
                "name": "[parameters('DataDisks')[copyIndex()].sku]"
            },
            "copy": {
                "name": "managedDiskResources",
                "count": "[if(equals(length(parameters('DataDisks')),0), 1 , length(parameters('DataDisks')))]"
            }
        }
    ],
    "outputs": {
        "return": {
            "type": "array",
            "value": "[variables('Disks')]"
        }
    }
}

Fails with the same out of bounds array error:

Code    : InvalidTemplate
Message : Deployment template validation failed: 'The template resource 'BogusName' at line '34' and column '9' is not valid: The language expression 
          property array index '0' is out of bounds.. Please see https://aka.ms/arm-template-expressions for usage details.'.
Details : 
Christopher G. Lewis
  • 4,777
  • 1
  • 27
  • 46
  • just start a new question about this one, i can fix it – 4c74356b41 Nov 27 '18 at 20:09
  • OK, so I paste your exact ARM template into a JSON file, change the `defaultValue` of the `emailAddresses` parameter to `""`, run an Azure deployment, and the output variable named `return` in the output is this: `[{ "name": "Email ", "emailAddress": "" }]` which obviously is not an empty array. Given you and @4c74356b41 are adamant this works I really don't get what I'm missing? – Tom Troughton Nov 28 '18 at 09:51
  • why do you need an empty array? just update output to return empty array if length equals zero – 4c74356b41 Nov 28 '18 at 11:19
  • this is more or less what you have to do, because it should have at least 1 iteration and if it does have 1 iteration with 0 length source array, you should substitute this fake array with something of your liking (i assume, an empty array in your case) – 4c74356b41 Nov 28 '18 at 11:26