Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'scope' is not working correctly when directly set conditionally #7367

Closed
satano opened this issue Jun 27, 2022 · 15 comments · Fixed by #8300
Closed

'scope' is not working correctly when directly set conditionally #7367

satano opened this issue Jun 27, 2022 · 15 comments · Fixed by #8300
Assignees
Labels
bug Something isn't working intermediate language Related to the intermediate language investigate Quality sprint: No
Milestone

Comments

@satano
Copy link

satano commented Jun 27, 2022

Bicep version

Bicep CLI version 0.7.4 (5afc312467)

Describe the bug

I want to get existing resource which is in different resource group, than the deployment itself. When I use scope conditionally, it is not working. It returns resource ID with deployment resource group name, not the othe one I want.

To Reproduce

Let's have a simple Bicep template, where I try to get info about some resource.

@description('App service plan resource group name. Can be empty if app service plan is in the same resource group as the service itself.')
param appServicePlanResourceGroupName string = ''

var appServicePlanName = 'test-svcplan'

// This is not working.
resource servicePlanWithCondition 'Microsoft.Web/serverfarms@2021-03-01' existing = {
  name: appServicePlanName
  scope: empty(appServicePlanResourceGroupName) ? resourceGroup() : resourceGroup(appServicePlanResourceGroupName)
}

// Using local variable works.
var _appServicePlanResourceGroupName = empty(appServicePlanResourceGroupName) ? resourceGroup().name : appServicePlanResourceGroupName

resource servicePlanWithoutCondition 'Microsoft.Web/serverfarms@2021-03-01' existing = {
  name: appServicePlanName
  scope: resourceGroup(_appServicePlanResourceGroupName)
}

output idScopeWithCondition string = servicePlanWithCondition.id
output idScopeWithoutCondition string = servicePlanWithoutCondition.id

I deploy this template: az deployment group create --resource-group rsg2 --template-file .\conditional-scope.bicep --parameters appServicePlanResourceGroupName=rsg1

So deployment goes to resource group rsg2, but wanted service plan is in resource group rsg1. The outputs of this deployment are:

"outputs": {
    "idScopeWithCondition": {
        "type": "String",
        "value": "/subscriptions/e321f168-8250-4ebb-b970-5b9f6a8f1847/resourceGroups/rsg2/providers/Microsoft.Web/serverfarms/test-svcplan"
    },
    "idScopeWithoutCondition": {
        "type": "String",
        "value": "/subscriptions/e321f168-8250-4ebb-b970-5b9f6a8f1847/resourceGroups/rsg1/providers/Microsoft.Web/serverfarms/test-svcplan"
    }
},

So basically, in both cases, I ask twice for the same resource, but as you can see, I get different results.

  • If scope is set directly with condition, the returned resource ID is incorrect – contains rsg2.
  • If I create helper local variable with correct resource group name (condition is there) and scope is without condition, it works OK.

Additional context

Furthermore, I recommend to improve error message, when the resource is not found. When I created my Bicep template, I used conditional scope definition, which is not working. In some other resource, I wanted to use that resource ID. It failed with error message:

Cannot find ServerFarm with name test-svcplan.

This message is not sufficient, because that service plan clearly existed. It would be really helpful to add full resource ID to the message which was not found, not just the name. I spent quite a lot of time trying to find out what's going on, because I did not know, that the problem was in resource group name in the resource ID.

@ghost ghost added the Needs: Triage 🔍 label Jun 27, 2022
@alex-frankel
Copy link
Collaborator

@davidcho23 -- is this related to the recent fix you merged recently (but is not yet released)?

@davidcho23
Copy link
Contributor

No, my recent change was blocking ternary operators being used to set the parent property: #7254. If my fix was the cause, there should at least be an error message and not allow the bicep file to build.

Interestingly, this bug is related to to the general lack of support we currently have for ternary operators. The Bicep team is currently investigating how to implement a holistic fix for support and validation of ternary expressions and other types of expressions.

@anthony-c-martin
Copy link
Member

