diff --git a/avm/res/virtual-machine-images/image-template/README.md b/avm/res/virtual-machine-images/image-template/README.md index 9ad8c63aa0..c411e4fe7d 100644 --- a/avm/res/virtual-machine-images/image-template/README.md +++ b/avm/res/virtual-machine-images/image-template/README.md @@ -18,7 +18,7 @@ This module deploys a Virtual Machine Image Template that can be consumed by Azu | :-- | :-- | | `Microsoft.Authorization/locks` | [2020-05-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-05-01/locks) | | `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | -| `Microsoft.VirtualMachineImages/imageTemplates` | [2023-07-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.VirtualMachineImages/2023-07-01/imageTemplates) | +| `Microsoft.VirtualMachineImages/imageTemplates` | [2024-02-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.VirtualMachineImages/2024-02-01/imageTemplates) | ## Usage examples @@ -202,6 +202,7 @@ module imageTemplate 'br/public:avm/res/virtual-machine-images/image-template:' lock: { kind: 'CanNotDelete' name: 'myCustomLockName' } + managedResourceTags: { + testKey1: 'testValue1' + testKey2: 'testValue2' + } optimizeVmBoot: 'Enabled' osDiskSizeGB: 127 roleAssignments: [ @@ -330,6 +337,9 @@ module imageTemplate 'br/public:avm/res/virtual-machine-images/image-template:" }, @@ -364,6 +380,12 @@ module imageTemplate 'br/public:avm/res/virtual-machine-images/image-template:If this field is empty, a resource group with a random name will be created.

If the resource group specified in this field doesn\'t exist, it will be created with the same name.

If the resource group specified exists, it must be empty and in the same region as the image template.

The resource group created will be deleted during template deletion if this field is empty or the resource group specified doesn\'t exist,

but if the resource group specified exists the resources created in the resource group will be deleted during template deletion and the resource group itself will remain.') param stagingResourceGroupResourceId string? -import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' @description('Optional. The lock settings of the service.') param lock lockType? @@ -43,7 +43,7 @@ param baseTime string = utcNow('yyyy-MM-dd-HH-mm-ss') @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' @description('Optional. Array of role assignments to create.') param roleAssignments roleAssignmentType[]? @@ -53,7 +53,7 @@ param distributions distributionType[] @description('Optional. List of User-Assigned Identities associated to the Build VM for accessing Azure resources such as Key Vaults from your customizer scripts. Be aware, the user assigned identities specified in the \'managedIdentities\' parameter must have the \'Managed Identity Operator\' role assignment on all the user assigned identities specified in this parameter for Azure Image Builder to be able to associate them to the build VM.') param vmUserAssignedIdentities array = [] -import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' @description('Required. The managed identity definition for this resource.') param managedIdentities managedIdentityOnlyUserAssignedType @@ -67,6 +67,30 @@ param validationProcess validationProcessType? @description('Optional. The optimize property can be enabled while creating a VM image and allows VM optimization to improve image creation time.') param optimizeVmBoot string? +@allowed([ + 'Enabled' + 'Disabled' +]) +@description('Optional. Indicates whether or not to automatically run the image template build on template creation or update.') +param autoRunState string = 'Disabled' + +@allowed([ + 'cleanup' + 'abort' +]) +@description('Optional. If there is a customizer error and this field is set to \'cleanup\', the build VM and associated network resources will be cleaned up. This is the default behavior. If there is a customizer error and this field is set to \'abort\', the build VM will be preserved.') +param errorHandlingOnCustomizerError string = 'cleanup' + +@allowed([ + 'cleanup' + 'abort' +]) +@description('Optional. If there is a validation error and this field is set to \'cleanup\', the build VM and associated network resources will be cleaned up. If there is a validation error and this field is set to \'abort\', the build VM will be preserved. This is the default behavior.') +param errorHandlingOnValidationError string = 'cleanup' + +@description('Optional. Tags that will be applied to the resource group and/or resources created by the service.') +param managedResourceTags object? + var identity = { type: 'UserAssigned' userAssignedIdentities: reduce( @@ -120,7 +144,7 @@ resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableT } } -resource imageTemplate 'Microsoft.VirtualMachineImages/imageTemplates@2023-07-01' = { +resource imageTemplate 'Microsoft.VirtualMachineImages/imageTemplates@2024-02-01' = { #disable-next-line use-stable-resource-identifiers // Disabling as ImageTemplates are not idempotent and hence always must have new name name: '${name}-${baseTime}' location: location @@ -139,51 +163,51 @@ resource imageTemplate 'Microsoft.VirtualMachineImages/imageTemplates@2023-07-01 : null } source: imageSource - customize: customizationSteps + ...(!empty(customizationSteps) + ? { + customize: customizationSteps + } + : {}) stagingResourceGroup: stagingResourceGroupResourceId - distribute: [ - for distribution in distributions: union( - { - type: distribution.type - artifactTags: distribution.?artifactTags ?? { - sourceType: imageSource.type - sourcePublisher: imageSource.?publisher - sourceOffer: imageSource.?offer - sourceSku: imageSource.?sku - sourceVersion: imageSource.?version - sourceImageId: imageSource.?imageId - sourceImageVersionID: imageSource.?imageVersionID - creationTime: baseTime + distribute: map(distributions, distribution => { + type: distribution.type + artifactTags: distribution.?artifactTags ?? { + sourceType: imageSource.type + sourcePublisher: imageSource.?publisher + sourceOffer: imageSource.?offer + sourceSku: imageSource.?sku + sourceVersion: imageSource.?version + sourceImageId: imageSource.?imageId + sourceImageVersionID: imageSource.?imageVersionID + creationTime: baseTime + } + ...(distribution.type == 'ManagedImage' + ? { + runOutputName: distribution.?runOutputName ?? '${distribution.imageName}-${baseTime}-ManagedImage' + location: distribution.?location ?? location + #disable-next-line use-resource-id-functions // Disabling rule as this is an input parameter that is used inside an array. + imageId: distribution.?imageResourceId ?? '${subscription().id}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Compute/images/${distribution.imageName}-${baseTime}' } - }, - (distribution.type == 'ManagedImage' - ? { - runOutputName: distribution.?runOutputName ?? '${distribution.imageName}-${baseTime}-ManagedImage' - location: distribution.?location ?? location - #disable-next-line use-resource-id-functions // Disabling rule as this is an input parameter that is used inside an array. - imageId: distribution.?imageResourceId ?? '${subscription().id}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Compute/images/${distribution.imageName}-${baseTime}' - } - : {}), - (distribution.type == 'SharedImage' - ? { - runOutputName: distribution.?runOutputName ?? (!empty(distribution.?sharedImageGalleryImageDefinitionResourceId) - ? '${last(split((distribution.sharedImageGalleryImageDefinitionResourceId ?? '/'), '/'))}-SharedImage' - : 'SharedImage') - galleryImageId: !empty(distribution.?sharedImageGalleryImageDefinitionTargetVersion) - ? '${distribution.sharedImageGalleryImageDefinitionResourceId}/versions/${distribution.sharedImageGalleryImageDefinitionTargetVersion}' - : distribution.sharedImageGalleryImageDefinitionResourceId - excludeFromLatest: distribution.?excludeFromLatest ?? false - replicationRegions: distribution.?replicationRegions ?? [location] - storageAccountType: distribution.?storageAccountType ?? 'Standard_LRS' - } - : {}), - (distribution.type == 'VHD' - ? { - runOutputName: distribution.?runOutputName ?? '${distribution.imageName}-VHD' - } - : {}) - ) - ] + : {}) + ...(distribution.type == 'SharedImage' + ? { + runOutputName: distribution.?runOutputName ?? (!empty(distribution.?sharedImageGalleryImageDefinitionResourceId) + ? '${last(split((distribution.sharedImageGalleryImageDefinitionResourceId ?? '/'), '/'))}-SharedImage' + : 'SharedImage') + galleryImageId: !empty(distribution.?sharedImageGalleryImageDefinitionTargetVersion) + ? '${distribution.sharedImageGalleryImageDefinitionResourceId}/versions/${distribution.sharedImageGalleryImageDefinitionTargetVersion}' + : distribution.sharedImageGalleryImageDefinitionResourceId + excludeFromLatest: distribution.?excludeFromLatest ?? false + replicationRegions: distribution.?replicationRegions ?? [location] + storageAccountType: distribution.?storageAccountType ?? 'Standard_LRS' + } + : {}) + ...(distribution.type == 'VHD' + ? { + runOutputName: distribution.?runOutputName ?? '${distribution.imageName}-VHD' + } + : {}) + }) #disable-next-line BCP225 // The discriminator property "type" value cannot be determined at compilation time. - which is fine validate: validationProcess optimize: optimizeVmBoot != null @@ -193,6 +217,14 @@ resource imageTemplate 'Microsoft.VirtualMachineImages/imageTemplates@2023-07-01 } } : null + autoRun: { + state: autoRunState + } + errorHandling: { + onCustomizerError: errorHandlingOnCustomizerError + onValidationError: errorHandlingOnValidationError + } + managedResourceTags: managedResourceTags } } diff --git a/avm/res/virtual-machine-images/image-template/main.json b/avm/res/virtual-machine-images/image-template/main.json index 52ab9f355b..b601983e44 100644 --- a/avm/res/virtual-machine-images/image-template/main.json +++ b/avm/res/virtual-machine-images/image-template/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "6178255053020724047" + "version": "0.33.13.18514", + "templateHash": "17172875475270738165" }, "name": "Virtual Machine Image Templates", "description": "This module deploys a Virtual Machine Image Template that can be consumed by Azure Image Builder (AIB)." @@ -332,7 +332,7 @@ "metadata": { "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } }, @@ -353,7 +353,7 @@ "metadata": { "description": "An AVM-aligned type for a managed identity configuration. To be used if only user-assigned identities are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } }, @@ -428,7 +428,7 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } } @@ -574,6 +574,46 @@ "metadata": { "description": "Optional. The optimize property can be enabled while creating a VM image and allows VM optimization to improve image creation time." } + }, + "autoRunState": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Indicates whether or not to automatically run the image template build on template creation or update." + } + }, + "errorHandlingOnCustomizerError": { + "type": "string", + "defaultValue": "cleanup", + "allowedValues": [ + "cleanup", + "abort" + ], + "metadata": { + "description": "Optional. If there is a customizer error and this field is set to 'cleanup', the build VM and associated network resources will be cleaned up. This is the default behavior. If there is a customizer error and this field is set to 'abort', the build VM will be preserved." + } + }, + "errorHandlingOnValidationError": { + "type": "string", + "defaultValue": "cleanup", + "allowedValues": [ + "cleanup", + "abort" + ], + "metadata": { + "description": "Optional. If there is a validation error and this field is set to 'cleanup', the build VM and associated network resources will be cleaned up. If there is a validation error and this field is set to 'abort', the build VM will be preserved. This is the default behavior." + } + }, + "managedResourceTags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags that will be applied to the resource group and/or resources created by the service." + } } }, "variables": { @@ -619,32 +659,12 @@ }, "imageTemplate": { "type": "Microsoft.VirtualMachineImages/imageTemplates", - "apiVersion": "2023-07-01", + "apiVersion": "2024-02-01", "name": "[format('{0}-{1}', parameters('name'), parameters('baseTime'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "identity": "[variables('identity')]", - "properties": { - "copy": [ - { - "name": "distribute", - "count": "[length(parameters('distributions'))]", - "input": "[union(createObject('type', parameters('distributions')[copyIndex('distribute')].type, 'artifactTags', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'artifactTags'), createObject('sourceType', parameters('imageSource').type, 'sourcePublisher', tryGet(parameters('imageSource'), 'publisher'), 'sourceOffer', tryGet(parameters('imageSource'), 'offer'), 'sourceSku', tryGet(parameters('imageSource'), 'sku'), 'sourceVersion', tryGet(parameters('imageSource'), 'version'), 'sourceImageId', tryGet(parameters('imageSource'), 'imageId'), 'sourceImageVersionID', tryGet(parameters('imageSource'), 'imageVersionID'), 'creationTime', parameters('baseTime')))), if(equals(parameters('distributions')[copyIndex('distribute')].type, 'ManagedImage'), createObject('runOutputName', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'runOutputName'), format('{0}-{1}-ManagedImage', parameters('distributions')[copyIndex('distribute')].imageName, parameters('baseTime'))), 'location', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'location'), parameters('location')), 'imageId', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'imageResourceId'), format('{0}/resourceGroups/{1}/providers/Microsoft.Compute/images/{2}-{3}', subscription().id, resourceGroup().name, parameters('distributions')[copyIndex('distribute')].imageName, parameters('baseTime')))), createObject()), if(equals(parameters('distributions')[copyIndex('distribute')].type, 'SharedImage'), createObject('runOutputName', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'runOutputName'), if(not(empty(tryGet(parameters('distributions')[copyIndex('distribute')], 'sharedImageGalleryImageDefinitionResourceId'))), format('{0}-SharedImage', last(split(coalesce(parameters('distributions')[copyIndex('distribute')].sharedImageGalleryImageDefinitionResourceId, '/'), '/'))), 'SharedImage')), 'galleryImageId', if(not(empty(tryGet(parameters('distributions')[copyIndex('distribute')], 'sharedImageGalleryImageDefinitionTargetVersion'))), format('{0}/versions/{1}', parameters('distributions')[copyIndex('distribute')].sharedImageGalleryImageDefinitionResourceId, parameters('distributions')[copyIndex('distribute')].sharedImageGalleryImageDefinitionTargetVersion), parameters('distributions')[copyIndex('distribute')].sharedImageGalleryImageDefinitionResourceId), 'excludeFromLatest', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'excludeFromLatest'), false()), 'replicationRegions', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'replicationRegions'), createArray(parameters('location'))), 'storageAccountType', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'storageAccountType'), 'Standard_LRS')), createObject()), if(equals(parameters('distributions')[copyIndex('distribute')].type, 'VHD'), createObject('runOutputName', coalesce(tryGet(parameters('distributions')[copyIndex('distribute')], 'runOutputName'), format('{0}-VHD', parameters('distributions')[copyIndex('distribute')].imageName))), createObject()))]" - } - ], - "buildTimeoutInMinutes": "[parameters('buildTimeoutInMinutes')]", - "vmProfile": { - "vmSize": "[parameters('vmSize')]", - "osDiskSizeGB": "[parameters('osDiskSizeGB')]", - "userAssignedIdentities": "[parameters('vmUserAssignedIdentities')]", - "vnetConfig": "[if(not(empty(parameters('subnetResourceId'))), createObject('subnetId', parameters('subnetResourceId')), null())]" - }, - "source": "[parameters('imageSource')]", - "customize": "[parameters('customizationSteps')]", - "stagingResourceGroup": "[parameters('stagingResourceGroupResourceId')]", - "validate": "[parameters('validationProcess')]", - "optimize": "[if(not(equals(parameters('optimizeVmBoot'), null())), createObject('vmBoot', createObject('state', parameters('optimizeVmBoot'))), null())]" - } + "properties": "[shallowMerge(createArray(createObject('buildTimeoutInMinutes', parameters('buildTimeoutInMinutes'), 'vmProfile', createObject('vmSize', parameters('vmSize'), 'osDiskSizeGB', parameters('osDiskSizeGB'), 'userAssignedIdentities', parameters('vmUserAssignedIdentities'), 'vnetConfig', if(not(empty(parameters('subnetResourceId'))), createObject('subnetId', parameters('subnetResourceId')), null())), 'source', parameters('imageSource')), if(not(empty(parameters('customizationSteps'))), createObject('customize', parameters('customizationSteps')), createObject()), createObject('stagingResourceGroup', parameters('stagingResourceGroupResourceId'), 'distribute', map(parameters('distributions'), lambda('distribution', shallowMerge(createArray(createObject('type', lambdaVariables('distribution').type, 'artifactTags', coalesce(tryGet(lambdaVariables('distribution'), 'artifactTags'), createObject('sourceType', parameters('imageSource').type, 'sourcePublisher', tryGet(parameters('imageSource'), 'publisher'), 'sourceOffer', tryGet(parameters('imageSource'), 'offer'), 'sourceSku', tryGet(parameters('imageSource'), 'sku'), 'sourceVersion', tryGet(parameters('imageSource'), 'version'), 'sourceImageId', tryGet(parameters('imageSource'), 'imageId'), 'sourceImageVersionID', tryGet(parameters('imageSource'), 'imageVersionID'), 'creationTime', parameters('baseTime')))), if(equals(lambdaVariables('distribution').type, 'ManagedImage'), createObject('runOutputName', coalesce(tryGet(lambdaVariables('distribution'), 'runOutputName'), format('{0}-{1}-ManagedImage', lambdaVariables('distribution').imageName, parameters('baseTime'))), 'location', coalesce(tryGet(lambdaVariables('distribution'), 'location'), parameters('location')), 'imageId', coalesce(tryGet(lambdaVariables('distribution'), 'imageResourceId'), format('{0}/resourceGroups/{1}/providers/Microsoft.Compute/images/{2}-{3}', subscription().id, resourceGroup().name, lambdaVariables('distribution').imageName, parameters('baseTime')))), createObject()), if(equals(lambdaVariables('distribution').type, 'SharedImage'), createObject('runOutputName', coalesce(tryGet(lambdaVariables('distribution'), 'runOutputName'), if(not(empty(tryGet(lambdaVariables('distribution'), 'sharedImageGalleryImageDefinitionResourceId'))), format('{0}-SharedImage', last(split(coalesce(lambdaVariables('distribution').sharedImageGalleryImageDefinitionResourceId, '/'), '/'))), 'SharedImage')), 'galleryImageId', if(not(empty(tryGet(lambdaVariables('distribution'), 'sharedImageGalleryImageDefinitionTargetVersion'))), format('{0}/versions/{1}', lambdaVariables('distribution').sharedImageGalleryImageDefinitionResourceId, lambdaVariables('distribution').sharedImageGalleryImageDefinitionTargetVersion), lambdaVariables('distribution').sharedImageGalleryImageDefinitionResourceId), 'excludeFromLatest', coalesce(tryGet(lambdaVariables('distribution'), 'excludeFromLatest'), false()), 'replicationRegions', coalesce(tryGet(lambdaVariables('distribution'), 'replicationRegions'), createArray(parameters('location'))), 'storageAccountType', coalesce(tryGet(lambdaVariables('distribution'), 'storageAccountType'), 'Standard_LRS')), createObject()), if(equals(lambdaVariables('distribution').type, 'VHD'), createObject('runOutputName', coalesce(tryGet(lambdaVariables('distribution'), 'runOutputName'), format('{0}-VHD', lambdaVariables('distribution').imageName))), createObject()))))), 'validate', parameters('validationProcess'), 'optimize', if(not(equals(parameters('optimizeVmBoot'), null())), createObject('vmBoot', createObject('state', parameters('optimizeVmBoot'))), null()), 'autoRun', createObject('state', parameters('autoRunState')), 'errorHandling', createObject('onCustomizerError', parameters('errorHandlingOnCustomizerError'), 'onValidationError', parameters('errorHandlingOnValidationError')), 'managedResourceTags', parameters('managedResourceTags'))))]" }, "imageTemplate_lock": { "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", @@ -724,7 +744,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('imageTemplate', '2023-07-01', 'full').location]" + "value": "[reference('imageTemplate', '2024-02-01', 'full').location]" } } } \ No newline at end of file diff --git a/avm/res/virtual-machine-images/image-template/tests/e2e/defaults/main.test.bicep b/avm/res/virtual-machine-images/image-template/tests/e2e/defaults/main.test.bicep index 246c9d8a1f..8981813b22 100644 --- a/avm/res/virtual-machine-images/image-template/tests/e2e/defaults/main.test.bicep +++ b/avm/res/virtual-machine-images/image-template/tests/e2e/defaults/main.test.bicep @@ -64,7 +64,6 @@ module testDeployment '../../../main.bicep' = { type: 'ManagedImage' } ] - managedIdentities: { userAssignedResourceIds: [ nestedDependencies.outputs.managedIdentityResourceId diff --git a/avm/res/virtual-machine-images/image-template/tests/e2e/max/main.test.bicep b/avm/res/virtual-machine-images/image-template/tests/e2e/max/main.test.bicep index f997482ca2..a67d6f9242 100644 --- a/avm/res/virtual-machine-images/image-template/tests/e2e/max/main.test.bicep +++ b/avm/res/virtual-machine-images/image-template/tests/e2e/max/main.test.bicep @@ -132,6 +132,13 @@ module testDeployment '../../../main.bicep' = { vmUserAssignedIdentities: [ nestedDependencies.outputs.managedIdentityResourceId ] + autoRunState: 'Enabled' + errorHandlingOnCustomizerError: 'cleanup' + errorHandlingOnValidationError: 'abort' + managedResourceTags: { + testKey1: 'testValue1' + testKey2: 'testValue2' + } lock: { kind: 'CanNotDelete' name: 'myCustomLockName' diff --git a/avm/res/virtual-machine-images/image-template/version.json b/avm/res/virtual-machine-images/image-template/version.json index 3f863a2bec..04a0dd1a80 100644 --- a/avm/res/virtual-machine-images/image-template/version.json +++ b/avm/res/virtual-machine-images/image-template/version.json @@ -1,7 +1,7 @@ { "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", - "version": "0.4", + "version": "0.5", "pathFilters": [ "./main.json" ] -} \ No newline at end of file +}