PowerShell / PSArm

PSArm is a PowerShell module that provides a PowerShell-embedded domain-specific language (DSL) for Azure Resource Manager (ARM) templates
MIT License
78 stars 19 forks source link

Array usage is not PowerShell friendly #141

Open rdbartram opened 3 years ago

rdbartram commented 3 years ago

This syntax is not powershell friendly and should look more like the second example

# working but not powershell(y)
    resource "vnet1"-ApiVersion 2020-08-01 -Namespace Microsoft.Network -Type virtualNetworks {
        properties {
            addressSpace {
                "10.0.0.0/24", "10.1.0.0/24", "10.2.0.0/24" | % {
                    addressPrefixes $_
                }
            }
        }
    }

# doesn't work and should in my opinion
    resource "vnet2" -ApiVersion 2020-08-01 -Namespace Microsoft.Network -Type virtualNetworks {
        properties {
            addressSpace {
                    addressPrefixes @("10.0.0.0/24", "10.1.0.0/24", "10.2.0.0/24")
                    # even better would be this, it not obvious this is an assignment...i know its a function...just weird
                    addressPrefixes  = @("10.0.0.0/24", "10.1.0.0/24", "10.2.0.0/24")
            }
        }
    }
irwins commented 3 years ago

Same...

I can't seem to figure out how to properly format an array...

param(
    [Parameter(Mandatory)]
    [string] $KeyVaultName,

    [Parameter()]
    [ValidateSet('WestEurope', 'NorthEurope', 'WestUs2')]
    [string] $Location = 'WestEurope'
)

$azADGroup = Get-AzADGroup -DisplayNameStartsWith 'platform-kv-01_secrets*' |
ForEach-Object {
    if ($_.DisplayName.EndsWith('_r')) {
        $secrets = @("Get","List")
    }

    if ($_.DisplayName.EndsWith('_rw')) {
        $secrets = @("Get","List","Create", "Delete","Update","Purge")
    }

    if ($_.DisplayName.EndsWith('_f')) {
        $secrets = @("All")
    }

    [PSCustomObject]@{
        ObjectId = $_.id
        Secrets = $secrets
    }
}

Arm {
    Resource $KeyVaultName -Namespace 'Microsoft.KeyVault' -Type 'vaults' -apiVersion '2019-09-01' {
        properties {
            tenantId (subscription).tenantId
            sku {
                family 'A'
                name 'standard'
            }
            accessPolicies { $azADGroup[0] | Foreach-Object {
                    tenantId (subscription).tenantId
                    objectId $($_.ObjectId)
                    permissions { $_.Secrets | Foreach-Object {
                            secrets $($_)
                        }
                    }
                }
            }
        }
    }
}

I can only generate one $azAdGroup entry at a time...