@davidcho23 - I think Alex might be wondering if we need a similar fix for scope as the fix you implemented for parent. It definitely feels to me like potentially a similar scenario we need to block (until we've got proper support)

@davidcho23
Copy link
Contributor

@anthony-c-martin Ah, sorry I misunderstood. Then yes, we should implement blocking using ternary operators for scope

@alex-frankel alex-frankel added bug Something isn't working and removed Needs: Triage 🔍 labels Jul 5, 2022
@alex-frankel alex-frankel added this to the v0.9 milestone Jul 5, 2022
@alex-frankel
Copy link
Collaborator

FYI @davidcho23, I added this optimistically to the 0.9 milestone and assigned you to it you have some free cycles. No worries if it needs to be pushed out further.

@sopelt
Copy link

sopelt commented Aug 26, 2022

This looks similar to #1876 ... and we have seen another probably related scenario: Conditional scope where the result should be a resource scope (and not a resource group):

param keyVaultName string
param principalId string
param roleDefinitionId string
param secretName string = ''

var hasSecret = secretName != null && secretName != ''

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName
  resource secret 'secrets' existing = if (hasSecret) {
    name: secretName
  }
}

resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
  scope: subscription()
  name: roleDefinitionId
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: hasSecret ? keyVault::secret : keyVault
  name: hasSecret ? guid(resourceGroup().id, keyVaultName, secretName, principalId, roleDefinition.id) : guid(resourceGroup().id, keyVaultName, principalId, roleDefinition.id)
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: principalId
    principalType: 'ServicePrincipal'
  }
}

When compiling to ARM the scope of the role assignment is fully missing without any warning/error (bicep cli 0.9.1, result below).

Is that covered by the ongoing work @alex-frankel / @davidcho23 ?

Thanks!

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.9.1.41621",
      "templateHash": "4150038258483440815"
    }
  },
  "parameters": {
    "keyVaultName": {
      "type": "string"
    },
    "principalId": {
      "type": "string"
    },
    "roleDefinitionId": {
      "type": "string"
    },
    "secretName": {
      "type": "string",
      "defaultValue": ""
    }
  },
  "variables": {
    "hasSecret": "[and(not(equals(parameters('secretName'), null())), not(equals(parameters('secretName'), '')))]"
  },
  "resources": [
    {
      "type": "Microsoft.Authorization/roleAssignments",
      "apiVersion": "2022-04-01",
      "name": "[if(variables('hasSecret'), guid(resourceGroup().id, parameters('keyVaultName'), parameters('secretName'), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))), guid(resourceGroup().id, parameters('keyVaultName'), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))))]",
      "properties": {
        "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]",
        "principalId": "[parameters('principalId')]",
        "principalType": "ServicePrincipal"
      }
    }
  ]
}

@anthony-c-martin
Copy link
Member

We have to revert #8300 unfortunately as it broke some legitimate scenarios.

@rouke-broersma
Copy link

rouke-broersma commented Nov 4, 2022

This is quite annoying to figure out because it doesn't look like you're doing anything wrong.

I just hit this trying to create a module that can peer two vnets where the vnets might optionally be in different subscriptions from the original deployment, and each other:

@description('vnet information { id, name, subscriptionId, resourceGroupName }')
param vnet1 object
@description('vnet information { id, name, subscriptionId, resourceGroupName }')
param vnet2 object

var vnet1RG = contains(vnet1, 'subscriptionId') ? resourceGroup(vnet1.subscriptionId, vnet1.resourceGroupName) : resourceGroup(vnet1.resourceGroupName)
var vnet2RG = contains(vnet2, 'subscriptionId') ? resourceGroup(vnet2.subscriptionId, vnet2.resourceGroupName) : resourceGroup(vnet2.resourceGroupName)

results in this template:

    "variables": {
        "vnet1RG": "[if(contains(parameters('vnet1'), 'subscriptionId'), createObject(), createObject())]",
        "vnet2RG": "[if(contains(parameters('vnet2'), 'subscriptionId'), createObject(), createObject())]"
    },

and eventually results in 'resource x in resource group not found'

@BigRollTide
Copy link

BigRollTide commented Apr 21, 2023

I am running into this bug as well trying to apply roles to resources with a conditional on the scope, and then the role is applied at the RG instead. As it is and until it is fixed, this will require me creating a conditional resource deployment for every condition, instead of just a conditional on the scope.

Is this still on a schedule to get fixed? (Using 0.16.2)

@brwilkinson
Copy link
Collaborator

@BigRollTide

You might be able to use this syntax

@BigRollTide
Copy link

@brwilkinson

I am not trying to scope on resourceGroup, but on a resource. I already scoped the module to the correct RG.

This is what I want to do:

param resourceGrantedTo string //resourceId of resource that is being assigned roles to - scope of module is already at the RG of this resource
param resourcePrincipalId string //principalId that will have new roles
param roleDefinitionIds array //array of role definition IDs to be assigned

