Merge "Azure: add support for User-assigned Managed Identities"

This commit is contained in:
Zuul
2025-12-03 15:59:49 +00:00
committed by Gerrit Code Review
6 changed files with 80 additions and 0 deletions

View File

@@ -749,6 +749,33 @@ section of the configuration.
If given, the size of the operating system disk, in GiB. If given, the size of the operating system disk, in GiB.
.. attr:: user-assigned-identities
:type: dict
:default: None
`User-assigned Managed Identities`_ to assign to the VM.
Useful for giving access to services without needing any secrets.
.. attr:: name
:required:
:type: str
The name of the User-assigned Managed Identity.
.. attr:: resource-group
:type: str
:default: The provider's resource group
Overrides :attr:`providers.[azure].resource-group`.
For example:
.. code-block:: yaml
user-assigned-identities:
- name: myLocalIdentity
- name: myRemoteIdentity
resource-group: remote-rg
.. _`Azure CLI`: https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest .. _`Azure CLI`: https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest
@@ -757,3 +784,5 @@ section of the configuration.
.. _`Azure User Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/user-data .. _`Azure User Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/user-data
.. _`Azure Custom Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/custom-data .. _`Azure Custom Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/custom-data
.. _`User-assigned Managed Identities`: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-rest-vm

View File

@@ -26,6 +26,7 @@ from nodepool.driver.utils import (
RateLimiter, RateLimiter,
ImageUploader, ImageUploader,
) )
from urllib.parse import urlparse
from nodepool.driver import statemachine from nodepool.driver import statemachine
from nodepool import exceptions from nodepool import exceptions
from . import azul from . import azul
@@ -677,6 +678,23 @@ class AzureAdapter(statemachine.Adapter):
if label.user_data: if label.user_data:
spec['properties']['userData'] = label.user_data spec['properties']['userData'] = label.user_data
# build resource id for all configured User-assigned Identities
uai_resource_ids = set()
for uai in label.user_assigned_identities:
uai_rg_name = uai.get("resource-group", self.resource_group)
uai_url = self.azul.managed_identities.url(
resourceGroupName=uai_rg_name, resourceName=uai['name'])
uai_resource_ids.add(urlparse(uai_url).path)
# adding empty userAssignedIdentities is not allowed by Azure
if uai_resource_ids:
spec['identity'] = {
'type': 'UserAssigned',
'userAssignedIdentities': {
rid: {} for rid in uai_resource_ids
},
}
with self.rate_limiter: with self.rate_limiter:
self.log.debug(f"Creating VM {hostname}") self.log.debug(f"Creating VM {hostname}")
return self.azul.virtual_machines.create( return self.azul.virtual_machines.create(

View File

@@ -268,6 +268,11 @@ class AzureCloud:
providerId='Microsoft.Compute', providerId='Microsoft.Compute',
resource='skus', resource='skus',
apiVersion='2019-04-01') apiVersion='2019-04-01')
self.managed_identities = AzureResourceProviderCRUD(
self,
providerId='Microsoft.ManagedIdentity',
resource='userAssignedIdentities',
apiVersion='2023-01-31')
def get(self, url, codes=[200]): def get(self, url, codes=[200]):
return self.request('GET', url, None, codes) return self.request('GET', url, None, codes)

View File

@@ -193,6 +193,8 @@ class AzureLabel(ConfigValue):
self.custom_data = self._encodeData(label.get('custom-data', None)) self.custom_data = self._encodeData(label.get('custom-data', None))
self.host_key_checking = self.pool.host_key_checking self.host_key_checking = self.pool.host_key_checking
self.volume_size = label.get('volume-size') self.volume_size = label.get('volume-size')
self.user_assigned_identities = label.get(
'user-assigned-identities', [])
def _encodeData(self, s): def _encodeData(self, s):
if not s: if not s:
@@ -205,6 +207,11 @@ class AzureLabel(ConfigValue):
v.Required('vm-size'): str, v.Required('vm-size'): str,
} }
user_assigned_identities = {
v.Required('name'): str,
'resource-group': str,
}
return { return {
v.Required('name'): str, v.Required('name'): str,
'cloud-image': str, 'cloud-image': str,
@@ -215,6 +222,7 @@ class AzureLabel(ConfigValue):
'user-data': str, 'user-data': str,
'custom-data': str, 'custom-data': str,
'volume-size': int, 'volume-size': int,
'user-assigned-identities': [user_assigned_identities],
} }

View File

@@ -93,6 +93,10 @@ providers:
dynamic-tenant: "Tenant is {{request.tenant_name}}" dynamic-tenant: "Tenant is {{request.tenant_name}}"
user-data: "This is the user data" user-data: "This is the user data"
custom-data: "This is the custom data" custom-data: "This is the custom data"
user-assigned-identities:
- name: localid
- name: otherid
resource-group: othergroup
- name: image-by-name - name: image-by-name
cloud-image: image-by-name cloud-image: image-by-name
hardware-profile: hardware-profile:

View File

@@ -99,6 +99,22 @@ class TestDriverAzure(tests.DBTestCase):
self.fake_azure.crud['Microsoft.Compute/virtualMachines']. self.fake_azure.crud['Microsoft.Compute/virtualMachines'].
requests[0]['properties']['userData'], requests[0]['properties']['userData'],
'VGhpcyBpcyB0aGUgdXNlciBkYXRh') # This is the user data 'VGhpcyBpcyB0aGUgdXNlciBkYXRh') # This is the user data
self.assertEqual(
self.fake_azure.crud['Microsoft.Compute/virtualMachines'].
requests[0]['identity'],
{
"type": "UserAssigned",
"userAssignedIdentities": {
f"/subscriptions/{self.fake_azure.subscription_id}/"
"resourceGroups/nodepool/"
"providers/Microsoft.ManagedIdentity/"
"userAssignedIdentities/localid": {},
f"/subscriptions/{self.fake_azure.subscription_id}/"
"resourceGroups/othergroup/"
"providers/Microsoft.ManagedIdentity/"
"userAssignedIdentities/otherid": {},
}
})
tags = (self.fake_azure.crud['Microsoft.Compute/virtualMachines']. tags = (self.fake_azure.crud['Microsoft.Compute/virtualMachines'].
requests[0]['tags']) requests[0]['tags'])
self.assertEqual(tags.get('team'), 'DevOps') self.assertEqual(tags.get('team'), 'DevOps')