The JSON file seems fine as far as I can tell for the one entry... I have three azAdGroups...

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "psarm",
      "version": "0.1.0.0",
      "psarm-psversion": "7.1.3",
      "templateHash": "13553475952233288655"
    }
  },
  "resources": [
    {
      "name": "keyvault",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2019-10-01",
      "properties": {
        "mode": "Incremental",
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "resources": [
            {
              "name": "platform-kv-01",
              "apiVersion": "2019-09-01",
              "type": "Microsoft.KeyVault/vaults",
              "location": "westeurope",
              "properties": {
                "tenantId": "[subscription().tenantId]",
                "sku": {
                  "family": "A",
                  "name": "standard"
                },
                "accessPolicies": [
                  {
                    "tenantId": "[subscription().tenantId]",
                    "objectId": "d427a0ec-ac40-48cd-b74d-56edd3f6945b",
                    "permissions": {
                      "keys": [
                        "Get",
                        "List"
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  ]
}

I haven't seen any AzureAd resource in bicep... Did notice that Terraform has one 😉 If I can use PowerShell to leverage getting the objectId... Just think of the possibilities... 💯

Ba4bes commented 3 years ago

I talked about this with @irwins and found the following syntax does work:

param(
    [Parameter(Mandatory)]
    [string] $KeyVaultName,

    [Parameter()]
    [ValidateSet('WestEurope', 'NorthEurope', 'WestUs2')]
    [string] $Location = 'WestEurope'
)

$azADGroup = Get-AzADGroup -DisplayNameStartsWith 'platform-kv-01_secrets*' |
ForEach-Object {
    if ($_.DisplayName.EndsWith('_r')) {
        $secrets = @("Get", "List")
    }

    if ($_.DisplayName.EndsWith('_rw')) {
        $secrets = @("Get", "List", "Create", "Delete", "Update", "Purge")
    }

    if ($_.DisplayName.EndsWith('_f')) {
        $secrets = @("All")
    }

    [PSCustomObject]@{
        ObjectId = $_.id
        Secrets  = $secrets
    }
}

Arm {
    Resource $KeyVaultName -Namespace 'Microsoft.KeyVault' -Type 'vaults' -apiVersion '2019-09-01' {
        properties {
            tenantId (subscription).tenantId
            sku {
                family 'A'
                name 'standard'
            }
            $azADGroup | Foreach-Object {
                accessPolicies {
                    tenantId (subscription).tenantId
                    objectId $($_.ObjectId)
                    permissions { $_.Secrets | Foreach-Object {
                            secrets $($_)
                        }
                    }
                }
            }
        }
    }
}

I do feel this is counter intuitive syntax and in my opinion the other suggestion by @rdbartram and Irwins original syntax makes a lot more sense..

kilasuit commented 3 years ago

It may seem counter intuitive but the end Access Policy in @irwins / @Ba4bes is definable only to a single objectID not an array of ObjectId's so you can only ever have it as you have above as per the Access Policy docs at https://docs.microsoft.com/en-us/azure/templates/microsoft.keyvault/vaults/accesspolicies?tabs=json#accesspolicyentry-object

Which ultimately is a limitation in ARM which @bmoore-msft / @alex-frankel may be able to help nudge the KV team to update the objectID of the AccessPolicyObject to allow an array in a future api-version update

Ba4bes commented 3 years ago

In the docs you linked I see AccessPolicyEntry, which is an object, and accessPolicies, which is an array. The PSARM syntax in this case looks like it is is creating multiple instances of the property accessPolicies. In my opinion it makes more sense if it is making multiple instances of AccessPolicyEntry and combine them into the array accessPolicies. A lot like what @irwins tried to do.

That is already happening in bicep, where the syntax to create a for loop would look something like this:

accessPolicies: [for accessPolicy in accessPolicies: {
      tenantId: tenantId
      objectId: accessPolicy.objectId
      permissions: {
        keys: accessPolicy.keys
        secrets: accessPolicy.secrets
        certificates: accessPolicy.certificates
      }
 }]

I would expect something similar to this to be an option for PSArm.

Same goes for the example by @rdbartram, where addressPrefixes is an array, not an object.

irwins commented 3 years ago

@Ba4bes Gave me an idea to try...

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "keyVault": {
            "value": {
                "name": "platform-kv-01",
                "sku": {
                    "family": "A",
                    "name": "standard"
                },
                "accessPolicies": [
                    {
                        "objectId": "92f011f6-697c-41a2-8584-defca729382f",
                        "permissions": {
                            "keys": ["All"],
                            "secrets": ["All"],
                            "certificates": ["All"]
                        }
                    },
                    {
                        "objectId": "d427a0ec-ac40-48cd-b74d-56edd3f6945f",
                        "permissions": {
                            "keys": ["Get","List"]
                        }
                    },
                    {
                        "objectId": "b6555562-e39c-407a-b846-9b3f1878df45",
                        "permissions": {
                            "keys": ["Get","List"],
                            "secrets": [ "All"],
                            "certificates": ["All"]
                        }
                    }
                ]
            }
        },
        "location": {
            "value": "westeurope"
        }
    }
}
// Parameter definitions
@description('KeyVault properties')
param keyVault object

@description('Resource location, defaults to resourceGroup location')
param location string = resourceGroup().location

// Resources
resource kv 'Microsoft.KeyVault/vaults@2021-04-01-preview' = {
  name: keyVault.name
  location:location
  properties: {
    tenantId: subscription().tenantId
    sku: keyVault.sku
    accessPolicies: [for accessPolicy in keyVault.accessPolicies: {
      tenantId: subscription().tenantId
      objectId: accessPolicy.objectId
      permissions: accessPolicy.permissions
    }]

  }
}

//Output definitions
output keyVaultUri string = kv.properties.vaultUri

Building this resulted in the following JSON file (Gotta love bicep for authoring...)

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.4.63.48766",
      "templateHash": "10484560537251782542"
    }
  },
  "parameters": {
    "keyVault": {
      "type": "object",
      "metadata": {
        "description": "KeyVault properties"
      }
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "metadata": {
        "description": "Resource location, defaults to resourceGroup location"
      }
    }
  },
  "functions": [],
  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2021-04-01-preview",
      "name": "[parameters('keyVault').name]",
      "location": "[parameters('location')]",
      "properties": {
        "copy": [
          {
            "name": "accessPolicies",
            "count": "[length(parameters('keyVault').accessPolicies)]",
            "input": {
              "tenantId": "[subscription().tenantId]",
              "objectId": "[parameters('keyVault').accessPolicies[copyIndex('accessPolicies')].objectId]",
              "permissions": {
                "keys": "[parameters('keyVault').accessPolicies[copyIndex('accessPolicies')].keys]",
                "secrets": "[parameters('keyVault').accessPolicies[copyIndex('accessPolicies')].secrets]",
                "certificates": "[parameters('keyVault').accessPolicies[copyIndex('accessPolicies')].certificates]"
              }
            }
          }
        ],
        "tenantId": "[subscription().tenantId]",
        "sku": "[parameters('keyVault').sku]"
      }
    }
  ],
  "outputs": {
    "keyVaultUri": {
      "type": "string",
      "value": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVault').name)).vaultUri]"
    }
  }
}

it worked as expected... image

I can always update the parameter file JIT with PowerShell 😉