resource storageAccount_res 'Microsoft.Storage/storageAccounts@2022-09-01' existing = if (contains(resourceGrantedTo,'Microsoft.Storage/storageAccounts') && length(split(resourceGrantedTo,'/')) == 9) {
name: last(split(resourceGrantedTo,'/'))
}

resource keyVault_res 'Microsoft.KeyVault/vaults@2023-02-01' existing = if (contains(resourceGrantedTo,'Microsoft.KeyVault/vaults') && length(split(resourceGrantedTo,'/')) == 9) {
name: last(split(resourceGrantedTo,'/'))
}

resource roleAssignment_res 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (role, index) in roleDefinitionIds : {
name: guid('${resourcePrincipalId}-${resourceGrantedTo}-${role}')
scope: length(split(resourceGrantedTo,'/')) == 9 ? contains(resourceGrantedTo,'Microsoft.Storage/storageAccounts') ? storageAccount_res : contains(resourceGrantedTo,'Microsoft.KeyVault/vaults') ? keyVault_res : resourceGroup() : resourceGroup()
properties: {
principalId: resourcePrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions',role)
principalType: 'ServicePrincipal'
}
}]

However, the roleAssignment_res does not work as expected, and always scopes it at the resourceGroup. I have outputted the storageAccount_res and the keyVault_res when calling the module, and they return the output of that resource, so I know that is working, and should never get to that last condition for the scope. I tried putting subscription() as the last conditions to see if it was getting there, and it still just applied the roles at the resourceGroup level (i.e., the current scope of the module). My workaround is to create a different roleAssignment resource for each type of resource - like this:

resource roleAssignment_st_res 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (role, index) in (contains(resourceGrantedTo,'Microsoft.Storage/storageAccounts') && length(split(resourceGrantedTo,'/')) == 9 ?roleDefinitionIds : []) : {
name: guid('${resourcePrincipalId}-${resourceGrantedTo}-${role}')
scope: storageAccount_res
properties: {
principalId: resourcePrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions',role)
principalType: 'ServicePrincipal'
}
}]

resource roleAssignment_kv_res 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (role, index) in (contains(resourceGrantedTo,'Microsoft.KeyVault/vaults') && length(split(resourceGrantedTo,'/')) == 9 ? roleDefinitionIds : []) : {
name: guid('${resourcePrincipalId}-${resourceGrantedTo}-${role}')
scope: keyVault_res
properties: {
principalId: resourcePrincipalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions',role)
principalType: 'ServicePrincipal'
}
}]

I do not know of a different type of workaround if I am scoping at a resource like you can with a ternary within resourceGroup(). So, this module will get much, much longer than it needs to be when accounting for any resourceType and sub resourceType that we need to assign rights to.

@brwilkinson
Copy link
Collaborator

brwilkinson commented Apr 24, 2023

@BigRollTide I see, in that case we have a separate workaround.

Works on any scope:

image

https://github.com/brwilkinson/AzureDeploymentFramework/blob/dev/ADF/bicep/KV-KeyVault.bicep#L173

Just take the resourceId: KV.id

Example of usage:

module RBAC 'x.RBAC-ALL.bicep' = [for (role, index) in rolesInfo: {
  name: take(replace('dp-rbac-role-${KV.name}-${role.name}', '@', '_'), 64)
  params: {
    resourceId: KV.id
    Global: Global
    roleInfo: role
    Type: contains(role, 'Type') ? role.Type : 'lookup'
    deployment: Deployment
  }
}]

There was more discussion on this topic here:

@brwilkinson
Copy link
Collaborator

@BigRollTide
Adding this one other anomoly for fileshares, uses same workaround, solution might be more clear here.

@lucasfijen
Copy link

lucasfijen commented Dec 20, 2023

Aparently this issue is still open after 1.5 year. This just cost me half a day to figure out:)
I found a nice bulky but clear workaround.

What didn't work within a azure vnet resource:
scope: (!empty(vnetRG) ? resourceGroup(toUpper(vnetRG)) : resourceGroup())

What does work:
scope: resourceGroup((!empty(vnetRG) ? toUpper(vnetRG) : resourceGroup().name))

So aparently if the conditional returns a resourcegroup object it fails, but if you return a string with the name, it will succeed. So this way you retrieve the resource group name and then just return that in the else statement.

Would be nice to get this fixed!

@stephaniezyen stephaniezyen added intermediate language Related to the intermediate language Quality sprint: No labels Jul 9, 2024
@stephaniezyen
Copy link
Contributor

Closing this out to track in #1876

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working intermediate language Related to the intermediate language investigate Quality sprint: No
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants