Skip to content

Commit

Permalink
#1411 Dynamic workflow JSON schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoraboeuf committed Feb 17, 2025
1 parent 19d54d0 commit 44c6acd
Show file tree
Hide file tree
Showing 42 changed files with 688 additions and 263 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import net.nemerosa.ontrack.extension.workflows.execution.AbstractTypedWorkflowN
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutorResult
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.json.parse
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.Event
import net.nemerosa.ontrack.model.events.EventTemplatingService
import net.nemerosa.ontrack.model.events.PlainEventRenderer
Expand All @@ -25,6 +26,7 @@ import java.util.*
import kotlin.jvm.optionals.getOrNull

@Component
@Documentation(AutoVersioningWorkflowNodeExecutorData::class)
class AutoVersioningWorkflowNodeExecutor(
extensionFeature: AutoVersioningExtensionFeature,
private val autoVersioningProcessingService: AutoVersioningProcessingService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutorCo
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutorResult
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.json.parse
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.SerializableEvent
import net.nemerosa.ontrack.model.security.SecurityService
import net.nemerosa.ontrack.model.structure.Build
Expand All @@ -22,6 +23,7 @@ import net.nemerosa.ontrack.model.structure.StructureService
import org.springframework.stereotype.Component

@Component
@Documentation(SlotPipelineCreationWorkflowNodeExecutorData::class)
class SlotPipelineCreationWorkflowNodeExecutor(
extensionFeature: EnvironmentsExtensionFeature,
private val slotService: SlotService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import net.nemerosa.ontrack.extension.workflows.engine.WorkflowInstance
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutor
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutorResult
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.SerializableEvent
import net.nemerosa.ontrack.model.security.SecurityService
import org.springframework.stereotype.Component

@Component
@Documentation(SlotPipelineDeployedWorkflowNodeExecutorData::class)
class SlotPipelineDeployedWorkflowNodeExecutor(
extensionFeature: EnvironmentsExtensionFeature,
private val slotService: SlotService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.nemerosa.ontrack.extension.environments.workflows.executors

/**
* Just a marker for the documentation
*/
class SlotPipelineDeployedWorkflowNodeExecutorData
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import net.nemerosa.ontrack.extension.workflows.engine.WorkflowInstance
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutor
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutorResult
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.SerializableEvent
import net.nemerosa.ontrack.model.security.SecurityService
import org.springframework.stereotype.Component

@Component
@Documentation(SlotPipelineDeployingWorkflowNodeExecutorData::class)
class SlotPipelineDeployingWorkflowNodeExecutor(
extensionFeature: EnvironmentsExtensionFeature,
private val slotService: SlotService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.nemerosa.ontrack.extension.environments.workflows.executors

/**
* Just a marker for the documentation
*/
class SlotPipelineDeployingWorkflowNodeExecutorData
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import net.nemerosa.ontrack.extension.notifications.channels.NotificationResult
import net.nemerosa.ontrack.extension.notifications.subscriptions.EventSubscriptionConfigException
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.Ack
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.Event
import net.nemerosa.ontrack.model.events.EventTemplatingService
import net.nemerosa.ontrack.model.events.PlainEventRenderer
Expand All @@ -26,6 +27,7 @@ import org.springframework.stereotype.Component
havingValue = "true",
matchIfMissing = false,
)
@Documentation(InMemoryNotificationChannelConfig::class)
class InMemoryNotificationChannel(
private val eventTemplatingService: EventTemplatingService,
) :
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.nemerosa.ontrack.extension.notifications.schema

import net.nemerosa.ontrack.extension.notifications.channels.NotificationChannel
import net.nemerosa.ontrack.model.annotations.getAPITypeDescription
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.json.schema.DynamicJsonSchemaProvider
import net.nemerosa.ontrack.model.json.schema.JsonType
import net.nemerosa.ontrack.model.json.schema.JsonTypeBuilder
import org.springframework.stereotype.Component
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.starProjectedType

@Component
class NotificationDynamicJsonSchemaProvider(
private val channels: List<NotificationChannel<*, *>>,
) : DynamicJsonSchemaProvider {

override val discriminatorValues: List<String>
get() = channels.map { it.type }

override fun getConfigurationTypes(builder: JsonTypeBuilder): Map<String, JsonType> =
channels.associate { channel ->
channel.type to getConfigurationType(channel, builder)
}

private fun getConfigurationType(
channel: NotificationChannel<*, *>,
builder: JsonTypeBuilder,
): JsonType {
val docClass = channel::class.findAnnotation<Documentation>()
?: error("${channel::class} does not have a Documentation annotation")
return builder.toType(
type = docClass.value.starProjectedType,
description = getAPITypeDescription(channel::class)
)
}

override fun toRef(id: String): String = "notification-config-$id"

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package net.nemerosa.ontrack.extension.notifications.mock
import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.notifications.channels.AbstractNotificationChannel
import net.nemerosa.ontrack.extension.notifications.channels.NotificationResult
import net.nemerosa.ontrack.it.waitUntil
import net.nemerosa.ontrack.extension.notifications.subscriptions.EventSubscriptionConfigException
import net.nemerosa.ontrack.it.waitUntil
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.Event
import net.nemerosa.ontrack.model.events.EventRendererRegistry
import net.nemerosa.ontrack.model.events.EventTemplatingService
Expand All @@ -18,6 +19,7 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime

@Component
@Documentation(MockNotificationChannelConfig::class)
class MockNotificationChannel(
private val eventTemplatingService: EventTemplatingService,
private val eventRendererRegistry: EventRendererRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.notifications.channels.AbstractNotificationChannel
import net.nemerosa.ontrack.extension.notifications.channels.NotificationResult
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.events.Event
import net.nemerosa.ontrack.model.events.EventTemplatingService
import net.nemerosa.ontrack.model.events.PlainEventRenderer
Expand All @@ -12,6 +13,7 @@ import net.nemerosa.ontrack.model.form.Text
import org.springframework.stereotype.Component

@Component
@Documentation(MockNotificationChannelConfig::class)
class OtherMockNotificationChannel(
private val eventTemplatingService: EventTemplatingService,
) :
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package net.nemerosa.ontrack.extension.workflows.definition

import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.workflows.schema.WorkflowDynamicJsonSchemaProvider
import net.nemerosa.ontrack.model.annotations.APIDescription
import net.nemerosa.ontrack.model.docs.DocumentationList
import net.nemerosa.ontrack.model.docs.SelfDocumented
import net.nemerosa.ontrack.model.json.schema.DynamicJsonSchema

/**
* Definition of a node in a workflow.
Expand All @@ -15,6 +17,11 @@ import net.nemerosa.ontrack.model.docs.SelfDocumented
* @property parents List of the IDs of the parents for this node
*/
@SelfDocumented
@DynamicJsonSchema(
discriminatorProperty = "executorId",
configurationProperty = "data",
provider = WorkflowDynamicJsonSchemaProvider::class,
)
data class WorkflowNode(
@APIDescription("Unique ID of the node in its workflow.")
val id: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package net.nemerosa.ontrack.extension.workflows.notifications
import net.nemerosa.ontrack.extension.workflows.definition.Workflow
import net.nemerosa.ontrack.model.annotations.APIDescription
import net.nemerosa.ontrack.model.docs.DocumentationField
import net.nemerosa.ontrack.model.json.schema.JsonSchemaRef

data class WorkflowNotificationChannelConfig(
@APIDescription("Workflow to run")
@DocumentationField
@JsonSchemaRef("workflow")
val workflow: Workflow,
@APIDescription("(used for test only) Short pause before launching the workflow")
val pauseMs: Long = 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package net.nemerosa.ontrack.extension.workflows.notifications

import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.notifications.schema.NotificationDynamicJsonSchemaProvider
import net.nemerosa.ontrack.model.annotations.APIDescription
import net.nemerosa.ontrack.model.json.schema.DynamicJsonSchema

@DynamicJsonSchema(
discriminatorProperty = "channel",
configurationProperty = "channelConfig",
provider = NotificationDynamicJsonSchemaProvider::class,
)
data class WorkflowNotificationChannelNodeData(
@APIDescription("Notification channel ID")
val channel: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package net.nemerosa.ontrack.extension.workflows.schema

import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutor
import net.nemerosa.ontrack.model.annotations.getAPITypeDescription
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.json.schema.DynamicJsonSchemaProvider
import net.nemerosa.ontrack.model.json.schema.JsonType
import net.nemerosa.ontrack.model.json.schema.JsonTypeBuilder
import org.springframework.stereotype.Component
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.starProjectedType

@Component
class WorkflowDynamicJsonSchemaProvider(
private val workflowNodeExecutors: List<WorkflowNodeExecutor>,
) : DynamicJsonSchemaProvider {

override val discriminatorValues: List<String>
get() = workflowNodeExecutors.map { it.id }

override fun getConfigurationTypes(builder: JsonTypeBuilder): Map<String, JsonType> =
workflowNodeExecutors.associate { executor ->
executor.id to getConfigurationType(executor, builder)
}

override fun toRef(id: String): String = "workflow-node-executor-$id"

private fun getConfigurationType(
executor: WorkflowNodeExecutor,
builder: JsonTypeBuilder,
): JsonType {
val docClass = executor::class.findAnnotation<Documentation>()
?: error("${executor::class} does not have a Documentation annotation")
return builder.toType(
type = docClass.value.starProjectedType,
description = getAPITypeDescription(executor::class)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package net.nemerosa.ontrack.extension.workflows.schema

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletResponse

@Controller
@RestController
@RequestMapping("/extension/workflows")
class WorkflowSchemaController(
private val workflowSchemaService: WorkflowSchemaService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,29 @@ package net.nemerosa.ontrack.extension.workflows.schema

import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.workflows.definition.Workflow
import net.nemerosa.ontrack.extension.workflows.definition.WorkflowNode
import net.nemerosa.ontrack.extension.workflows.execution.WorkflowNodeExecutor
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.annotations.getAPITypeDescription
import net.nemerosa.ontrack.model.docs.Documentation
import net.nemerosa.ontrack.model.json.schema.jsonSchema
import net.nemerosa.ontrack.model.support.EnvService
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Service
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.starProjectedType

@Service
class WorkflowSchemaServiceImpl(
private val envService: EnvService,
private val workflowNodeExecutors: List<WorkflowNodeExecutor>,
private val applicationContext: ApplicationContext,
) : WorkflowSchemaService {

override fun createJsonSchema(): JsonNode =
jsonSchema(
override fun createJsonSchema(): JsonNode {
val schema = jsonSchema(
ref = "workflow",
id = "https://ontrack.run/${envService.version.display}/schema/workflow",
title = "Ontrack CasC",
description = "Configuration as code for Ontrack",
title = "Ontrack workflow",
description = "Workflow definition for Ontrack",
root = Workflow::class,
) {
idConfig(
idProperty = WorkflowNode::executorId,
dataProperty = WorkflowNode::data,
types = workflowNodeExecutors,
typeBuilder = { type, toSchema ->
val docClass = type::class.findAnnotation<Documentation>()
?: error("$type does not have a Documentation annotation")
toSchema(
docClass.value.starProjectedType,
getAPITypeDescription(type::class)
).asJson()
},
typeId = { it.id },
typeRef = { "workflow-node-executor-${it.id}" },
)
) { cls ->
applicationContext.getBeansOfType(cls.java).values.single()
}
return schema.asJson()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.nemerosa.ontrack.kdsl.acceptance.tests.workflows

import net.nemerosa.ontrack.kdsl.acceptance.tests.AbstractACCDSLTestSupport
import net.nemerosa.ontrack.kdsl.spec.extension.workflows.workflows
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class ACCDSLWorkflowJSONSchema : AbstractACCDSLTestSupport() {

@Test
fun `Downloading the JSON schema for workflows`() {
val schema = ontrack.workflows.downloadJsonSchema()
assertEquals(
"#/\$defs/workflow",
schema.path("\$ref").asText()
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,6 @@ class WorkflowsMgt(connector: Connector) : Connected(connector) {
}
}

fun downloadJsonSchema() = connector.get("/extension/workflows/download/schema/json").body.asJson()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.nemerosa.ontrack.model.json.schema

abstract class AbstractJsonBaseType(
val type: String,
description: String?
) : AbstractJsonDescribedType(description)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.nemerosa.ontrack.model.json.schema

import com.fasterxml.jackson.annotation.JsonInclude

abstract class AbstractJsonDescribedType(
@JsonInclude(JsonInclude.Include.NON_NULL)
val description: String?,
) : AbstractJsonType()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.nemerosa.ontrack.model.json.schema

abstract class AbstractJsonNamedType(
val title: String,
type: String,
description: String?,
) : AbstractJsonBaseType(type, description)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package net.nemerosa.ontrack.model.json.schema

abstract class AbstractJsonType : JsonType
Loading

0 comments on commit 44c6acd

Please sign in to comment.