Skip to content

Commit

Permalink
TW-79696 Enable support for linked templates on Azure Cloud plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyaFomenko committed Mar 13, 2023
1 parent d441831 commit 11b6383
Show file tree
Hide file tree
Showing 13 changed files with 592 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ class AzureCloudClientFactory(cloudRegistrar: CloudRegistrar,
param.getParameter(AzureConstants.SPOT_VM)?.toBoolean(),
param.getParameter(AzureConstants.ENABLE_SPOT_PRICE)?.toBoolean(),
param.getParameter(AzureConstants.SPOT_PRICE)?.toInt(),
param.getParameter(AzureConstants.ENABLE_ACCELERATED_NETWORKING)?.toBoolean())
param.getParameter(AzureConstants.ENABLE_ACCELERATED_NETWORKING)?.toBoolean(),
param.getParameter(AzureConstants.DISABLE_TEMPLATE_MODIFICATION)?.toBoolean())
}.apply {
AzureUtils.setPasswords(AzureCloudImageDetails::class.java, params, this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ class AzureCloudImageDetails(
@SerializedName(AzureConstants.SPOT_PRICE)
val spotPrice: Int?,
@SerializedName(AzureConstants.ENABLE_ACCELERATED_NETWORKING)
val enableAcceleratedNetworking: Boolean?
val enableAcceleratedNetworking: Boolean?,
@SerializedName(AzureConstants.DISABLE_TEMPLATE_MODIFICATION)
val disableTemplateModification: Boolean?

) : CloudImagePasswordDetails {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ class AzureConstants {
val enableAcceleratedNetworking: String
get() = ENABLE_ACCELERATED_NETWORKING

val disableTemplateModification: String
get() = DISABLE_TEMPLATE_MODIFICATION

companion object {
const val CLOUD_CODE = "arm"

Expand Down Expand Up @@ -185,6 +188,7 @@ class AzureConstants {
const val ENABLE_SPOT_PRICE = "enableSpotPrice"
const val SPOT_PRICE = "spotPrice"
const val ENABLE_ACCELERATED_NETWORKING = "enableAcceleratedNetworking"
const val DISABLE_TEMPLATE_MODIFICATION = "disableTemplateModification"

const val TAG_SERVER = "teamcity-server"
const val TAG_PROFILE = "teamcity-profile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ class AzureApiConnectorImpl(
val handler = instance.image.handler
val builder = handler!!.prepareBuilder(instance)
.setCustomData(customData)
.setTags(VM_RESOURCE_NAME, instance.properties)
.setVMTags(instance.properties)

val details = instance.image.imageDetails
val groupId = when (details.target) {
Expand Down Expand Up @@ -1055,9 +1055,7 @@ class AzureApiConnectorImpl(
companion object {
private val LOG = Logger.getInstance(AzureApiConnectorImpl::class.java.name)
private val RESOURCE_GROUP_PATTERN = Regex("resourceGroups/([^/]+)/providers/")

private const val CONTAINER_RESOURCE_NAME = "[parameters('containerName')]"
private const val VM_RESOURCE_NAME = "[parameters('vmName')]"
private val SERVICE_TYPES = mapOf(
"Microsoft.ContainerInstance" to listOf("containerGroups"),
"Microsoft.Compute" to listOf("virtualMachines")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class AzureTemplateHandler(private val connector: AzureApiConnector) : AzureHand

override suspend fun prepareBuilder(instance: AzureCloudInstance) = coroutineScope {
val details = instance.image.imageDetails
ArmTemplateBuilder(details.template!!)
ArmTemplateBuilder(details.template!!, details.disableTemplateModification ?: false)
.setParameterValue("vmName", instance.name)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,42 @@ fun AzureCloudImageDetails.checkTemplate(exceptions: ArrayList<Throwable>) {
throw CheckedCloudException("Invalid JSON template", e)
}

try {
root["parameters"]["vmName"] as ObjectNode
val templateParameters = try {
AzureUtils.getTemplateParameters(root)
} catch (e: Exception) {
throw CheckedCloudException("No 'vmName' parameter", e)
throw CheckedCloudException("Invalid JSON template", e)
}

val providedParameters = AzureUtils.getProvidedTemplateParameters(disableTemplateModification)
val matchingErrors = AzureUtils
.getMatchedTemplateParameters(providedParameters, templateParameters)
.filter {!it.isMatched && (it.isProvided || !it.hasValue) }

if (matchingErrors.any()) {
val message = matchingErrors
.sortedBy { it.name }
.map {
when {
it.isProvided -> "Parameter '${it.name}' is required but not declared in template"
!it.hasValue -> "Parameter '${it.name}' is declared in template but cannot be resolved"
else -> "Problem with '${it.name}' parameter"
}
}
.joinToString(";${System::lineSeparator}")
throw CheckedCloudException(message);
}

try {
val resources = root["resources"] as ArrayNode
resources.filterIsInstance<ObjectNode>()
if (disableTemplateModification != true) {
try {
val resources = root["resources"] as ArrayNode
resources.filterIsInstance<ObjectNode>()
.first {
it["type"].asText() == "Microsoft.Compute/virtualMachines" &&
it["name"].asText() == "[parameters('vmName')]"
}
} catch (e: Exception) {
throw CheckedCloudException("No virtual machine resource with name set to 'vmName' parameter", e)
} catch (e: Exception) {
throw CheckedCloudException("No virtual machine resource with name set to 'vmName' parameter", e)
}
}
} catch (e: Throwable) {
AzureTemplateHandler.LOG.infoAndDebugDetails("Invalid template", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import java.util.*
/**
* Allows to customize ARM template.
*/
class ArmTemplateBuilder(template: String) {
class ArmTemplateBuilder(template: String, private val disableTemplateModification: Boolean = false) {

private val mapper = ObjectMapper()
private var root: ObjectNode
Expand Down Expand Up @@ -68,6 +68,14 @@ class ArmTemplateBuilder(template: String) {
return this
}

fun setVMTags(tags: Map<String, String>): ArmTemplateBuilder {
if (!disableTemplateModification) {
return setTags("[parameters('vmName')]", tags)
}
tags.entries.forEach { (name, value) -> setParameterValue(name, value) }
return this
}

@Suppress("unused", "MayBeConstant")
fun setPublicIp(): ArmTemplateBuilder {
(root["variables"] as ObjectNode).apply {
Expand Down Expand Up @@ -154,16 +162,19 @@ class ArmTemplateBuilder(template: String) {
}

fun setCustomData(customData: String): ArmTemplateBuilder {
(root["resources"] as ArrayNode).apply {
this.filterIsInstance<ObjectNode>()
if (!disableTemplateModification) {
(root["resources"] as ArrayNode).apply {
this.filterIsInstance<ObjectNode>()
.first { it["name"].asText() == "[parameters('vmName')]" }
.apply {
val properties = (this["properties"] as? ObjectNode) ?: this.putObject("properties")
val osProfile = (properties["osProfile"] as? ObjectNode) ?: properties.putObject("osProfile")
osProfile.put("customData", customData)
}
}
return this
}
return this
return setParameterValue("customData", customData)
}

fun setParameterValue(name: String, value: String): ArmTemplateBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package jetbrains.buildServer.clouds.azure.arm.utils

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.intellij.openapi.util.io.StreamUtil
import com.microsoft.aad.adal4j.AuthenticationException
Expand All @@ -26,6 +27,7 @@ import jetbrains.buildServer.clouds.azure.arm.AzureCloudImageDetails
import jetbrains.buildServer.clouds.azure.arm.AzureCloudImageType
import jetbrains.buildServer.util.ExceptionUtil
import jetbrains.buildServer.util.StringUtil
import org.springframework.util.StringUtils
import java.io.IOException

/**
Expand All @@ -35,6 +37,18 @@ object AzureUtils {
private val INVALID_TENANT = Regex("AADSTS90002: No service namespace named '([\\w-]+)' was found in the data store\\.")
private val ENVIRONMENT_VARIABLE_REGEX = Regex("^([a-z_][a-z0-9_]*?)=.*?\$", RegexOption.IGNORE_CASE)
private val CUSTOM_TAG_REGEX = Regex("^([^<>%&\\\\?/]*?)=.*?\$", RegexOption.IGNORE_CASE)
private val SIMPLE_TEMPLATE_PARAMETERS = listOf(
TemplateParameterDescriptor("vmName", "string", true)
)
private val FULL_TEMPLATE_PARAMETERS = listOf(
TemplateParameterDescriptor("vmName", "string", true),
TemplateParameterDescriptor("customData", "string", true),
TemplateParameterDescriptor("teamcity-profile", "string", true),
TemplateParameterDescriptor("teamcity-image-hash", "string", true),
TemplateParameterDescriptor("teamcity-data-hash", "string", true),
TemplateParameterDescriptor("teamcity-server", "string", true),
TemplateParameterDescriptor("teamcity-source", "string", true),
)

internal val mapper = ObjectMapper()

Expand Down Expand Up @@ -94,6 +108,71 @@ object AzureUtils {
json
}

internal fun getTemplateParameters(templateJson: String) : List<TemplateParameterDescriptor> {
if (StringUtils.isEmpty(templateJson)) {
throw TemplateParameterException("Template is empty")
}

try {
val reader = mapper.reader()
val template = reader.readTree(templateJson)

return getTemplateParameters(template)
}
catch(e: TemplateParameterException) {
throw e;
}
catch(e: Exception) {
throw TemplateParameterException("Incorrect template format", e)
}
}

internal fun getTemplateParameters(template: JsonNode) : List<TemplateParameterDescriptor> {
val parameters = template["parameters"]
if (parameters == null) {
throw TemplateParameterException("Incorrect tenplate format. No 'paramaters' section")
}
try {
return parameters
.fields()
.asSequence()
.map { (fieldName, value) ->
val type = value.get("type").asText()
val hasValue = value.has("defaultValue")
TemplateParameterDescriptor(fieldName, type, hasValue)
}
.toList()
}
catch(e: TemplateParameterException) {
throw e;
}
catch(e: Exception) {
throw TemplateParameterException("Incorrect template format", e)
}
}

internal fun getProvidedTemplateParameters(disableTemplateModification: Boolean?): List<TemplateParameterDescriptor> =
if (disableTemplateModification == true) FULL_TEMPLATE_PARAMETERS else SIMPLE_TEMPLATE_PARAMETERS

internal fun getMatchedTemplateParameters(
providedParameters: List<TemplateParameterDescriptor>,
templateParameters: List<TemplateParameterDescriptor>
): List<MatchedTemplateParameterDescriptior> {
val parametersMap = providedParameters
.map { it.name to MatchedTemplateParameterDescriptior(it, true, false) }
.toMap()
.toMutableMap()

templateParameters.forEach {
val parameter = parametersMap[it.name]
parametersMap[it.name] = if (parameter != null) {
MatchedTemplateParameterDescriptior(parameter, parameter.isProvided, true)
} else {
MatchedTemplateParameterDescriptior(it, false, false)
}
}
return parametersMap.values.toList()
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -110,3 +189,21 @@ private data class CloudAuthError(val error: String? = null,
fun AzureCloudImageDetails.isVmInstance(): Boolean {
return deployTarget == AzureCloudDeployTarget.Instance || type != AzureCloudImageType.Container
}

internal open class TemplateParameterDescriptor(
val name: String,
val type: String,
val hasValue: Boolean
) {
}

internal class MatchedTemplateParameterDescriptior(
value: TemplateParameterDescriptor,
val isProvided: Boolean,
val isMatched: Boolean
) : TemplateParameterDescriptor(value.name, value.type, value.hasValue) {
}
class TemplateParameterException : Exception {
constructor(message: String) : super(message) { }
constructor(message: String, rootCause: Throwable) : super(message, rootCause) { }
}
Loading

0 comments on commit 11b6383

Please sign in to comment.