diff --git a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudClientFactory.kt b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudClientFactory.kt index 0320164a..d7b1cdac 100644 --- a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudClientFactory.kt +++ b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudClientFactory.kt @@ -103,7 +103,10 @@ class AzureCloudClientFactory(cloudRegistrar: CloudRegistrar, param.getParameter(AzureConstants.ENABLE_SPOT_PRICE)?.toBoolean(), param.getParameter(AzureConstants.SPOT_PRICE)?.toInt(), param.getParameter(AzureConstants.ENABLE_ACCELERATED_NETWORKING)?.toBoolean(), - param.getParameter(AzureConstants.DISABLE_TEMPLATE_MODIFICATION)?.toBoolean()) + param.getParameter(AzureConstants.DISABLE_TEMPLATE_MODIFICATION)?.toBoolean(), + param.getParameter(AzureConstants.USER_ASSIGNED_IDENTITY) ?: "", + param.getParameter(AzureConstants.ENABLE_SYSTEM_ASSIGNED_IDENTITY)?.toBoolean() + ) }.apply { AzureUtils.setPasswords(AzureCloudImageDetails::class.java, params, this) } diff --git a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageDetails.kt b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageDetails.kt index b56810fa..0f2ebfec 100644 --- a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageDetails.kt +++ b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageDetails.kt @@ -88,7 +88,11 @@ class AzureCloudImageDetails( @SerializedName(AzureConstants.ENABLE_ACCELERATED_NETWORKING) val enableAcceleratedNetworking: Boolean?, @SerializedName(AzureConstants.DISABLE_TEMPLATE_MODIFICATION) - val disableTemplateModification: Boolean? + val disableTemplateModification: Boolean?, + @SerializedName(AzureConstants.USER_ASSIGNED_IDENTITY) + val userAssignedIdentity: String? = null, + @SerializedName(AzureConstants.ENABLE_SYSTEM_ASSIGNED_IDENTITY) + val enableSystemAssignedIdentity: Boolean? ) : CloudImagePasswordDetails { diff --git a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureConstants.kt b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureConstants.kt index 462cd5ab..a721106e 100644 --- a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureConstants.kt +++ b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureConstants.kt @@ -146,6 +146,12 @@ class AzureConstants { val disableTemplateModification: String get() = DISABLE_TEMPLATE_MODIFICATION + val userAssignedIdentity: String + get() = USER_ASSIGNED_IDENTITY + + val enableSystemAssignedIdentity: String + get() = ENABLE_SYSTEM_ASSIGNED_IDENTITY + companion object { const val CLOUD_CODE = "arm" @@ -189,6 +195,8 @@ class AzureConstants { const val SPOT_PRICE = "spotPrice" const val ENABLE_ACCELERATED_NETWORKING = "enableAcceleratedNetworking" const val DISABLE_TEMPLATE_MODIFICATION = "disableTemplateModification" + const val USER_ASSIGNED_IDENTITY = "userAssignedIdentity" + const val ENABLE_SYSTEM_ASSIGNED_IDENTITY = "enableSystemAssignedIdentity" const val TAG_SERVER = "teamcity-server" const val TAG_PROFILE = "teamcity-profile" diff --git a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/types/AzureImageHandler.kt b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/types/AzureImageHandler.kt index 7c304015..dabf94b6 100644 --- a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/types/AzureImageHandler.kt +++ b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/types/AzureImageHandler.kt @@ -83,6 +83,8 @@ class AzureImageHandler(private val connector: AzureApiConnector) : AzureHandler if (details.enableAcceleratedNetworking == true) { builder.enableAcceleratedNerworking() } + + builder.setupIdentity(details.userAssignedIdentity, details.enableSystemAssignedIdentity) builder } diff --git a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/utils/ArmTemplateBuilder.kt b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/utils/ArmTemplateBuilder.kt index 0ac75ea6..c0ebd348 100644 --- a/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/utils/ArmTemplateBuilder.kt +++ b/plugin-azure-server/src/main/kotlin/jetbrains/buildServer/clouds/azure/arm/utils/ArmTemplateBuilder.kt @@ -384,6 +384,12 @@ class ArmTemplateBuilder(template: String, private val disableTemplateModificati } } + private fun getFirstResourceOfType(type: String): ObjectNode { + val resources = root["resources"] as ArrayNode + val groups = resources.filterIsInstance().first { it["type"].asText() == type } + return groups + } + private fun getPropertiesOfResource(resourceName: String): ObjectNode { return getPropertiesOfResource("name", resourceName) } @@ -428,6 +434,26 @@ class ArmTemplateBuilder(template: String, private val disableTemplateModificati return this } + fun setupIdentity(userAssignedIdentity: String?, enableSystemAssignedIdentity: Boolean?): ArmTemplateBuilder { + val resource = getFirstResourceOfType("Microsoft.Compute/virtualMachines") + + val hasUserAssigned = !userAssignedIdentity.isNullOrEmpty(); + val hasSystemAssigned = enableSystemAssignedIdentity == true; + + if (hasUserAssigned || hasSystemAssigned) { + val identity = resource.putObject("identity") + + val fullType = if (hasUserAssigned && hasSystemAssigned) "SystemAssigned, UserAssigned" else if (hasSystemAssigned) "SystemAssigned" else "UserAssigned" + identity.put("type", fullType) + + if (hasUserAssigned) { + identity.putObject("userAssignedIdentities").putObject(userAssignedIdentity) + } + } + + return this + } + companion object { private val LOG = Logger.getInstance(ArmTemplateBuilder::class.java.name) private val PRICE_DIVIDER = 100000F diff --git a/plugin-azure-server/src/main/resources/buildServerResources/images.vm.js b/plugin-azure-server/src/main/resources/buildServerResources/images.vm.js index 0dbf962f..8c2e40c3 100644 --- a/plugin-azure-server/src/main/resources/buildServerResources/images.vm.js +++ b/plugin-azure-server/src/main/resources/buildServerResources/images.vm.js @@ -95,6 +95,9 @@ function ArmImagesViewModel($, ko, dialog, config) { self.template = ko.observable(''); self.disableTemplateModification = ko.observable(false); + self.userAssignedIdentity = ko.observable(); + self.enableSystemAssignedIdentity = ko.observable(false); + var requiredForDeployment = { required: { onlyIf: function () { @@ -416,6 +419,8 @@ function ArmImagesViewModel($, ko, dialog, config) { .extend({min: 0.00001, max: 20000}), enableAcceleratedNetworking: ko.observable(false), disableTemplateModification: self.disableTemplateModification, + userAssignedIdentity: ko.observable(), + enableSystemAssignedIdentity: self.enableSystemAssignedIdentity }); // Data from Azure APIs @@ -671,6 +676,8 @@ function ArmImagesViewModel($, ko, dialog, config) { image.enableSpotPrice = JSON.parse(image.enableSpotPrice || "false"); image.enableAcceleratedNetworking = JSON.parse(image.enableAcceleratedNetworking || "false"); image.disableTemplateModification = JSON.parse(image.disableTemplateModification || "false"); + image.userAssignedIdentity = image.userAssignedIdentity; + image.enableSystemAssignedIdentity = JSON.parse(image.enableSystemAssignedIdentity || "false"); }); self.images(images); @@ -714,7 +721,9 @@ function ArmImagesViewModel($, ko, dialog, config) { customTags: "", spotVm: false, enableSpotPrice: false, - spotPrice: spotPriceDefault * priceDivider + spotPrice: spotPriceDefault * priceDivider, + userAssignedIdentity: "", + systemAssignedIdentity: false, }; // Pre-fill collections while loading resources @@ -788,6 +797,8 @@ function ArmImagesViewModel($, ko, dialog, config) { model.spotPrice(image.spotPrice != null ? image.spotPrice/priceDivider : undefined); model.enableAcceleratedNetworking(image.enableAcceleratedNetworking); model.disableTemplateModification(image.disableTemplateModification); + model.userAssignedIdentity(image.userAssignedIdentity); + model.enableSystemAssignedIdentity(image.enableSystemAssignedIdentity); model.registryPassword(""); model.vmPassword(""); @@ -847,7 +858,9 @@ function ArmImagesViewModel($, ko, dialog, config) { enableSpotPrice: model.enableSpotPrice(), spotPrice: model.spotPrice() != null ? Math.trunc(parseFloat(model.spotPrice())*priceDivider) : undefined, enableAcceleratedNetworking: model.enableAcceleratedNetworking(), - disableTemplateModification: model.disableTemplateModification() + disableTemplateModification: model.disableTemplateModification(), + userAssignedIdentity: model.userAssignedIdentity(), + enableSystemAssignedIdentity: model.enableSystemAssignedIdentity(), }; var originalImage = self.originalImage; diff --git a/plugin-azure-server/src/main/resources/buildServerResources/settings.jsp b/plugin-azure-server/src/main/resources/buildServerResources/settings.jsp index ef2d92bd..cab4f6d7 100644 --- a/plugin-azure-server/src/main/resources/buildServerResources/settings.jsp +++ b/plugin-azure-server/src/main/resources/buildServerResources/settings.jsp @@ -493,6 +493,27 @@ + + + + + Supply the ARM resource id for the identity according to the virtual machine identity schema. + + + + + + + + + Create a system-assigned identity. + + + diff --git a/plugin-azure-server/src/test/kotlin/jetbrains.buildServer.clouds/utils/ArmTemplateBuilderTest.kt b/plugin-azure-server/src/test/kotlin/jetbrains.buildServer.clouds/utils/ArmTemplateBuilderTest.kt index 5ab9e94a..08deddf8 100644 --- a/plugin-azure-server/src/test/kotlin/jetbrains.buildServer.clouds/utils/ArmTemplateBuilderTest.kt +++ b/plugin-azure-server/src/test/kotlin/jetbrains.buildServer.clouds/utils/ArmTemplateBuilderTest.kt @@ -210,4 +210,73 @@ class ArmTemplateBuilderTest { Assert.assertEquals(builder.toString(), "{\"resources\":[{\"type\":\"Microsoft.Network/networkInterfaces\",\"name\":\"myName\",\"properties\":{\"enableAcceleratedNetworking\":true}}]}") } + + fun testSetupSystemIdentity() { + val builder = ArmTemplateBuilder("""{"resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "name": "myName", + "properties": { + } + } + ]}""").setupIdentity(null, true) + + Assert.assertEquals(builder.toString(), + "{\"resources\":[{\"type\":\"Microsoft.Compute/virtualMachines\",\"name\":\"myName\",\"properties\":{},\"identity\":{\"type\":\"SystemAssigned\"}}]}") + } + + fun testSetupSystemAssignedIdentity() { + val builder = ArmTemplateBuilder("""{"resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "name": "myName", + "properties": { + } + } + ]}""").setupIdentity(null, true) + + Assert.assertEquals(builder.toString(), + "{\"resources\":[{\"type\":\"Microsoft.Compute/virtualMachines\",\"name\":\"myName\",\"properties\":{},\"identity\":{\"type\":\"SystemAssigned\"}}]}") + } + + fun testSetupUserAssignedIdentity() { + val builder = ArmTemplateBuilder("""{"resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "name": "myName", + "properties": { + } + } + ]}""").setupIdentity("someIdentity", false) + + Assert.assertEquals(builder.toString(), + "{\"resources\":[{\"type\":\"Microsoft.Compute/virtualMachines\",\"name\":\"myName\",\"properties\":{},\"identity\":{\"type\":\"UserAssigned\",\"userAssignedIdentities\":{\"someIdentity\":{}}}}]}") } + + fun testSetupSystemAndUserAssignedIdentity() { + val builder = ArmTemplateBuilder("""{"resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "name": "myName", + "properties": { + } + } + ]}""").setupIdentity("someIdentity", true) + + Assert.assertEquals(builder.toString(), + "{\"resources\":[{\"type\":\"Microsoft.Compute/virtualMachines\",\"name\":\"myName\",\"properties\":{},\"identity\":{\"type\":\"SystemAssigned, UserAssigned\",\"userAssignedIdentities\":{\"someIdentity\":{}}}}]}") + } + + fun testSetupIdentityWithNoIdentities() { + val builder = ArmTemplateBuilder("""{"resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "name": "myName", + "properties": { + } + } + ]}""").setupIdentity("", false) + + Assert.assertEquals(builder.toString(), + "{\"resources\":[{\"type\":\"Microsoft.Compute/virtualMachines\",\"name\":\"myName\",\"properties\":{}}]}") + } } diff --git a/plugin-azure-server/src/test/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageTest.kt b/plugin-azure-server/src/test/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageTest.kt index 3ca0f0c1..3cd2b6ab 100644 --- a/plugin-azure-server/src/test/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageTest.kt +++ b/plugin-azure-server/src/test/kotlin/jetbrains/buildServer/clouds/azure/arm/AzureCloudImageTest.kt @@ -74,7 +74,9 @@ class AzureCloudImageTest : MockObjectTestCase() { enableSpotPrice = null, spotPrice = null, enableAcceleratedNetworking = null, - disableTemplateModification = null + disableTemplateModification = null, + userAssignedIdentity = null, + enableSystemAssignedIdentity = null ) myJob = SupervisorJob()