From b6c4d59a1dba7375b142313e4ba5586888751e9d Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 9 Jul 2024 21:04:09 +0200 Subject: [PATCH] feat(model-server): add endpoint /about Show the server version in the endpoint. Link to the endpoint in the title bar. --- build-logic/build.gradle.kts | 26 ++++++ .../kotlin/org/modelix/GenerateVersion.kt | 79 +++++++++++++++++ build.gradle.kts | 1 - bulk-model-sync-lib/build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 + .../model/server/light/LightModelServer.kt | 4 +- .../model-server-operative.yaml | 26 ++++++ model-server/build.gradle.kts | 3 + .../kotlin/org/modelix/model/server/Main.kt | 10 ++- .../model/server/handlers/AboutApiImpl.kt | 35 ++++++++ .../model/server/templates/PageWithMenuBar.kt | 1 + .../model/server/ModelServerTestUtil.kt | 6 +- .../model/server/handlers/AboutApiTest.kt | 87 +++++++++++++++++++ .../modelix/modelql/client/ModelQLClient.kt | 4 +- modelql-core/build.gradle.kts | 22 +---- .../modelix/modelql/core/VersionAndData.kt | 4 +- .../modelix/modelql/server/ModelQLServer.kt | 4 +- 17 files changed, 282 insertions(+), 34 deletions(-) create mode 100644 build-logic/src/main/kotlin/org/modelix/GenerateVersion.kt create mode 100644 model-server/src/main/kotlin/org/modelix/model/server/handlers/AboutApiImpl.kt create mode 100644 model-server/src/test/kotlin/org/modelix/model/server/handlers/AboutApiTest.kt diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index bc0172f0f0..2565a29e02 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,3 +1,29 @@ plugins { `kotlin-dsl` } + +dependencies { + // This should ideally be `compileOnly` and not `implementation`. + // The compiled `build-logic` should not bundle the plugin code. + // The project applying code from "build-logic" should add the plugins themselves. + // + // But using `compileOnly` we run into https://github.com/gradle/gradle/issues/23709 + // This is indirectly related to applying any settings plugin in our root "settings.gradle.kts". + // ``` + // plugins { + // id("modelix-repositories") + // } + // ``` + // Applying any settings plugin causes an `InstrumentingVisitableURLClassLoader` to be added as class loader. + // It results in throwing "java.lang.NoClassDefFoundError: org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper`. + // + // We therefore use `implementation` as a workaround and bundle the plugin code with "build-logic". + // + // Because we use `implementation` and not `compileOnly` only, + // they must not add any of the Kotlin Gradle plugins again in the applying projects. + // This means we must not call `alias(libs.plugins.kotlin.multiplatform)` + // and `alias(libs.plugins.kotlin.jvm), which tries to add them again as plugins. + // We just have to call `kotlin("multiplatform")` and `kotlin("jvm")` which just enables the plugins, + // but uses the plugin code bundled with `build-logic`. + implementation(libs.kotlin.gradlePlugin) +} diff --git a/build-logic/src/main/kotlin/org/modelix/GenerateVersion.kt b/build-logic/src/main/kotlin/org/modelix/GenerateVersion.kt new file mode 100644 index 0000000000..3c3d0f5960 --- /dev/null +++ b/build-logic/src/main/kotlin/org/modelix/GenerateVersion.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.modelix + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper + +/** + * Causes code containing the Modelix version to be generated and used in a Kolin project. + * The Kotlin project can be a JVM oder Multiplatform. + */ +fun Project.registerVersionGenerationTask(packageName: String) { + val packagePath = packageName.replace('.', '/') + + val generateVersionVariable = tasks.register("generateVersionVariable") { + doLast { + val fileContent = buildString { + appendLine("package $packageName") + appendLine() + appendLine("""const val MODELIX_VERSION: String = "$version"""") + if (project.name == "modelql-core") { + appendLine("""@Deprecated("Use the new MODELIX_VERSION", replaceWith = ReplaceWith("MODELIX_VERSION"))""") + appendLine("const val modelqlVersion: String = MODELIX_VERSION") + } + } + val outputDir = layout.buildDirectory.dir("version_gen/$packagePath").get().asFile + outputDir.mkdirs() + outputDir.resolve("Version.kt").writeText(fileContent) + } + } + + // Generate version constant for Kotlin JVM projects + plugins.withType { + extensions.configure { + sourceSets["main"].kotlin.srcDir(layout.buildDirectory.dir("version_gen")) + } + + tasks.withType().all { + dependsOn(generateVersionVariable) + } + } + + // Generate version constant for Kotlin Multiplatform projects + plugins.withType { + extensions.configure { + sourceSets.commonMain { + kotlin.srcDir(layout.buildDirectory.dir("version_gen")) + } + } + + tasks.withType().all { + dependsOn(generateVersionVariable) + } + + tasks.withType().all { + dependsOn(generateVersionVariable) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 434d310f77..3f865c46f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,6 @@ buildscript { plugins { `maven-publish` `version-catalog` - alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.gitVersion) alias(libs.plugins.spotless) apply false diff --git a/bulk-model-sync-lib/build.gradle.kts b/bulk-model-sync-lib/build.gradle.kts index 042096938c..8e02ebb2f4 100644 --- a/bulk-model-sync-lib/build.gradle.kts +++ b/bulk-model-sync-lib/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - alias(libs.plugins.kotlin.multiplatform) + kotlin("multiplatform") } kotlin { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5e8a7c303..bd86aa612f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ apache-cxf = ["apache-cxf-client", "apache-cxf-sse"] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } gitVersion = { id = "com.palantir.git-version", version = "3.1.0" } spotless = { id = "com.diffplug.spotless", version = "6.25.0" } modelix-mps-buildtools = { id = "org.modelix.mps.build-tools", version.ref = "modelixBuildtools" } @@ -49,6 +50,7 @@ kotlin-serialization-yaml = { group = "com.charleskorn.kaml", name = "kaml", ver kotlin-logging = { group = "io.github.microutils", name = "kotlin-logging", version = "3.0.5" } kotlin-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version = "0.3.7" } kotlin-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.6.0" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } kotlin-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinCoroutines" } diff --git a/model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt b/model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt index 87e380eb32..bad34c131f 100644 --- a/model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt +++ b/model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt @@ -67,7 +67,7 @@ import org.modelix.model.server.api.SetPropertyOpData import org.modelix.model.server.api.SetReferenceOpData import org.modelix.model.server.api.VersionData import org.modelix.model.server.api.buildModelQuery -import org.modelix.modelql.core.modelqlVersion +import org.modelix.modelql.core.MODELIX_VERSION import org.modelix.modelql.server.ModelQLServer import java.time.Duration import java.util.* @@ -202,7 +202,7 @@ class LightModelServer @JvmOverloads constructor(val port: Int, val rootNodeProv } } get("/version") { - call.respondText(modelqlVersion ?: "unknown") + call.respondText(MODELIX_VERSION ?: "unknown") } get("/health") { val output = StringBuilder() diff --git a/model-server-openapi/specifications/model-server-operative.yaml b/model-server-openapi/specifications/model-server-operative.yaml index 01a207fc9a..e9cd0b238b 100644 --- a/model-server-openapi/specifications/model-server-operative.yaml +++ b/model-server-openapi/specifications/model-server-operative.yaml @@ -39,6 +39,22 @@ paths: default: $ref: '#/components/responses/GeneralError' + /about: + get: + operationId: getAboutInformation + x-modelix-media-type-handlers: + - v1: + - 'application/x.modelix.about+json;version=1' + tags: + - about + responses: + "200": + description: Response with information about the model server. + content: + 'application/x.modelix.about+json;version=1': + schema: + $ref: "#/components/schemas/AboutV1" + components: responses: Healthy: @@ -110,3 +126,13 @@ components: e.g. by adding a fragment identifier or sub-path to the problem type. May be used to locate the root of this problem in the source code. example: '/some/uri-reference#specific-occurrence-context' + + AboutV1: + x-modelix-media-type: 'application/x.modelix.about+json;version=1' + description: Information about the model server + type: object + properties: + version: + type: string + required: + - version diff --git a/model-server/build.gradle.kts b/model-server/build.gradle.kts index a11d7ad977..053e485cd2 100644 --- a/model-server/build.gradle.kts +++ b/model-server/build.gradle.kts @@ -1,6 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer import io.gitlab.arturbosch.detekt.Detekt +import org.modelix.registerVersionGenerationTask import org.openapitools.generator.gradle.plugin.tasks.GenerateTask plugins { @@ -270,3 +271,5 @@ openApiFiles.forEach { tasks.withType { exclude("**/org/modelix/api/**") } + +project.registerVersionGenerationTask("org.modelix.model.server") diff --git a/model-server/src/main/kotlin/org/modelix/model/server/Main.kt b/model-server/src/main/kotlin/org/modelix/model/server/Main.kt index d8a3f5cd6e..71e45bad68 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/Main.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/Main.kt @@ -48,11 +48,11 @@ import kotlinx.serialization.json.Json import org.apache.commons.io.FileUtils import org.apache.ignite.Ignition import org.modelix.api.v1.Problem -import org.modelix.api.v2.Paths.registerJsonTypes import org.modelix.authorization.ModelixAuthorization import org.modelix.authorization.NoPermissionException import org.modelix.authorization.NotLoggedInException import org.modelix.model.InMemoryModels +import org.modelix.model.server.handlers.AboutApiImpl import org.modelix.model.server.handlers.HealthApiImpl import org.modelix.model.server.handlers.HttpException import org.modelix.model.server.handlers.IdsApiImpl @@ -79,6 +79,8 @@ import java.io.IOException import java.nio.charset.StandardCharsets import java.time.Duration import javax.sql.DataSource +import org.modelix.api.operative.Paths.registerJsonTypes as registerJsonTypesOperative +import org.modelix.api.v2.Paths.registerJsonTypes as registerJsonTypesV2 object Main { private val LOG = LoggerFactory.getLogger(Main::class.java) @@ -95,6 +97,7 @@ object Main { return } + LOG.info("Version: $MODELIX_VERSION") LOG.info("Max memory (bytes): ${Runtime.getRuntime().maxMemory()}") LOG.info("Server process started") LOG.info("In memory: ${cmdLineArgs.inmemory}") @@ -196,7 +199,8 @@ object Main { } install(ContentNegotiation) { json() - registerJsonTypes() + registerJsonTypesV2() + registerJsonTypesOperative() } install(CORS) { anyHost() @@ -219,7 +223,7 @@ object Main { routing { HealthApiImpl(repositoriesManager, globalStoreClient, inMemoryModels).installRoutes(this) - + AboutApiImpl().installRoutes(this) staticResources("/public", "public") if (cmdLineArgs.noSwaggerUi) { diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/AboutApiImpl.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/AboutApiImpl.kt new file mode 100644 index 0000000000..c8ed696778 --- /dev/null +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/AboutApiImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.modelix.model.server.handlers + +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.response.respond +import io.ktor.util.pipeline.PipelineContext +import org.modelix.api.operative.AboutApi +import org.modelix.api.operative.AboutV1 +import org.modelix.model.server.MODELIX_VERSION + +/** + * Responding information about the model server. + */ +class AboutApiImpl : AboutApi() { + override suspend fun PipelineContext.getAboutInformationV1() { + val about = AboutV1(MODELIX_VERSION) + call.respond(about) + } +} diff --git a/model-server/src/main/kotlin/org/modelix/model/server/templates/PageWithMenuBar.kt b/model-server/src/main/kotlin/org/modelix/model/server/templates/PageWithMenuBar.kt index dc7f10ff39..e96eb3952f 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/templates/PageWithMenuBar.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/templates/PageWithMenuBar.kt @@ -42,6 +42,7 @@ class PageWithMenuBar(val activePage: String, val baseUrl: String) : Template() shouldBe AboutV1(MODELIX_VERSION) + response.shouldHaveContentType(aboutV1ContentType) + } + + @Test + fun getAboutInformationWithJsonContentType() = testApplication { + val client = setupApplication() + + val response = client.get("/about") + + response shouldHaveStatus HttpStatusCode.OK + response.body() shouldBe AboutV1(MODELIX_VERSION) + response.shouldHaveContentType(ContentType.Application.Json.withCharset(Charsets.UTF_8)) + } +} diff --git a/modelql-client/src/commonMain/kotlin/org/modelix/modelql/client/ModelQLClient.kt b/modelql-client/src/commonMain/kotlin/org/modelix/modelql/client/ModelQLClient.kt index ce0a25bafc..5d4bd28dd9 100644 --- a/modelql-client/src/commonMain/kotlin/org/modelix/modelql/client/ModelQLClient.kt +++ b/modelql-client/src/commonMain/kotlin/org/modelix/modelql/client/ModelQLClient.kt @@ -26,11 +26,11 @@ import org.modelix.model.api.INodeReference import org.modelix.model.area.IArea import org.modelix.modelql.core.IMonoStep import org.modelix.modelql.core.IUnboundQuery +import org.modelix.modelql.core.MODELIX_VERSION import org.modelix.modelql.core.SerializationContext import org.modelix.modelql.core.UnboundQuery import org.modelix.modelql.core.VersionAndData import org.modelix.modelql.core.castToInstance -import org.modelix.modelql.core.modelqlVersion import org.modelix.modelql.untyped.UntypedModelQL import org.modelix.modelql.untyped.query @@ -82,7 +82,7 @@ class ModelQLClient(val url: String, val client: HttpClient, includedSerializers LOG.debug { "result: $text" } return text } - else -> throw RuntimeException("Query failed : $query \nclient version: $modelqlVersion\n${response.status}\n${response.bodyAsText()}") + else -> throw RuntimeException("Query failed : $query \nclient version: $MODELIX_VERSION\n${response.status}\n${response.bodyAsText()}") } } diff --git a/modelql-core/build.gradle.kts b/modelql-core/build.gradle.kts index 7ad694bd5b..a7fe74d8f0 100644 --- a/modelql-core/build.gradle.kts +++ b/modelql-core/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.modelix.registerVersionGenerationTask /* * Licensed under the Apache License, Version 2.0 (the "License"); @@ -76,28 +77,11 @@ kotlin { } } -val generateVersionVariable by tasks.registering { - doLast { - val outputDir = project.layout.buildDirectory.dir("version_gen/org/modelix/modelql/core").get().asFile - outputDir.mkdirs() - outputDir.resolve("Version.kt").writeText( - """ - package org.modelix.modelql.core - - const val modelqlVersion: String = "$version" - - """.trimIndent(), - ) - } -} - tasks.withType().all { - dependsOn(generateVersionVariable) compilerOptions { jvmTarget.set(JvmTarget.JVM_11) freeCompilerArgs.add("-Xjvm-default=all-compatibility") } } -tasks.withType().all { - dependsOn(generateVersionVariable) -} + +project.registerVersionGenerationTask("org.modelix.modelql.core") diff --git a/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/VersionAndData.kt b/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/VersionAndData.kt index cb07dec87d..f19851c088 100644 --- a/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/VersionAndData.kt +++ b/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/VersionAndData.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.json.JsonPrimitive @Serializable class VersionAndData(val data: E, val version: String?) { - constructor(data: E) : this(data, modelqlVersion) + constructor(data: E) : this(data, MODELIX_VERSION) companion object { private val LOG = mu.KotlinLogging.logger { } fun readVersionOnly(text: String): String? { @@ -41,7 +41,7 @@ class VersionAndData(val data: E, val version: String?) { ) } catch (ex: Exception) { val actualVersion = readVersionOnly(serializedJson) - val expectedVersion = modelqlVersion + val expectedVersion = MODELIX_VERSION if (actualVersion != expectedVersion) { throw RuntimeException("Deserialization failed. Version $expectedVersion expected, but was $actualVersion", ex) } else { diff --git a/modelql-server/src/main/kotlin/org/modelix/modelql/server/ModelQLServer.kt b/modelql-server/src/main/kotlin/org/modelix/modelql/server/ModelQLServer.kt index 412b9378f4..5246d10f46 100644 --- a/modelql-server/src/main/kotlin/org/modelix/modelql/server/ModelQLServer.kt +++ b/modelql-server/src/main/kotlin/org/modelix/modelql/server/ModelQLServer.kt @@ -27,10 +27,10 @@ import org.modelix.model.api.INode import org.modelix.model.area.IArea import org.modelix.modelql.core.IMonoUnboundQuery import org.modelix.modelql.core.IStepOutput +import org.modelix.modelql.core.MODELIX_VERSION import org.modelix.modelql.core.QueryGraphDescriptor import org.modelix.modelql.core.SerializationContext import org.modelix.modelql.core.VersionAndData -import org.modelix.modelql.core.modelqlVersion import org.modelix.modelql.core.upcast import org.modelix.modelql.untyped.UntypedModelQL import org.modelix.modelql.untyped.createQueryExecutor @@ -110,7 +110,7 @@ class ModelQLServer private constructor(val rootNodeProvider: () -> INode?, val } catch (ex: Throwable) { afterQueryExecution() call.respondText( - text = "server version: $modelqlVersion\n" + ex.stackTraceToString(), + text = "server version: $MODELIX_VERSION\n" + ex.stackTraceToString(), status = HttpStatusCode.InternalServerError, ) }