From 2c938cb20322b4c630f3c9098fae01e6c12e27f6 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:19:56 +0200 Subject: [PATCH 01/65] feat: gradle publishing to dev repo --- .github/workflows/release-dev.yml | 5 +++++ build.gradle.kts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 4c85976..d6ae809 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -44,6 +44,11 @@ jobs: id: commit_hash run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - name: Publish to SimpleCloud Repository + run: ./gradlew publishAllPublicationsToSimplecloudRepository + env: + COMMIT_HASH: ${{ env.COMMIT_HASH }} + - name: Create Release id: create_release uses: actions/create-release@v1 diff --git a/build.gradle.kts b/build.gradle.kts index 9eaec1f..a2b0a8a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,9 +29,31 @@ subprojects { } publishing { + repositories { + maven { + name = "simplecloud" + url = uri("https://repo.simplecloud.app/snapshots/") + credentials { + username = (project.findProperty("simplecloudUsername") as? String)?: System.getenv("SIMPLECLOUD_USERNAME") + password = (project.findProperty("simplecloudPassword") as? String)?: System.getenv("SIMPLECLOUD_PASSWORD") + } + authentication { + create("basic") + } + } + } + publications { + // Not publish controller-runtime + if (project.name == "controller-runtime") { + return@publications + } + create("mavenJava") { from(components["java"]) + + val commitHash = System.getenv("COMMIT_HASH")?: return@create + version = "${project.version}-dev.$commitHash" } } } From 3272b999fadb61fe17d8c4f4ac745fe535a3e3e5 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:24:00 +0200 Subject: [PATCH 02/65] fix: gradle publication --- .github/workflows/release-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index d6ae809..f06240c 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -45,7 +45,7 @@ jobs: run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Publish to SimpleCloud Repository - run: ./gradlew publishAllPublicationsToSimplecloudRepository + run: ./gradlew publishMavenJavaPublicationToSimplecloudRepository env: COMMIT_HASH: ${{ env.COMMIT_HASH }} From b6a99869cd3824b9113a014e1d5e05b4a50ac3d8 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:30:50 +0200 Subject: [PATCH 03/65] fix: gradle publication --- build.gradle.kts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a2b0a8a..c4c9922 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,9 +7,13 @@ plugins { `maven-publish` } +val baseVersion = "0.0.30" +val commitHash = System.getenv("COMMIT_HASH") +val snapshotversion = "${baseVersion}-dev.$commitHash" + allprojects { group = "app.simplecloud.controller" - version = "0.0.30" + version = if (commitHash != null) snapshotversion else baseVersion repositories { mavenCentral() @@ -51,9 +55,6 @@ subprojects { create("mavenJava") { from(components["java"]) - - val commitHash = System.getenv("COMMIT_HASH")?: return@create - version = "${project.version}-dev.$commitHash" } } } From 5d47331a37b49de0729d7af02e349c19b598ff4f Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:34:35 +0200 Subject: [PATCH 04/65] fix: gradle publication signing --- build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index c4c9922..1704a99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -114,6 +114,10 @@ subprojects { } signing { + if (commitHash == null) { + return@signing + } + sign(publishing.publications) useGpgCmd() } From 32f1d7dae4eae9f0adb3aabdf6a31db854e3fcb9 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:37:26 +0200 Subject: [PATCH 05/65] fix: gradle publication signing --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1704a99..0da2ac2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -114,7 +114,7 @@ subprojects { } signing { - if (commitHash == null) { + if (commitHash != null) { return@signing } From 7a0e3660ce186c91bfa9fbe401b4a96f5cce1807 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:41:18 +0200 Subject: [PATCH 06/65] fix: gradle publication credentials --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0da2ac2..290ef95 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,8 +38,8 @@ subprojects { name = "simplecloud" url = uri("https://repo.simplecloud.app/snapshots/") credentials { - username = (project.findProperty("simplecloudUsername") as? String)?: System.getenv("SIMPLECLOUD_USERNAME") - password = (project.findProperty("simplecloudPassword") as? String)?: System.getenv("SIMPLECLOUD_PASSWORD") + username = System.getenv("SIMPLECLOUD_USERNAME")?: (project.findProperty("simplecloudUsername") as? String) + password = System.getenv("SIMPLECLOUD_PASSWORD")?: (project.findProperty("simplecloudPassword") as? String) } authentication { create("basic") From bf5da83c7ea02b20261738bca519180b8abdd4a7 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 14 Oct 2024 11:43:48 +0200 Subject: [PATCH 07/65] fix: gradle publication workflow credentials --- .github/workflows/release-dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index f06240c..95e0569 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -48,6 +48,8 @@ jobs: run: ./gradlew publishMavenJavaPublicationToSimplecloudRepository env: COMMIT_HASH: ${{ env.COMMIT_HASH }} + SIMPLECLOUD_USERNAME: ${{ secrets.SIMPLECLOUD_USERNAME }} + SIMPLECLOUD_PASSWORD: ${{ secrets.SIMPLECLOUD_PASSWORD }} - name: Create Release id: create_release From 73f0d06304e7b3e0cd8ea238d7c7b41d4ebc0f93 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 21 Oct 2024 01:25:21 +0200 Subject: [PATCH 08/65] refactor: make stub for ServerHost optional --- .../controller/runtime/host/ServerHostRepository.kt | 6 +++--- .../simplecloud/controller/runtime/server/ServerService.kt | 6 +++--- .../app/simplecloud/controller/shared/host/ServerHost.kt | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostRepository.kt index 468d17e..bb4d857 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostRepository.kt @@ -28,7 +28,7 @@ class ServerHostRepository : Repository { suspend fun areServerHostsAvailable(): Boolean { return coroutineScope { return@coroutineScope hosts.any { - val channel = it.value.stub.channel as ManagedChannel + val channel = it.value.stub?.channel as ManagedChannel val state = channel.getState(true) state == ConnectivityState.IDLE || state == ConnectivityState.READY } @@ -36,8 +36,8 @@ class ServerHostRepository : Repository { } override suspend fun delete(element: ServerHost): Boolean { - val host = hosts.get(element.id) ?: return false - (host.stub.channel as ManagedChannel).shutdown().awaitTermination(5L, TimeUnit.SECONDS) + val host = hosts[element.id] ?: return false + (host.stub?.channel as ManagedChannel).shutdown().awaitTermination(5L, TimeUnit.SECONDS) return hosts.remove(element.id, element) } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 4b44ef1..b57f175 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -42,7 +42,7 @@ class ServerService( serverRepository.findServersByHostId(serverHost.id).forEach { server -> logger.info("Reattaching Server ${server.uniqueId} of group ${server.group}...") try { - val result = serverHost.stub.reattachServer(server.toDefinition()) + val result = serverHost.stub?.reattachServer(server.toDefinition()) ?: throw StatusException(Status.INTERNAL.withDescription("Could not reattach server, is the host misconfigured?")) serverRepository.save(Server.fromDefinition(result)) logger.info("Success!") } catch (e: Exception) { @@ -163,7 +163,7 @@ class ServerService( val numericalId = numericalIdRepository.findNextNumericalId(group.name) val server = buildServer(group, numericalId, forwardingSecret) serverRepository.save(server) - val stub = host.stub + val stub = host.stub ?: throw StatusException(Status.INTERNAL.withDescription("Server host has no stub")) serverRepository.save(server) try { val result = stub.startServer( @@ -220,7 +220,7 @@ class ServerService( ?: throw Status.NOT_FOUND .withDescription("No server host was found matching this server.") .asRuntimeException() - val stub = host.stub + val stub = host.stub ?: throw StatusException(Status.INTERNAL.withDescription("Server host has no stub")) try { val stopped = stub.stopServer(server) pubSubClient.publish( diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt index d5aaefd..5e52a29 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt @@ -5,14 +5,12 @@ import build.buf.gen.simplecloud.controller.v1.ServerHostDefinition import build.buf.gen.simplecloud.controller.v1.ServerHostServiceGrpcKt import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder -import org.spongepowered.configurate.objectmapping.ConfigSerializable -@ConfigSerializable data class ServerHost( val id: String, val host: String, val port: Int, - val stub: ServerHostServiceGrpcKt.ServerHostServiceCoroutineStub, + val stub: ServerHostServiceGrpcKt.ServerHostServiceCoroutineStub? = null, ) { fun toDefinition(): ServerHostDefinition { From 5c086f7d0b7d8fa1fc004fb7fca05068a1f3f780 Mon Sep 17 00:00:00 2001 From: Kaseax Date: Mon, 21 Oct 2024 23:06:03 +0200 Subject: [PATCH 09/65] refactor: migrate forwarding secret --- .../simplecloud/controller/runtime/ControllerRuntime.kt | 1 - .../controller/runtime/server/ServerService.kt | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 36ad827..92518c9 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -110,7 +110,6 @@ class ControllerRuntime( serverRepository, hostRepository, groupRepository, - controllerStartCommand.forwardingSecret, authCallCredentials, PubSubClient(controllerStartCommand.grpcHost, controllerStartCommand.pubSubGrpcPort, authCallCredentials) ) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index b57f175..5f7191f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -21,7 +21,6 @@ class ServerService( private val serverRepository: ServerRepository, private val hostRepository: ServerHostRepository, private val groupRepository: GroupRepository, - private val forwardingSecret: String, private val authCallCredentials: AuthCallCredentials, private val pubSubClient: PubSubClient, ) : ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineImplBase() { @@ -161,7 +160,7 @@ class ServerService( private suspend fun startServer(host: ServerHost, group: Group): ServerDefinition { val numericalId = numericalIdRepository.findNextNumericalId(group.name) - val server = buildServer(group, numericalId, forwardingSecret) + val server = buildServer(group, numericalId) serverRepository.save(server) val stub = host.stub ?: throw StatusException(Status.INTERNAL.withDescription("Server host has no stub")) serverRepository.save(server) @@ -182,7 +181,7 @@ class ServerService( } } - private fun buildServer(group: Group, numericalId: Int, forwardingSecret: String): Server { + private fun buildServer(group: Group, numericalId: Int): Server { return Server.fromDefinition( ServerDefinition.newBuilder() .setNumericalId(numericalId) @@ -197,8 +196,7 @@ class ServerService( .setPlayerCount(0) .setUniqueId(UUID.randomUUID().toString().replace("-", "")).putAllCloudProperties( mapOf( - *group.properties.entries.map { it.key to it.value }.toTypedArray(), - "forwarding-secret" to forwardingSecret + *group.properties.entries.map { it.key to it.value }.toTypedArray() ) ).build() ) From 83e4dcf5a2c1e0539f00033ed76bb862f4d4118d Mon Sep 17 00:00:00 2001 From: David Date: Fri, 25 Oct 2024 22:14:48 +0200 Subject: [PATCH 10/65] fix: make get group by name not return empty response --- .../app/simplecloud/controller/runtime/group/GroupService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt index a57f9ab..201933a 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt @@ -12,7 +12,7 @@ class GroupService( override suspend fun getGroupByName(request: GetGroupByNameRequest): GetGroupByNameResponse { val group = groupRepository.find(request.groupName) ?: throw StatusException(Status.NOT_FOUND.withDescription("This group does not exist")) - return getGroupByNameResponse { group.toDefinition() } + return getGroupByNameResponse { this.group = group.toDefinition() } } override suspend fun getAllGroups(request: GetAllGroupsRequest): GetAllGroupsResponse { From d654d1f502acabc3979d10f30e2ae372983cd8d5 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 31 Oct 2024 20:36:34 +0100 Subject: [PATCH 11/65] refactor: make group properties mutable --- .../kotlin/app/simplecloud/controller/shared/group/Group.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt index 034784a..3d47a07 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt @@ -15,7 +15,7 @@ data class Group( val maxOnlineCount: Long = 0, val maxPlayers: Long = 0, val newServerPlayerRatio: Long = -1, - val properties: Map = mapOf() + val properties: MutableMap = mutableMapOf() ) { fun toDefinition(): GroupDefinition { From cb8e83bfd0c9a333b2b9a68abb26ef9077d76f62 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 31 Oct 2024 20:38:55 +0100 Subject: [PATCH 12/65] refactor: revert group property mutability --- .../kotlin/app/simplecloud/controller/shared/group/Group.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt index 3d47a07..5346678 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt @@ -15,7 +15,7 @@ data class Group( val maxOnlineCount: Long = 0, val maxPlayers: Long = 0, val newServerPlayerRatio: Long = -1, - val properties: MutableMap = mutableMapOf() + val properties: Map = mutableMapOf() ) { fun toDefinition(): GroupDefinition { From 6b9d2ab3c85147a9e30fb7424df8f0fb3a7af429 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Fri, 8 Nov 2024 15:24:01 +0100 Subject: [PATCH 13/65] refactor: bump proto specs --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 600fb9c..0868795 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ log4j = "2.20.0" protobuf = "3.25.2" grpc = "1.61.0" grpcKotlin = "1.4.1" -simpleCloudProtoSpecs = "1.4.1.1.20241001163139.58018cb317ed" +simpleCloudProtoSpecs = "1.4.1.1.20241108142018.a431612a8826" simpleCloudPubSub = "1.0.5" jooq = "3.19.3" configurate = "4.1.2" From 414266d4712520ed661149592f95e5639b7a8ad2 Mon Sep 17 00:00:00 2001 From: Philipp Date: Sat, 9 Nov 2024 10:56:05 +0100 Subject: [PATCH 14/65] feat: track metrics --- build.gradle.kts | 1 + controller-runtime/build.gradle.kts | 1 + .../runtime/launcher/ControllerStartCommand.kt | 16 ++++++++++++++-- .../controller/runtime/launcher/Launcher.kt | 15 +++++++++++---- gradle/libs.versions.toml | 2 ++ 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 290ef95..75cb55e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ allprojects { repositories { mavenCentral() maven("https://buf.build/gen/maven") + maven("https://repo.simplecloud.app/snapshots") } } diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts index 1a6d03e..68796b3 100644 --- a/controller-runtime/build.gradle.kts +++ b/controller-runtime/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { api(rootProject.libs.bundles.jooq) api(rootProject.libs.sqliteJdbc) jooqCodegen(rootProject.libs.jooqMetaExtensions) + implementation(rootProject.libs.simplecloud.metrics) implementation(rootProject.libs.bundles.log4j) implementation(rootProject.libs.clikt) implementation(rootProject.libs.spotifyCompletableFutures) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 505fde3..0478f85 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -2,18 +2,22 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.controller.runtime.ControllerRuntime import app.simplecloud.controller.shared.secret.AuthFileSecretFactory +import app.simplecloud.metrics.internal.api.MetricsCollector import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.defaultLazy import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.sources.PropertiesValueSource import java.io.File import java.nio.file.Path -class ControllerStartCommand : CliktCommand() { +class ControllerStartCommand( + private val metricsCollector: MetricsCollector? +) : CliktCommand() { init { context { @@ -45,6 +49,10 @@ class ControllerStartCommand : CliktCommand() { val authSecret: String by option(help = "Auth secret", envvar = "AUTH_SECRET_KEY") .defaultLazy { AuthFileSecretFactory.loadOrCreate(authSecretPath) } + val trackMetrics: Boolean by option(help = "Track metrics", envvar = "TRACK_METRICS") + .boolean() + .default(true) + private val forwardingSecretPath: Path by option( help = "Path to the forwarding secret (default: .forwarding.secret)", envvar = "FORWARDING_SECRET_PATH" @@ -52,10 +60,14 @@ class ControllerStartCommand : CliktCommand() { .path() .default(Path.of(".secrets", "forwarding.secret")) - val forwardingSecret: String by option(help = "Forwarding secrewt", envvar = "FORWARDING_SECRET") + val forwardingSecret: String by option(help = "Forwarding secret", envvar = "FORWARDING_SECRET") .defaultLazy { AuthFileSecretFactory.loadOrCreate(forwardingSecretPath) } override fun run() { + if (trackMetrics) { + metricsCollector?.start() + } + val controllerRuntime = ControllerRuntime(this) controllerRuntime.start() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt index 10c2f99..d5cca11 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt @@ -1,16 +1,23 @@ package app.simplecloud.controller.runtime.launcher +import app.simplecloud.metrics.internal.api.MetricsCollector import org.apache.logging.log4j.LogManager -fun main(args: Array) { - configureLog4j() - ControllerStartCommand().main(args) +suspend fun main(args: Array) { + val metricsCollector = try { + MetricsCollector.create("controller") + } catch (e: Exception) { + null + } + configureLog4j(metricsCollector) + ControllerStartCommand(metricsCollector).main(args) } -fun configureLog4j() { +fun configureLog4j(metricsCollector: MetricsCollector?) { val globalExceptionHandlerLogger = LogManager.getLogger("GlobalExceptionHandler") Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + metricsCollector?.recordError(throwable) globalExceptionHandlerLogger.error("Uncaught exception in thread ${thread.name}", throwable) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0868795..0d33f7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ grpc = "1.61.0" grpcKotlin = "1.4.1" simpleCloudProtoSpecs = "1.4.1.1.20241108142018.a431612a8826" simpleCloudPubSub = "1.0.5" +simplecloud-metrics = "1.0.0" jooq = "3.19.3" configurate = "4.1.2" sqliteJdbc = "3.44.1.0" @@ -33,6 +34,7 @@ grpcNettyShaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } simpleCloudProtoSpecs = { module = "build.buf.gen:simplecloud_proto-specs_grpc_kotlin", version.ref = "simpleCloudProtoSpecs" } simpleCloudPubSub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simpleCloudPubSub" } +simplecloud-metrics = { module = "app.simplecloud:internal-metrics-api", version.ref = "simplecloud-metrics" } qooq = { module = "org.jooq:jooq-kotlin", version.ref = "jooq" } qooqMeta = { module = "org.jooq:jooq-meta", version.ref = "jooq" } From 5f4735d5ea2f0fbd1ec48eb759ae5d18c7bbfac2 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 11 Nov 2024 15:10:23 +0100 Subject: [PATCH 15/65] refactor: remove unused arguments --- .../runtime/launcher/ControllerStartCommand.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 505fde3..a158852 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -45,15 +45,6 @@ class ControllerStartCommand : CliktCommand() { val authSecret: String by option(help = "Auth secret", envvar = "AUTH_SECRET_KEY") .defaultLazy { AuthFileSecretFactory.loadOrCreate(authSecretPath) } - private val forwardingSecretPath: Path by option( - help = "Path to the forwarding secret (default: .forwarding.secret)", - envvar = "FORWARDING_SECRET_PATH" - ) - .path() - .default(Path.of(".secrets", "forwarding.secret")) - - val forwardingSecret: String by option(help = "Forwarding secrewt", envvar = "FORWARDING_SECRET") - .defaultLazy { AuthFileSecretFactory.loadOrCreate(forwardingSecretPath) } override fun run() { val controllerRuntime = ControllerRuntime(this) From b02c5aec2bf8fa16d8004e4299cbf5de970b0d60 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 11 Nov 2024 15:13:22 +0100 Subject: [PATCH 16/65] refactor: readd import --- .../controller/runtime/launcher/ControllerStartCommand.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 53f871e..2c723d1 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -2,6 +2,7 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.controller.runtime.ControllerRuntime import app.simplecloud.controller.shared.secret.AuthFileSecretFactory +import app.simplecloud.metrics.internal.api.MetricsCollector import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.parameters.options.default From fc16c1bc67cc158a0a5be077f3d749035be4a44d Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 11 Nov 2024 15:14:34 +0100 Subject: [PATCH 17/65] refactor: remove unused arguments --- .../runtime/launcher/ControllerStartCommand.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 2c723d1..5a5852a 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -53,16 +53,6 @@ class ControllerStartCommand( .boolean() .default(true) - private val forwardingSecretPath: Path by option( - help = "Path to the forwarding secret (default: .forwarding.secret)", - envvar = "FORWARDING_SECRET_PATH" - ) - .path() - .default(Path.of(".secrets", "forwarding.secret")) - - val forwardingSecret: String by option(help = "Forwarding secrewt", envvar = "FORWARDING_SECRET") - .defaultLazy { AuthFileSecretFactory.loadOrCreate(forwardingSecretPath) } - override fun run() { if (trackMetrics) { metricsCollector?.start() From 8f2298c7d2725742e00ce8f5a202db8ffee3f85d Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 11 Nov 2024 15:15:19 +0100 Subject: [PATCH 18/65] refactor: make arg private --- .../controller/runtime/launcher/ControllerStartCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 5a5852a..97973d4 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -49,7 +49,7 @@ class ControllerStartCommand( val authSecret: String by option(help = "Auth secret", envvar = "AUTH_SECRET_KEY") .defaultLazy { AuthFileSecretFactory.loadOrCreate(authSecretPath) } - val trackMetrics: Boolean by option(help = "Track metrics", envvar = "TRACK_METRICS") + private val trackMetrics: Boolean by option(help = "Track metrics", envvar = "TRACK_METRICS") .boolean() .default(true) From 23609dd6eb49ef35395908e0571d93c796b6dad4 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 12 Nov 2024 17:09:51 +0100 Subject: [PATCH 19/65] feat: working oauth server with client registration --- build.gradle.kts | 6 +- controller-runtime/build.gradle.kts | 19 +- controller-runtime/src/main/db/schema.sql | 18 +- .../controller/runtime/ControllerRuntime.kt | 19 +- .../launcher/ControllerStartCommand.kt | 7 +- .../runtime/oauth/AuthClientRepository.kt | 78 ++++++++ .../runtime/oauth/AuthTokenRepository.kt | 84 ++++++++ .../controller/runtime/oauth/JwtHandler.kt | 41 ++++ .../controller/runtime/oauth/OAuthClient.kt | 9 + .../controller/runtime/oauth/OAuthServer.kt | 180 ++++++++++++++++++ .../controller/runtime/oauth/OAuthToken.kt | 9 + .../controller/runtime/oauth/PKCEHandler.kt | 18 ++ controller-shared/build.gradle.kts | 4 +- gradle/libs.versions.toml | 109 ++++++----- 14 files changed, 541 insertions(+), 60 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PKCEHandler.kt diff --git a/build.gradle.kts b/build.gradle.kts index 75cb55e..486ecae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { alias(libs.plugins.kotlin) alias(libs.plugins.shadow) - alias(libs.plugins.sonatypeCentralPortalPublisher) + alias(libs.plugins.sonatype.central.portal.publisher) `maven-publish` } @@ -29,8 +29,8 @@ subprojects { apply(plugin = "maven-publish") dependencies { - testImplementation(rootProject.libs.kotlinTest) - implementation(rootProject.libs.kotlinJvm) + testImplementation(rootProject.libs.kotlin.test) + implementation(rootProject.libs.kotlin.jvm) } publishing { diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts index 68796b3..165b642 100644 --- a/controller-runtime/build.gradle.kts +++ b/controller-runtime/build.gradle.kts @@ -1,17 +1,20 @@ plugins { application - alias(libs.plugins.jooqCodegen) + alias(libs.plugins.jooq.codegen) + java } dependencies { api(project(":controller-shared")) - api(rootProject.libs.bundles.jooq) - api(rootProject.libs.sqliteJdbc) - jooqCodegen(rootProject.libs.jooqMetaExtensions) - implementation(rootProject.libs.simplecloud.metrics) - implementation(rootProject.libs.bundles.log4j) - implementation(rootProject.libs.clikt) - implementation(rootProject.libs.spotifyCompletableFutures) + api(libs.bundles.jooq) + api(libs.sqlite.jdbc) + jooqCodegen(libs.jooq.meta.extensions) + implementation(libs.simplecloud.metrics) + implementation(libs.bundles.log4j) + implementation(libs.clikt) + implementation(libs.spotify.completablefutures) + implementation(libs.bundles.ktor) + implementation(libs.nimbus.jose.jwt) } application { diff --git a/controller-runtime/src/main/db/schema.sql b/controller-runtime/src/main/db/schema.sql index d779577..869724f 100644 --- a/controller-runtime/src/main/db/schema.sql +++ b/controller-runtime/src/main/db/schema.sql @@ -25,4 +25,20 @@ CREATE TABLE IF NOT EXISTS cloud_server_properties( key varchar NOT NULL, value varchar, CONSTRAINT compound_key PRIMARY KEY (server_id, key) -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS oauth2_client_details( + client_id varchar PRIMARY KEY, + client_secret varchar, + redirect_uri varchar, + grant_types varchar, + scope varchar +); + +CREATE TABLE IF NOT EXISTS oauth2_tokens( + token_id varchar PRIMARY KEY, + client_id varchar, + access_token varchar, + scope varchar, + expires_in timestamp +) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 92518c9..fd2e1ca 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -5,6 +5,7 @@ import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.group.GroupService import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.runtime.launcher.ControllerStartCommand +import app.simplecloud.controller.runtime.oauth.OAuthServer import app.simplecloud.controller.runtime.reconciler.Reconciler import app.simplecloud.controller.runtime.server.ServerNumericalIdRepository import app.simplecloud.controller.runtime.server.ServerRepository @@ -34,6 +35,7 @@ class ControllerRuntime( private val serverRepository = ServerRepository(database, numericalIdRepository) private val hostRepository = ServerHostRepository() private val pubSubService = PubSubService() + private val authServer = OAuthServer(controllerStartCommand, database) private val reconciler = Reconciler( groupRepository, serverRepository, @@ -47,6 +49,7 @@ class ControllerRuntime( fun start() { setupDatabase() + startAuthServer() startPubSubGrpcServer() startGrpcServer() startReconciler() @@ -54,6 +57,15 @@ class ControllerRuntime( loadServers() } + private fun startAuthServer() { + logger.info("Starting auth server...") + thread { + authServer.start() + logger.info("Auth server stopped.") + } + + } + private fun setupDatabase() { logger.info("Setting up database...") database.setup() @@ -74,6 +86,7 @@ class ControllerRuntime( thread { server.start() server.awaitTermination() + logger.info("GRPC Server stopped.") } } @@ -111,7 +124,11 @@ class ControllerRuntime( hostRepository, groupRepository, authCallCredentials, - PubSubClient(controllerStartCommand.grpcHost, controllerStartCommand.pubSubGrpcPort, authCallCredentials) + PubSubClient( + controllerStartCommand.grpcHost, + controllerStartCommand.pubSubGrpcPort, + authCallCredentials + ) ) ) .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret)) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 97973d4..5854627 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -17,7 +17,7 @@ import java.nio.file.Path class ControllerStartCommand( private val metricsCollector: MetricsCollector? -) : CliktCommand() { +) : CliktCommand() { init { context { @@ -39,6 +39,11 @@ class ControllerStartCommand( val pubSubGrpcPort: Int by option(help = "PubSub Grpc port (default: 5817)", envvar = "PUBSUB_GRPC_PORT").int() .default(5817) + val authorizationPort: Int by option( + help = "Authorization port (default: 5818)", + envvar = "AUTHORIZATION_PORT" + ).int().default(5818) + private val authSecretPath: Path by option( help = "Path to auth secret file (default: .auth.secret)", envvar = "AUTH_SECRET_PATH" diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt new file mode 100644 index 0000000..c3ee0ba --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt @@ -0,0 +1,78 @@ +package app.simplecloud.controller.runtime.oauth + +import app.simplecloud.controller.runtime.Repository +import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.shared.db.tables.records.Oauth2ClientDetailsRecord +import app.simplecloud.controller.shared.db.tables.references.OAUTH2_CLIENT_DETAILS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.withContext +import org.jooq.exception.DataAccessException + +class AuthClientRepository( + private val database: Database +) : Repository { + + override suspend fun getAll(): List { + return database.context.selectFrom(OAUTH2_CLIENT_DETAILS) + .asFlow() + .toCollection(mutableListOf()) + .map { mapRecordToClient(it) } + } + + override suspend fun find(identifier: String): OAuthClient? { + return database.context.selectFrom(OAUTH2_CLIENT_DETAILS) + .where(OAUTH2_CLIENT_DETAILS.CLIENT_ID.eq(identifier)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToClient(it) } + } + + override fun save(element: OAuthClient) { + database.context.insertInto( + OAUTH2_CLIENT_DETAILS, + + OAUTH2_CLIENT_DETAILS.CLIENT_ID, + OAUTH2_CLIENT_DETAILS.CLIENT_SECRET, + OAUTH2_CLIENT_DETAILS.GRANT_TYPES, + OAUTH2_CLIENT_DETAILS.REDIRECT_URI, + OAUTH2_CLIENT_DETAILS.SCOPE, + ).values( + element.clientId, + element.clientSecret, + element.grantTypes, + element.redirectUri, + element.scope, + ).onDuplicateKeyUpdate() + .set(OAUTH2_CLIENT_DETAILS.CLIENT_ID, element.clientId) + .set(OAUTH2_CLIENT_DETAILS.CLIENT_SECRET, element.clientSecret) + .set(OAUTH2_CLIENT_DETAILS.GRANT_TYPES, element.grantTypes) + .set(OAUTH2_CLIENT_DETAILS.REDIRECT_URI, element.redirectUri) + .set(OAUTH2_CLIENT_DETAILS.SCOPE, element.scope) + .executeAsync() + } + + override suspend fun delete(element: OAuthClient): Boolean { + return withContext(Dispatchers.IO) { + try { + database.context.deleteFrom(OAUTH2_CLIENT_DETAILS) + .where(OAUTH2_CLIENT_DETAILS.CLIENT_ID.eq(element.clientId)) + .execute() + return@withContext true + } catch (e: DataAccessException) { + return@withContext false + } + } + } + + private fun mapRecordToClient(record: Oauth2ClientDetailsRecord): OAuthClient { + return OAuthClient( + clientId = record.clientId!!, + clientSecret = record.clientSecret!!, + grantTypes = record.grantTypes!!, + redirectUri = record.redirectUri, + scope = record.scope, + ) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt new file mode 100644 index 0000000..776937f --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt @@ -0,0 +1,84 @@ +package app.simplecloud.controller.runtime.oauth + +import app.simplecloud.controller.runtime.Repository +import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.shared.db.tables.records.Oauth2TokensRecord +import app.simplecloud.controller.shared.db.tables.references.OAUTH2_TOKENS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.withContext +import org.jooq.exception.DataAccessException +import java.time.Duration +import java.time.LocalDateTime + +class AuthTokenRepository(private val database: Database): Repository { + override suspend fun getAll(): List { + return database.context.selectFrom(OAUTH2_TOKENS) + .asFlow() + .toCollection(mutableListOf()) + .map { mapRecordToToken(it) } + } + + override suspend fun find(identifier: String): OAuthToken? { + return database.context.selectFrom(OAUTH2_TOKENS) + .where(OAUTH2_TOKENS.TOKEN_ID.eq(identifier)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToToken(it) } + } + + suspend fun findByAccessToken(token: String): OAuthToken? { + return database.context.selectFrom(OAUTH2_TOKENS) + .where(OAUTH2_TOKENS.ACCESS_TOKEN.eq(token)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToToken(it) } + } + + override fun save(element: OAuthToken) { + database.context.insertInto( + OAUTH2_TOKENS, + + OAUTH2_TOKENS.TOKEN_ID, + OAUTH2_TOKENS.ACCESS_TOKEN, + OAUTH2_TOKENS.SCOPE, + OAUTH2_TOKENS.CLIENT_ID, + OAUTH2_TOKENS.EXPIRES_IN, + ).values( + element.id, + element.accessToken, + element.scope, + element.clientId, + if(element.expiresIn != null) LocalDateTime.now().plusSeconds(element.expiresIn.toLong()) else null + ).onDuplicateKeyUpdate() + .set(OAUTH2_TOKENS.TOKEN_ID, element.id) + .set(OAUTH2_TOKENS.ACCESS_TOKEN, element.accessToken) + .set(OAUTH2_TOKENS.SCOPE, element.scope) + .set(OAUTH2_TOKENS.CLIENT_ID, element.clientId) + .set(OAUTH2_TOKENS.EXPIRES_IN, if(element.expiresIn != null) LocalDateTime.now().plusSeconds(element.expiresIn.toLong()) else null) + .executeAsync() + } + + override suspend fun delete(element: OAuthToken): Boolean { + return withContext(Dispatchers.IO) { + try { + database.context.deleteFrom(OAUTH2_TOKENS) + .where(OAUTH2_TOKENS.CLIENT_ID.eq(element.clientId)) + .execute() + return@withContext true + } catch (e: DataAccessException) { + return@withContext false + } + } + } + + private fun mapRecordToToken(record: Oauth2TokensRecord): OAuthToken { + return OAuthToken( + id = record.tokenId!!, + scope = record.scope ?: "", + expiresIn = if(record.expiresIn != null) Duration.between(LocalDateTime.now(), record.expiresIn!!).toSeconds().toInt() else null, + accessToken = record.accessToken!!, + clientId = record.clientId!!, + ) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt new file mode 100644 index 0000000..c75f540 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt @@ -0,0 +1,41 @@ +package app.simplecloud.controller.runtime.oauth + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.MACSigner +import com.nimbusds.jose.crypto.MACVerifier +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.util.* + +class JwtHandler(private val secret: String, private val issuer: String) { + + /** + * Generates a jwt token + * @param subject the subject to sign + * @param expiresIn time span in seconds or null if it should not expire + * @return the JWT token as a string + */ + fun generateJwt(subject: String, expiresIn: Int? = null, scope: String = ""): String { + val signer = MACSigner(secret.toByteArray()) + val claimsSet = JWTClaimsSet.Builder() + .subject(subject) + .claim("scope", scope) + .issuer(issuer) + if (expiresIn != null) + claimsSet.expirationTime(Date(System.currentTimeMillis() + expiresIn * 1000L)) + val signedJWT = SignedJWT(JWSHeader(JWSAlgorithm.HS256), claimsSet.build()) + signedJWT.sign(signer) + return signedJWT.serialize() + } + + /** + * @return Whether the provided token was signed by this handler or not + */ + fun verifyJwt(token: String): Boolean { + val signedJWT = SignedJWT.parse(token) + val verifier = MACVerifier(secret.toByteArray()) + return signedJWT.verify(verifier) + } + +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt new file mode 100644 index 0000000..a65f164 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt @@ -0,0 +1,9 @@ +package app.simplecloud.controller.runtime.oauth + +data class OAuthClient( + val clientId: String, + val clientSecret: String, + val redirectUri: String? = null, + val grantTypes: String, + val scope: String? = null, +) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt new file mode 100644 index 0000000..b12cac5 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -0,0 +1,180 @@ +package app.simplecloud.controller.runtime.oauth + +import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.runtime.launcher.ControllerStartCommand +import com.fasterxml.jackson.databind.SerializationFeature +import io.ktor.http.* +import io.ktor.serialization.jackson.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.util.* + +class OAuthServer(private val args: ControllerStartCommand, database: Database) { + private val issuer = "http://${args.grpcHost}:${args.authorizationPort}" + private val secret = args.authSecret + private val jwtHandler = JwtHandler(secret, issuer) + private val pkceHandler = PKCEHandler() + private val clientRepository = AuthClientRepository(database) + private val tokenRepository = AuthTokenRepository(database) + + //code to client_id, code_challenge and scope (this is in memory because it is only in use temporary) + private val flowData = mutableMapOf>() + + fun start() { + embeddedServer(Netty, host = args.grpcHost, port = args.authorizationPort) { + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) + } + } + + routing { + // Register clients + post("/register_client") { + val params = call.receiveParameters() + val providedMasterToken = params["master_token"] + if (providedMasterToken != secret) { + call.respond(HttpStatusCode.Forbidden, "Invalid master token") + return@post + } + val redirectUri = params["redirect_uri"] + val grantTypes = params["grant_types"] + if (grantTypes == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid grant_types") + return@post + } + val scope = params["scope"] + val clientId = "client-${UUID.randomUUID().toString().replace("-", "").substring(0, 8)}" + val clientSecret = "secret-${UUID.randomUUID().toString().replace("-", "")}" + val client = OAuthClient(clientId, clientSecret, redirectUri, grantTypes, scope) + clientRepository.save(client) + call.respond(mapOf("client_id" to clientId, "client_secret" to clientSecret)) + } + + // Authorization endpoint (simulating authorization code flow) + get("/authorize") { + val clientId = call.request.queryParameters["client_id"] + val redirectUri = call.request.queryParameters["redirect_uri"] + val challengeMethod = call.request.queryParameters["code_challenge_method"] + val challenge = call.request.queryParameters["code_challenge"] + val scope = call.request.queryParameters["scope"] + if (clientId == null || redirectUri == null || scope == null || challenge == null) { + call.respond( + HttpStatusCode.BadRequest, + "You have to provide redirect_uri, client_id, scope and challenge" + ) + return@get + } + if (challengeMethod == null || challengeMethod != "S256") { + call.respond(HttpStatusCode.BadRequest, "Invalid challenge, S256 is supported.") + return@get + } + val client = clientRepository.find(clientId) + if (client == null) { + call.respond(HttpStatusCode.NotFound, "Client not found") + return@get + } + if (!client.grantTypes.contains("authorization_code")) { + call.respond(HttpStatusCode.BadRequest, "User authorization is not supported by the client") + return@get + } + + if (!client.grantTypes.contains("pkce")) { + call.respond( + HttpStatusCode.BadRequest, + "User authorization using PKCE is not supported by the client" + ) + return@get + } + + if (client.scope != null && !client.scope.contains(scope)) { + call.respond(HttpStatusCode.BadRequest, "This scope is not supported by the client") + return@get + } + + val authorizationCode = UUID.randomUUID().toString().replace("-", "") + flowData[authorizationCode] = listOf(client.clientId, challenge, scope) + call.respondRedirect("$redirectUri?code=$authorizationCode") + } + + // Token endpoint + post("/token") { + val params = call.receiveParameters() + val clientId = params["client_id"] + val clientSecret = params["client_secret"] + val code = params["code"] + val codeVerifier = params["code_verifier"] + + if (clientId == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a client id") + return@post + } + + if (clientSecret == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a client secret") + return@post + } + + val client = clientRepository.find(clientId) + if (client == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a valid client id") + return@post + } + + if (client.clientSecret != clientSecret) { + call.respond(HttpStatusCode.BadRequest, "Invalid client secret") + return@post + } + + if (client.grantTypes.contains("authorization_code") && client.grantTypes.contains("pkce")) { + if (codeVerifier == null || code == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a code and a code verifier") + return@post + } + val reconstructedChallenge = pkceHandler.generateCodeChallenge(codeVerifier) + val originalChallenge = flowData[code]?.get(1) + //If we can reconstruct the challenge, the authorization context was made in a secure context + if (originalChallenge == reconstructedChallenge) { + val token = OAuthToken( + id = UUID.randomUUID().toString(), + clientId = clientId, + accessToken = jwtHandler.generateJwt( + clientId, + expiresIn = 3600, + scope = flowData[code]?.get(2)!! + ), + expiresIn = 3600, + scope = flowData[code]?.get(2)!! + ) + tokenRepository.save(token) + + return@post + } + call.respond( + HttpStatusCode.BadRequest, + "The token request was not made in the same context as the authorization." + ) + return@post + } else if (client.grantTypes.contains("client_credentials")) { + val token = OAuthToken( + id = UUID.randomUUID().toString(), + clientId = clientId, + accessToken = jwtHandler.generateJwt(clientId, scope = client.scope ?: "*"), + scope = client.scope ?: "*" + ) + tokenRepository.save(token) + return@post + } else { + call.respond(HttpStatusCode.BadRequest, "Invalid client") + return@post + } + } + } + }.start(wait = true) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt new file mode 100644 index 0000000..95032fc --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt @@ -0,0 +1,9 @@ +package app.simplecloud.controller.runtime.oauth + +data class OAuthToken( + val id: String, + val clientId: String, + val accessToken: String, + val scope: String, + val expiresIn: Int? = null +) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PKCEHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PKCEHandler.kt new file mode 100644 index 0000000..39efeac --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PKCEHandler.kt @@ -0,0 +1,18 @@ +package app.simplecloud.controller.runtime.oauth + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.* + +class PKCEHandler { + fun generateCodeVerifier(): String { + // Code verifier is a random string (e.g., 43-128 characters) + return UUID.randomUUID().toString().replace("-", "") + } + + fun generateCodeChallenge(codeVerifier: String): String { + // SHA256 the code verifier and base64 encode it + val digest = MessageDigest.getInstance("SHA-256").digest(codeVerifier.toByteArray(StandardCharsets.UTF_8)) + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest) + } +} \ No newline at end of file diff --git a/controller-shared/build.gradle.kts b/controller-shared/build.gradle.kts index 9a3361e..d1cd2a2 100644 --- a/controller-shared/build.gradle.kts +++ b/controller-shared/build.gradle.kts @@ -1,7 +1,7 @@ dependencies { api(rootProject.libs.bundles.proto) - api(rootProject.libs.simpleCloudPubSub) + api(rootProject.libs.simplecloud.pubsub) api(rootProject.libs.bundles.configurate) api(rootProject.libs.clikt) - api(rootProject.libs.kotlinCoroutines) + api(rootProject.libs.kotlin.coroutines) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d33f7e..205fd14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,81 +1,102 @@ [versions] kotlin = "2.0.20" -kotlinCoroutines = "1.9.0" +kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" protobuf = "3.25.2" grpc = "1.61.0" -grpcKotlin = "1.4.1" -simpleCloudProtoSpecs = "1.4.1.1.20241108142018.a431612a8826" -simpleCloudPubSub = "1.0.5" +grpc-kotlin = "1.4.1" +simplecloud-protospecs = "1.4.1.1.20241108142018.a431612a8826" +simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" configurate = "4.1.2" -sqliteJdbc = "3.44.1.0" +sqlite-jdbc = "3.44.1.0" clikt = "4.3.0" -sonatypeCentralPortalPublisher = "1.2.3" -spotifyCompletableFutures = "0.3.6" +sonatype-central-portal-publisher = "1.2.3" +spotify-completablefutures = "0.3.6" +ktor = "3.0.1" +nimbus = "9.46" [libraries] -kotlinJvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } -kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlinCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } +kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } -log4jCore = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } -log4jApi = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } -log4jSlf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } +log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } -protobufKotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } +protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } -grpcStub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } -grpcKotlinStub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpcKotlin" } -grpcProtobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } -grpcNettyShaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } +grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-kotlin" } +grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } +grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } -simpleCloudProtoSpecs = { module = "build.buf.gen:simplecloud_proto-specs_grpc_kotlin", version.ref = "simpleCloudProtoSpecs" } -simpleCloudPubSub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simpleCloudPubSub" } +simplecloud-protospecs = { module = "build.buf.gen:simplecloud_proto-specs_grpc_kotlin", version.ref = "simplecloud-protospecs" } +simplecloud-pubsub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simplecloud-pubsub" } simplecloud-metrics = { module = "app.simplecloud:internal-metrics-api", version.ref = "simplecloud-metrics" } -qooq = { module = "org.jooq:jooq-kotlin", version.ref = "jooq" } -qooqMeta = { module = "org.jooq:jooq-meta", version.ref = "jooq" } -jooqMetaExtensions = { module = "org.jooq:jooq-meta-extensions", version.ref = "jooq" } -jooqKotlinCoroutines = { module = "org.jooq:jooq-kotlin-coroutines", version.ref = "jooq" } +jooq = { module = "org.jooq:jooq-kotlin", version.ref = "jooq" } +jooq-meta = { module = "org.jooq:jooq-meta", version.ref = "jooq" } +jooq-meta-extensions = { module = "org.jooq:jooq-meta-extensions", version.ref = "jooq" } +jooq-kotlin-coroutines = { module = "org.jooq:jooq-kotlin-coroutines", version.ref = "jooq" } -configurateYaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } -configurateExtraKotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" } +configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } +configurate-extra-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" } -sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } -spotifyCompletableFutures = { module = "com.spotify:completable-futures", version.ref = "spotifyCompletableFutures" } +spotify-completablefutures = { module = "com.spotify:completable-futures", version.ref = "spotify-completablefutures" } + +ktor-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } +ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } +ktor-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" } +ktor-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" } +ktor-jackson = { module = "io.ktor:ktor-serialization-jackson", version.ref = "ktor" } +ktor-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } + +nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" } + + [bundles] log4j = [ - "log4jCore", - "log4jApi", - "log4jSlf4j" + "log4j-core", + "log4j-api", + "log4j-slf4j" ] proto = [ - "protobufKotlin", - "grpcStub", - "grpcKotlinStub", - "grpcProtobuf", - "grpcNettyShaded", - "simpleCloudProtoSpecs", + "protobuf-kotlin", + "grpc-stub", + "grpc-kotlin-stub", + "grpc-protobuf", + "grpc-netty-shaded", + "simplecloud-protospecs", ] jooq = [ - "qooq", - "qooqMeta", - "jooqKotlinCoroutines" + "jooq", + "jooq-meta", + "jooq-kotlin-coroutines" ] configurate = [ - "configurateYaml", - "configurateExtraKotlin" + "configurate-yaml", + "configurate-extra-kotlin" +] +ktor = [ + "ktor-core", + "ktor-netty", + "ktor-auth", + "ktor-auth-jwt", + "ktor-content-negotiation", + "ktor-jackson" ] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } -jooqCodegen = { id = "org.jooq.jooq-codegen-gradle", version.ref = "jooq" } -sonatypeCentralPortalPublisher = { id = "net.thebugmc.gradle.sonatype-central-portal-publisher", version.ref = "sonatypeCentralPortalPublisher" } \ No newline at end of file +jooq-codegen = { id = "org.jooq.jooq-codegen-gradle", version.ref = "jooq" } +sonatype-central-portal-publisher = { id = "net.thebugmc.gradle.sonatype-central-portal-publisher", version.ref = "sonatype-central-portal-publisher" } \ No newline at end of file From 134927939caf10ec410683869a654983c459171c Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 12 Nov 2024 20:40:10 +0100 Subject: [PATCH 20/65] feat: introspection endpoint --- .../controller/runtime/oauth/OAuthServer.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index b12cac5..d4f4dc8 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -174,6 +174,31 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) return@post } } + + post("/introspect") { + val params = call.receiveParameters() + val token = params["token"] + if (token == null) { + call.respond(HttpStatusCode.BadRequest, "Token is missing") + return@post + } + + val authToken = tokenRepository.findByAccessToken(token) + if (authToken == null) { + call.respond(HttpStatusCode.OK, mapOf("active" to false)) + return@post + } + + // If the token exists, respond with token details + call.respond( + mapOf( + "active" to ((authToken.expiresIn ?: 1) > 0), + "client_id" to authToken.clientId, + "scope" to authToken.scope, + "exp" to authToken.expiresIn, + ) + ) + } } }.start(wait = true) } From b88afc910595f2ba302c4dc9b31515f7837dc179 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 12 Nov 2024 22:16:44 +0100 Subject: [PATCH 21/65] fix: return types in introspect and token endpoint --- .../controller/runtime/oauth/OAuthServer.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index d4f4dc8..f2eba4d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -12,6 +12,7 @@ import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import org.apache.logging.log4j.LogManager import java.util.* class OAuthServer(private val args: ControllerStartCommand, database: Database) { @@ -152,7 +153,11 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) scope = flowData[code]?.get(2)!! ) tokenRepository.save(token) - + call.respond(mapOf( + "access_token" to token.accessToken, + "scope" to token.scope, + "exp" to (token.expiresIn ?: -1), + )) return@post } call.respond( @@ -168,6 +173,11 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) scope = client.scope ?: "*" ) tokenRepository.save(token) + call.respond(mapOf( + "access_token" to token.accessToken, + "scope" to token.scope, + "exp" to (token.expiresIn ?: -1), + )) return@post } else { call.respond(HttpStatusCode.BadRequest, "Invalid client") @@ -195,7 +205,7 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) "active" to ((authToken.expiresIn ?: 1) > 0), "client_id" to authToken.clientId, "scope" to authToken.scope, - "exp" to authToken.expiresIn, + "exp" to (authToken.expiresIn ?: -1), ) ) } From 2533c821b3075943023abfbbfdb134f3fabab9db Mon Sep 17 00:00:00 2001 From: dayeeet Date: Thu, 14 Nov 2024 23:23:58 +0100 Subject: [PATCH 22/65] feat: token introspection using oauth --- controller-runtime/build.gradle.kts | 2 - controller-runtime/src/main/db/schema.sql | 90 ++++--- .../controller/runtime/ControllerRuntime.kt | 4 +- .../launcher/ControllerStartCommand.kt | 4 +- .../controller/runtime/launcher/Launcher.kt | 18 +- .../runtime/oauth/AuthGroupRepository.kt | 63 +++++ .../runtime/oauth/AuthenticationHandler.kt | 81 +++++++ .../runtime/oauth/AuthorizationHandler.kt | 227 ++++++++++++++++++ .../controller/runtime/oauth/OAuthGroup.kt | 6 + .../controller/runtime/oauth/OAuthServer.kt | 206 +++------------- .../controller/runtime/oauth/OAuthUser.kt | 10 + controller-shared/build.gradle.kts | 3 + .../controller/shared/MetadataKeys.kt | 1 + .../shared/auth/AuthSecretInterceptor.kt | 25 +- .../controller/shared/auth}/JwtHandler.kt | 5 +- .../shared/auth/OAuthIntrospector.kt | 34 +++ gradle/libs.versions.toml | 3 + 17 files changed, 569 insertions(+), 213 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt rename {controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth => controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth}/JwtHandler.kt (91%) create mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts index 165b642..c78f690 100644 --- a/controller-runtime/build.gradle.kts +++ b/controller-runtime/build.gradle.kts @@ -13,8 +13,6 @@ dependencies { implementation(libs.bundles.log4j) implementation(libs.clikt) implementation(libs.spotify.completablefutures) - implementation(libs.bundles.ktor) - implementation(libs.nimbus.jose.jwt) } application { diff --git a/controller-runtime/src/main/db/schema.sql b/controller-runtime/src/main/db/schema.sql index 869724f..ab4a76a 100644 --- a/controller-runtime/src/main/db/schema.sql +++ b/controller-runtime/src/main/db/schema.sql @@ -3,42 +3,74 @@ Execute jooqCodegen to create java classes for these files. */ -CREATE TABLE IF NOT EXISTS cloud_servers( - unique_id varchar NOT NULL PRIMARY KEY, - group_name varchar NOT NULL, - host_id varchar NOT NULL, - numerical_id int NOT NULL, - ip varchar NOT NULL, - port int NOT NULL, - minimum_memory int NOT NULL, - maximum_memory int NOT NULL, - max_players int NOT NULL, - player_count int NOT NULL, - state varchar NOT NULL, - type varchar NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS cloud_servers +( + unique_id varchar NOT NULL PRIMARY KEY, + group_name varchar NOT NULL, + host_id varchar NOT NULL, + numerical_id int NOT NULL, + ip varchar NOT NULL, + port int NOT NULL, + minimum_memory int NOT NULL, + maximum_memory int NOT NULL, + max_players int NOT NULL, + player_count int NOT NULL, + state varchar NOT NULL, + type varchar NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS cloud_server_properties( +CREATE TABLE IF NOT EXISTS cloud_server_properties +( server_id varchar NOT NULL, - key varchar NOT NULL, - value varchar, + key varchar NOT NULL, + value varchar, CONSTRAINT compound_key PRIMARY KEY (server_id, key) ); -CREATE TABLE IF NOT EXISTS oauth2_client_details( - client_id varchar PRIMARY KEY, +CREATE TABLE IF NOT EXISTS oauth2_client_details +( + client_id varchar PRIMARY KEY, client_secret varchar, - redirect_uri varchar, - grant_types varchar, - scope varchar + redirect_uri varchar, + grant_types varchar, + scope varchar ); -CREATE TABLE IF NOT EXISTS oauth2_tokens( - token_id varchar PRIMARY KEY, - client_id varchar, +CREATE TABLE IF NOT EXISTS oauth2_users +( + user_id varchar PRIMARY KEY, + groups varchar, + scopes varchar, + username varchar NOT NULL, + hashed_password varchar NOT NULL +); + + +CREATE TABLE IF NOT EXISTS oauth2_tokens +( + token_id varchar PRIMARY KEY, + client_id varchar, access_token varchar, - scope varchar, - expires_in timestamp -) + scope varchar, + expires_in timestamp, + user_id varchar, + CONSTRAINT fk_user_token FOREIGN KEY (user_id) REFERENCES oauth2_users (user_id) ON DELETE CASCADE +); + + +CREATE TABLE IF NOT EXISTS oauth2_groups +( + group_name varchar PRIMARY KEY, + scopes varchar +); + +CREATE TABLE IF NOT EXISTS oauth2_user_groups +( + user_id VARCHAR, + group_name VARCHAR, + PRIMARY KEY (user_id, group_name), + CONSTRAINT fk_user_group_user FOREIGN KEY (user_id) REFERENCES oauth2_users (user_id) ON DELETE CASCADE, + CONSTRAINT fk_user_group_group FOREIGN KEY (group_name) REFERENCES oauth2_groups (group_name) ON DELETE CASCADE +); diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index fd2e1ca..34598fb 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -131,14 +131,14 @@ class ControllerRuntime( ) ) ) - .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret)) + .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret, controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) .build() } private fun createPubSubGrpcServer(): Server { return ServerBuilder.forPort(controllerStartCommand.pubSubGrpcPort) .addService(pubSubService) - .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret)) + .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret, controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) .build() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 5854627..ec32876 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -16,7 +16,7 @@ import java.io.File import java.nio.file.Path class ControllerStartCommand( - private val metricsCollector: MetricsCollector? + //private val metricsCollector: MetricsCollector? ) : CliktCommand() { init { @@ -60,7 +60,7 @@ class ControllerStartCommand( override fun run() { if (trackMetrics) { - metricsCollector?.start() + //metricsCollector?.start() } val controllerRuntime = ControllerRuntime(this) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt index d5cca11..35a79fd 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt @@ -5,19 +5,25 @@ import org.apache.logging.log4j.LogManager suspend fun main(args: Array) { - val metricsCollector = try { + /** val metricsCollector = try { MetricsCollector.create("controller") } catch (e: Exception) { null - } - configureLog4j(metricsCollector) - ControllerStartCommand(metricsCollector).main(args) + } */ + configureLog4j( + // metricsCollector + ) + ControllerStartCommand( + // metricsCollector + ).main(args) } -fun configureLog4j(metricsCollector: MetricsCollector?) { +fun configureLog4j( + // metricsCollector: MetricsCollector? +) { val globalExceptionHandlerLogger = LogManager.getLogger("GlobalExceptionHandler") Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - metricsCollector?.recordError(throwable) + //metricsCollector?.recordError(throwable) globalExceptionHandlerLogger.error("Uncaught exception in thread ${thread.name}", throwable) } } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt new file mode 100644 index 0000000..ad92a3b --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt @@ -0,0 +1,63 @@ +package app.simplecloud.controller.runtime.oauth + +import app.simplecloud.controller.runtime.Repository +import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.shared.db.tables.records.Oauth2GroupsRecord +import app.simplecloud.controller.shared.db.tables.references.OAUTH2_GROUPS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.withContext +import org.jooq.exception.DataAccessException + +class AuthGroupRepository(private val database: Database) : Repository { + override suspend fun getAll(): List { + return database.context.selectFrom(OAUTH2_GROUPS) + .asFlow() + .toCollection(mutableListOf()) + .map { mapRecordToGroup(it) } + } + + override suspend fun find(identifier: String): OAuthGroup? { + return database.context.selectFrom(OAUTH2_GROUPS) + .where(OAUTH2_GROUPS.GROUP_NAME.eq(identifier)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToGroup(it) } + } + + override fun save(element: OAuthGroup) { + database.context.insertInto( + OAUTH2_GROUPS, + + OAUTH2_GROUPS.GROUP_NAME, + OAUTH2_GROUPS.SCOPES + ).values( + element.name, + element.scopes.joinToString(";"), + ).onDuplicateKeyUpdate() + .set(OAUTH2_GROUPS.GROUP_NAME, element.name) + .set(OAUTH2_GROUPS.SCOPES, element.scopes.joinToString(";")) + .executeAsync() + } + + override suspend fun delete(element: OAuthGroup): Boolean { + return withContext(Dispatchers.IO) { + try { + database.context.deleteFrom(OAUTH2_GROUPS) + .where(OAUTH2_GROUPS.GROUP_NAME.eq(element.name)) + .execute() + return@withContext true + } catch (e: DataAccessException) { + return@withContext false + } + } + } + + private fun mapRecordToGroup(record: Oauth2GroupsRecord): OAuthGroup { + return OAuthGroup( + scopes = record.scopes?.split(";") ?: emptyList(), + name = record.groupName!!, + ) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt new file mode 100644 index 0000000..694dc4c --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -0,0 +1,81 @@ +package app.simplecloud.controller.runtime.oauth + +import io.ktor.http.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +class AuthenticationHandler( + private val groupRepository: AuthGroupRepository +) { + suspend fun saveGroup(call: RoutingCall) { + //TODO: permission check + val params = call.receiveParameters() + val groupName = params["group_name"] + if (groupName == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a group name") + return + } + val scopes = params["scopes"]?.split(";") ?: emptyList() + groupRepository.save(OAuthGroup(scopes, groupName)) + call.respond("Group successfully updated") + } + + suspend fun getGroup(call: RoutingCall) { + //TODO: permission check + val group = loadGroup(call) ?: return + call.respond(mapOf("group_name" to group.name, "scope" to group.scopes.joinToString(" "))) + } + + suspend fun getGroups(call: RoutingCall) { + //TODO: permission check + val groups = groupRepository.getAll() + call.respond(listOf(groups.map { + mapOf( + "group_name" to it.name, + "scope" to it.scopes.joinToString(" ") + ) + }).flatten()) + } + + suspend fun deleteGroup(call: RoutingCall) { + //TODO: permission check + val group = loadGroup(call) ?: return + groupRepository.delete(group) + call.respond("Group successfully deleted") + } + + private suspend fun loadGroup(call: RoutingCall): OAuthGroup? { + val params = call.receiveParameters() + val groupName = params["group_name"] + if (groupName == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a group name") + return null + } + val group = groupRepository.find(groupName) + if (group == null) { + call.respond(HttpStatusCode.NotFound, "Group not found") + return null + } + return group + } + + + suspend fun createUser() { + //TODO: permission check + } + + suspend fun updateUser() { + //TODO: permission check + + } + + suspend fun deleteUser() { + //TODO: permission check + + } + + suspend fun login() { + + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt new file mode 100644 index 0000000..5f2d3f7 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt @@ -0,0 +1,227 @@ +package app.simplecloud.controller.runtime.oauth + +import app.simplecloud.controller.shared.auth.JwtHandler +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.util.* + +class AuthorizationHandler( + private val secret: String, + private val clientRepository: AuthClientRepository, + private val tokenRepository: AuthTokenRepository, + private val pkceHandler: PKCEHandler, + private val jwtHandler: JwtHandler, + private val flowData: MutableMap> +) { + + suspend fun registerClient(call: RoutingCall) { + val params = call.receiveParameters() + val providedMasterToken = params["master_token"] + if (providedMasterToken != secret) { + call.respond(HttpStatusCode.Forbidden, "Invalid master token") + return + } + val redirectUri = params["redirect_uri"] + val grantTypes = params["grant_types"] + if (grantTypes == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid grant_types") + return + } + val scope = params["scope"] + val clientId = "client-${UUID.randomUUID().toString().replace("-", "").substring(0, 8)}" + val clientSecret = "secret-${UUID.randomUUID().toString().replace("-", "")}" + val client = OAuthClient(clientId, clientSecret, redirectUri, grantTypes, scope) + clientRepository.save(client) + call.respond(mapOf("client_id" to clientId, "client_secret" to clientSecret)) + } + + suspend fun authorizeRequest(call: ApplicationCall) { + val params = call.receiveParameters() + val clientId = params["client_id"] + val redirectUri = params["redirect_uri"] + val challengeMethod = params["code_challenge_method"] + val challenge = params["code_challenge"] + val scope = params["scope"] + if (clientId == null || redirectUri == null || scope == null || challenge == null) { + call.respond( + HttpStatusCode.BadRequest, + "You have to provide redirect_uri, client_id, scope and challenge" + ) + return + } + if (challengeMethod == null || challengeMethod != "S256") { + call.respond(HttpStatusCode.BadRequest, "Invalid challenge, S256 is supported.") + return + } + val client = clientRepository.find(clientId) + if (client == null) { + call.respond(HttpStatusCode.NotFound, "Client not found") + return + } + if (!client.grantTypes.contains("authorization_code")) { + call.respond(HttpStatusCode.BadRequest, "User authorization is not supported by the client") + return + } + + if (!client.grantTypes.contains("pkce")) { + call.respond( + HttpStatusCode.BadRequest, + "User authorization using PKCE is not supported by the client" + ) + return + } + + if (client.scope != null && !client.scope.contains(scope)) { + call.respond(HttpStatusCode.BadRequest, "This scope is not supported by the client") + return + } + + val authorizationCode = UUID.randomUUID().toString().replace("-", "") + flowData[authorizationCode] = listOf(client.clientId, challenge, scope) + call.respond(mapOf("redirectUri" to "$redirectUri?code=$authorizationCode")) + } + + suspend fun tokenRequest(call: ApplicationCall) { + val params = call.receiveParameters() + val clientId = params["client_id"] + val clientSecret = params["client_secret"] + val code = params["code"] + val codeVerifier = params["code_verifier"] + + if (clientId == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a client id") + return + } + + if (clientSecret == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a client secret") + return + } + + val client = clientRepository.find(clientId) + if (client == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a valid client id") + return + } + + if (client.clientSecret != clientSecret) { + call.respond(HttpStatusCode.BadRequest, "Invalid client secret") + return + } + + if (client.grantTypes.contains("authorization_code") && client.grantTypes.contains("pkce")) { + if (codeVerifier == null || code == null) { + call.respond(HttpStatusCode.BadRequest, "You have to provide a code and a code verifier") + return + } + val reconstructedChallenge = pkceHandler.generateCodeChallenge(codeVerifier) + val originalChallenge = flowData[code]?.get(1) + //If we can reconstruct the challenge, the authorization context was made in a secure context + if (originalChallenge == reconstructedChallenge) { + val token = OAuthToken( + id = UUID.randomUUID().toString(), + clientId = clientId, + accessToken = jwtHandler.generateJwt( + clientId, + expiresIn = 3600, + scope = flowData[code]?.get(2)!! + ), + expiresIn = 3600, + scope = flowData[code]?.get(2)!! + ) + tokenRepository.save(token) + call.respond( + mapOf( + "access_token" to token.accessToken, + "scope" to token.scope, + "exp" to (token.expiresIn ?: -1), + ) + ) + return + } + call.respond( + HttpStatusCode.BadRequest, + "The token request was not made in the same context as the authorization." + ) + return + } else if (client.grantTypes.contains("client_credentials")) { + val token = OAuthToken( + id = UUID.randomUUID().toString(), + clientId = clientId, + accessToken = jwtHandler.generateJwt(clientId, scope = client.scope ?: "*"), + scope = client.scope ?: "*" + ) + tokenRepository.save(token) + call.respond( + mapOf( + "access_token" to token.accessToken, + "scope" to token.scope, + "exp" to (token.expiresIn ?: -1), + ) + ) + return + } else { + call.respond(HttpStatusCode.BadRequest, "Invalid client") + return + } + } + + suspend fun revokeRequest(call: RoutingCall) { + val params = call.receiveParameters() + val accessToken = params["access_token"] + if (accessToken == null) { + call.respond(HttpStatusCode.BadRequest, "Access token is invalid") + return + } + val token = tokenRepository.findByAccessToken(accessToken) + if (token == null) { + call.respond(HttpStatusCode.BadRequest, "Access token is invalid") + return + } + + if (tokenRepository.delete(token)) { + call.respond("Access token revoked") + return + } + + call.respond(HttpStatusCode.InternalServerError, "Could not delete token") + + } + + suspend fun introspectRequest(call: ApplicationCall) { + val params = call.receiveParameters() + val token = params["token"] + if (token == null) { + call.respond(HttpStatusCode.BadRequest, "Token is missing") + return + } + + val authToken = tokenRepository.findByAccessToken(token) + if (authToken == null) { + call.respond(HttpStatusCode.OK, mapOf("active" to false)) + return + } + + val active = ((authToken.expiresIn ?: 1) > 0) && jwtHandler.verifyJwt(token) + if (!active) { + tokenRepository.delete(authToken) + call.respond(mapOf("active" to false)) + } + + // If the token exists, respond with token details + call.respond( + mapOf( + "active" to true, + "token_id" to authToken.id, + "client_id" to authToken.clientId, + "scope" to authToken.scope, + "exp" to (authToken.expiresIn ?: -1), + ) + ) + } + + +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt new file mode 100644 index 0000000..3fe95d6 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt @@ -0,0 +1,6 @@ +package app.simplecloud.controller.runtime.oauth + +data class OAuthGroup( + val scopes: List = emptyList(), + val name: String, +) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index f2eba4d..0a0516f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -2,18 +2,19 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.runtime.database.Database import app.simplecloud.controller.runtime.launcher.ControllerStartCommand +import app.simplecloud.controller.shared.auth.JwtHandler +import app.simplecloud.controller.shared.auth.OAuthIntrospector import com.fasterxml.jackson.databind.SerializationFeature -import io.ktor.http.* +import com.nimbusds.jwt.JWTClaimsSet +import io.ktor.client.* import io.ktor.serialization.jackson.* import io.ktor.server.application.* +import io.ktor.server.auth.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import org.apache.logging.log4j.LogManager -import java.util.* class OAuthServer(private val args: ControllerStartCommand, database: Database) { private val issuer = "http://${args.grpcHost}:${args.authorizationPort}" @@ -26,6 +27,12 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) //code to client_id, code_challenge and scope (this is in memory because it is only in use temporary) private val flowData = mutableMapOf>() + private val authorizationHandler = + AuthorizationHandler(secret, clientRepository, tokenRepository, pkceHandler, jwtHandler, flowData) + + private val introspector = OAuthIntrospector(secret, issuer) + + fun start() { embeddedServer(Netty, host = args.grpcHost, port = args.authorizationPort) { install(ContentNegotiation) { @@ -34,180 +41,43 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) } } - routing { - // Register clients - post("/register_client") { - val params = call.receiveParameters() - val providedMasterToken = params["master_token"] - if (providedMasterToken != secret) { - call.respond(HttpStatusCode.Forbidden, "Invalid master token") - return@post - } - val redirectUri = params["redirect_uri"] - val grantTypes = params["grant_types"] - if (grantTypes == null) { - call.respond(HttpStatusCode.BadRequest, "Invalid grant_types") - return@post - } - val scope = params["scope"] - val clientId = "client-${UUID.randomUUID().toString().replace("-", "").substring(0, 8)}" - val clientSecret = "secret-${UUID.randomUUID().toString().replace("-", "")}" - val client = OAuthClient(clientId, clientSecret, redirectUri, grantTypes, scope) - clientRepository.save(client) - call.respond(mapOf("client_id" to clientId, "client_secret" to clientSecret)) + install(Authentication) { + bearer { + authenticate { credential -> introspector.introspect(credential.token) } } + } - // Authorization endpoint (simulating authorization code flow) - get("/authorize") { - val clientId = call.request.queryParameters["client_id"] - val redirectUri = call.request.queryParameters["redirect_uri"] - val challengeMethod = call.request.queryParameters["code_challenge_method"] - val challenge = call.request.queryParameters["code_challenge"] - val scope = call.request.queryParameters["scope"] - if (clientId == null || redirectUri == null || scope == null || challenge == null) { - call.respond( - HttpStatusCode.BadRequest, - "You have to provide redirect_uri, client_id, scope and challenge" - ) - return@get - } - if (challengeMethod == null || challengeMethod != "S256") { - call.respond(HttpStatusCode.BadRequest, "Invalid challenge, S256 is supported.") - return@get - } - val client = clientRepository.find(clientId) - if (client == null) { - call.respond(HttpStatusCode.NotFound, "Client not found") - return@get - } - if (!client.grantTypes.contains("authorization_code")) { - call.respond(HttpStatusCode.BadRequest, "User authorization is not supported by the client") - return@get - } - - if (!client.grantTypes.contains("pkce")) { - call.respond( - HttpStatusCode.BadRequest, - "User authorization using PKCE is not supported by the client" - ) - return@get - } + routing { - if (client.scope != null && !client.scope.contains(scope)) { - call.respond(HttpStatusCode.BadRequest, "This scope is not supported by the client") - return@get - } + // AUTHORIZATION - val authorizationCode = UUID.randomUUID().toString().replace("-", "") - flowData[authorizationCode] = listOf(client.clientId, challenge, scope) - call.respondRedirect("$redirectUri?code=$authorizationCode") + // Client registration endpoint + post("/oauth/register_client") { + authorizationHandler.registerClient(call) + } + // Authorization endpoint (simulating authorization code flow) + post("/oauth/authorize") { + authorizationHandler.authorizeRequest(call) } - // Token endpoint - post("/token") { - val params = call.receiveParameters() - val clientId = params["client_id"] - val clientSecret = params["client_secret"] - val code = params["code"] - val codeVerifier = params["code_verifier"] - - if (clientId == null) { - call.respond(HttpStatusCode.BadRequest, "You have to provide a client id") - return@post - } - - if (clientSecret == null) { - call.respond(HttpStatusCode.BadRequest, "You have to provide a client secret") - return@post - } - - val client = clientRepository.find(clientId) - if (client == null) { - call.respond(HttpStatusCode.BadRequest, "You have to provide a valid client id") - return@post - } - - if (client.clientSecret != clientSecret) { - call.respond(HttpStatusCode.BadRequest, "Invalid client secret") - return@post - } - - if (client.grantTypes.contains("authorization_code") && client.grantTypes.contains("pkce")) { - if (codeVerifier == null || code == null) { - call.respond(HttpStatusCode.BadRequest, "You have to provide a code and a code verifier") - return@post - } - val reconstructedChallenge = pkceHandler.generateCodeChallenge(codeVerifier) - val originalChallenge = flowData[code]?.get(1) - //If we can reconstruct the challenge, the authorization context was made in a secure context - if (originalChallenge == reconstructedChallenge) { - val token = OAuthToken( - id = UUID.randomUUID().toString(), - clientId = clientId, - accessToken = jwtHandler.generateJwt( - clientId, - expiresIn = 3600, - scope = flowData[code]?.get(2)!! - ), - expiresIn = 3600, - scope = flowData[code]?.get(2)!! - ) - tokenRepository.save(token) - call.respond(mapOf( - "access_token" to token.accessToken, - "scope" to token.scope, - "exp" to (token.expiresIn ?: -1), - )) - return@post - } - call.respond( - HttpStatusCode.BadRequest, - "The token request was not made in the same context as the authorization." - ) - return@post - } else if (client.grantTypes.contains("client_credentials")) { - val token = OAuthToken( - id = UUID.randomUUID().toString(), - clientId = clientId, - accessToken = jwtHandler.generateJwt(clientId, scope = client.scope ?: "*"), - scope = client.scope ?: "*" - ) - tokenRepository.save(token) - call.respond(mapOf( - "access_token" to token.accessToken, - "scope" to token.scope, - "exp" to (token.expiresIn ?: -1), - )) - return@post - } else { - call.respond(HttpStatusCode.BadRequest, "Invalid client") - return@post - } + post("/oauth/token") { + authorizationHandler.tokenRequest(call) + } + // Revocation endpoint + post("/oauth/revoke") { + authorizationHandler.revokeRequest(call) + } + // Introspection endpoint + post("/oauth/introspect") { + authorizationHandler.introspectRequest(call) } - post("/introspect") { - val params = call.receiveParameters() - val token = params["token"] - if (token == null) { - call.respond(HttpStatusCode.BadRequest, "Token is missing") - return@post - } + // AUTHENTICATION - val authToken = tokenRepository.findByAccessToken(token) - if (authToken == null) { - call.respond(HttpStatusCode.OK, mapOf("active" to false)) - return@post + authenticate { + get("/test_protection") { + call.respond(call.principal() ?: "Claims not found") } - - // If the token exists, respond with token details - call.respond( - mapOf( - "active" to ((authToken.expiresIn ?: 1) > 0), - "client_id" to authToken.clientId, - "scope" to authToken.scope, - "exp" to (authToken.expiresIn ?: -1), - ) - ) } } }.start(wait = true) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt new file mode 100644 index 0000000..b5c34f3 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt @@ -0,0 +1,10 @@ +package app.simplecloud.controller.runtime.oauth + +data class OAuthUser( + val groups: List = emptyList(), + val scopes: List = emptyList(), + val userId: String, + val username: String, + val hashedPassword: String, + val tokenId: String? = null, +) \ No newline at end of file diff --git a/controller-shared/build.gradle.kts b/controller-shared/build.gradle.kts index d1cd2a2..334e1f5 100644 --- a/controller-shared/build.gradle.kts +++ b/controller-shared/build.gradle.kts @@ -4,4 +4,7 @@ dependencies { api(rootProject.libs.bundles.configurate) api(rootProject.libs.clikt) api(rootProject.libs.kotlin.coroutines) + api(libs.bundles.ktor) + api(libs.nimbus.jose.jwt) + implementation(libs.gson) } diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt index bf9c277..ec38e16 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt @@ -5,5 +5,6 @@ import io.grpc.Metadata object MetadataKeys { val AUTH_SECRET_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) + val SCOPES = Metadata.Key.of("Scopes", Metadata.ASCII_STRING_MARSHALLER) } \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt index 290c710..93c0276 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt @@ -2,23 +2,42 @@ package app.simplecloud.controller.shared.auth import app.simplecloud.controller.shared.MetadataKeys import io.grpc.* +import io.ktor.client.* +import kotlinx.coroutines.runBlocking class AuthSecretInterceptor( - private val secretKey: String + private val secretKey: String, + authHost: String, + authPort: Int, ) : ServerInterceptor { + private val oAuthIntrospector = OAuthIntrospector(secretKey, "http://$authHost:$authPort") + override fun interceptCall( call: ServerCall, headers: Metadata, next: ServerCallHandler ): ServerCall.Listener { val secretKey = headers.get(MetadataKeys.AUTH_SECRET_KEY) - if (this.secretKey != secretKey) { + if (secretKey == null) { call.close(Status.UNAUTHENTICATED, headers) return object : ServerCall.Listener() {} } - return Contexts.interceptCall(Context.current(), call, headers, next) + if (this.secretKey == secretKey) { + headers.put(MetadataKeys.SCOPES, "*") + return Contexts.interceptCall(Context.current(), call, headers, next) + } + return runBlocking { + val oAuthResult = oAuthIntrospector.introspect(secretKey) + if (oAuthResult == null) { + call.close(Status.UNAUTHENTICATED, headers) + return@runBlocking object : ServerCall.Listener() {} + } + headers.put(MetadataKeys.SCOPES, oAuthResult.getClaim("scope").toString()) + return@runBlocking Contexts.interceptCall(Context.current(), call, headers, next) + } + } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt similarity index 91% rename from controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt rename to controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt index c75f540..90c6394 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/JwtHandler.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt @@ -1,4 +1,4 @@ -package app.simplecloud.controller.runtime.oauth +package app.simplecloud.controller.shared.auth import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader @@ -38,4 +38,7 @@ class JwtHandler(private val secret: String, private val issuer: String) { return signedJWT.verify(verifier) } + fun decodeJwt(token: String): SignedJWT { + return SignedJWT.parse(token) + } } \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt new file mode 100644 index 0000000..27bdc98 --- /dev/null +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt @@ -0,0 +1,34 @@ +package app.simplecloud.controller.shared.auth + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.nimbusds.jwt.JWTClaimsSet +import io.ktor.client.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* + +class OAuthIntrospector(secret: String, private val issuer: String) { + private val client = HttpClient() + private val jwtHandler = JwtHandler(secret, issuer) + private val gson = Gson() + + suspend fun introspect(token: String): JWTClaimsSet? { + try { + val response = client.submitForm( + url = "$issuer/oauth/introspect", + formParameters = parameters { + append("token", token) + } + ) + val body = gson.fromJson(response.bodyAsText(), JsonObject::class.java) + return if (!response.status.isSuccess() || !body["active"].asBoolean) { + null + } else { + jwtHandler.decodeJwt(token).jwtClaimsSet + } + }catch (e: Exception) { + return null + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 205fd14..aabd1ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ sonatype-central-portal-publisher = "1.2.3" spotify-completablefutures = "0.3.6" ktor = "3.0.1" nimbus = "9.46" +gson = "2.7" [libraries] kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -61,6 +62,8 @@ ktor-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } + [bundles] From 7a71c0b0b9e63d613dc8fe6cb0103bb0f6835cb4 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 15 Nov 2024 16:21:40 +0100 Subject: [PATCH 23/65] fix: metrics --- .../controller/runtime/ControllerRuntime.kt | 35 ++++++++++++++----- .../launcher/ControllerStartCommand.kt | 6 ++-- .../controller/runtime/launcher/Launcher.kt | 1 + gradle/libs.versions.toml | 9 +++-- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 92518c9..7773d3e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -19,7 +19,6 @@ import io.grpc.Server import io.grpc.ServerBuilder import kotlinx.coroutines.* import org.apache.logging.log4j.LogManager -import kotlin.concurrent.thread class ControllerRuntime( private val controllerStartCommand: ControllerStartCommand @@ -45,13 +44,23 @@ class ControllerRuntime( private val server = createGrpcServer() private val pubSubServer = createPubSubGrpcServer() - fun start() { + suspend fun start() { setupDatabase() startPubSubGrpcServer() startGrpcServer() startReconciler() loadGroups() loadServers() + + suspendCancellableCoroutine { continuation -> + Runtime.getRuntime().addShutdownHook(Thread { + server.shutdown() + continuation.resume(Unit) { cause, _, _ -> + logger.info("Server shutdown due to: $cause") + } + }) + } + } private fun setupDatabase() { @@ -71,17 +80,27 @@ class ControllerRuntime( private fun startGrpcServer() { logger.info("Starting gRPC server...") - thread { - server.start() - server.awaitTermination() + CoroutineScope(Dispatchers.Default).launch { + try { + server.start() + server.awaitTermination() + } catch (e: Exception) { + logger.error("Error in gRPC server", e) + throw e + } } } private fun startPubSubGrpcServer() { logger.info("Starting pubsub gRPC server...") - thread { - pubSubServer.start() - pubSubServer.awaitTermination() + CoroutineScope(Dispatchers.Default).launch { + try { + pubSubServer.start() + pubSubServer.awaitTermination() + } catch (e: Exception) { + logger.error("Error in gRPC server", e) + throw e + } } } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 0478f85..c71b1f9 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -3,7 +3,7 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.controller.runtime.ControllerRuntime import app.simplecloud.controller.shared.secret.AuthFileSecretFactory import app.simplecloud.metrics.internal.api.MetricsCollector -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.command.SuspendingCliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.defaultLazy @@ -17,7 +17,7 @@ import java.nio.file.Path class ControllerStartCommand( private val metricsCollector: MetricsCollector? -) : CliktCommand() { +) : SuspendingCliktCommand() { init { context { @@ -63,7 +63,7 @@ class ControllerStartCommand( val forwardingSecret: String by option(help = "Forwarding secret", envvar = "FORWARDING_SECRET") .defaultLazy { AuthFileSecretFactory.loadOrCreate(forwardingSecretPath) } - override fun run() { + override suspend fun run() { if (trackMetrics) { metricsCollector?.start() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt index d5cca11..b7188c8 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt @@ -1,6 +1,7 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.metrics.internal.api.MetricsCollector +import com.github.ajalt.clikt.command.main import org.apache.logging.log4j.LogManager diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d33f7e..6fecc24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,8 @@ kotlin = "2.0.20" kotlinCoroutines = "1.9.0" shadow = "8.3.3" -log4j = "2.20.0" +log4j = "2.22.0" +slf4j = "2.0.16" protobuf = "3.25.2" grpc = "1.61.0" grpcKotlin = "1.4.1" @@ -12,7 +13,7 @@ simplecloud-metrics = "1.0.0" jooq = "3.19.3" configurate = "4.1.2" sqliteJdbc = "3.44.1.0" -clikt = "4.3.0" +clikt = "5.0.1" sonatypeCentralPortalPublisher = "1.2.3" spotifyCompletableFutures = "0.3.6" @@ -24,6 +25,7 @@ kotlinCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", v log4jCore = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4jApi = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4jSlf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } +slf4jApi = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } # Add this protobufKotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } @@ -54,7 +56,8 @@ spotifyCompletableFutures = { module = "com.spotify:completable-futures", versio log4j = [ "log4jCore", "log4jApi", - "log4jSlf4j" + "log4jSlf4j", + "slf4jApi" ] proto = [ "protobufKotlin", From 95ca9ed9a8be555f3b2b827988d325cef6ba4165 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Fri, 15 Nov 2024 21:50:21 +0100 Subject: [PATCH 24/65] feat: user groups, users, login --- controller-runtime/build.gradle.kts | 1 + controller-runtime/src/main/db/schema.sql | 3 +- .../runtime/oauth/AuthClientRepository.kt | 6 +- .../runtime/oauth/AuthGroupRepository.kt | 33 ++-- .../runtime/oauth/AuthTokenRepository.kt | 32 ++- .../runtime/oauth/AuthUserRepository.kt | 124 ++++++++++++ .../runtime/oauth/AuthenticationHandler.kt | 183 ++++++++++++++++-- .../runtime/oauth/AuthorizationHandler.kt | 23 ++- .../controller/runtime/oauth/OAuthClient.kt | 2 +- .../controller/runtime/oauth/OAuthServer.kt | 45 ++++- .../controller/runtime/oauth/OAuthToken.kt | 5 +- .../controller/runtime/oauth/OAuthUser.kt | 2 +- .../runtime/oauth/PasswordEncoder.kt | 15 ++ .../controller/runtime/oauth/Scope.kt | 33 ++++ .../controller/shared/MetadataKeys.kt | 4 +- .../shared/auth/AuthSecretInterceptor.kt | 14 +- .../controller/shared/auth/JwtHandler.kt | 18 +- gradle/libs.versions.toml | 3 + 18 files changed, 468 insertions(+), 78 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthUserRepository.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PasswordEncoder.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts index c78f690..c0b3bc7 100644 --- a/controller-runtime/build.gradle.kts +++ b/controller-runtime/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(libs.simplecloud.metrics) implementation(libs.bundles.log4j) implementation(libs.clikt) + implementation(libs.spring.crypto) implementation(libs.spotify.completablefutures) } diff --git a/controller-runtime/src/main/db/schema.sql b/controller-runtime/src/main/db/schema.sql index ab4a76a..1b2367e 100644 --- a/controller-runtime/src/main/db/schema.sql +++ b/controller-runtime/src/main/db/schema.sql @@ -41,9 +41,8 @@ CREATE TABLE IF NOT EXISTS oauth2_client_details CREATE TABLE IF NOT EXISTS oauth2_users ( user_id varchar PRIMARY KEY, - groups varchar, scopes varchar, - username varchar NOT NULL, + username varchar UNIQUE NOT NULL, hashed_password varchar NOT NULL ); diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt index c3ee0ba..84d0cc9 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt @@ -43,13 +43,13 @@ class AuthClientRepository( element.clientSecret, element.grantTypes, element.redirectUri, - element.scope, + element.scope.joinToString(";"), ).onDuplicateKeyUpdate() .set(OAUTH2_CLIENT_DETAILS.CLIENT_ID, element.clientId) .set(OAUTH2_CLIENT_DETAILS.CLIENT_SECRET, element.clientSecret) .set(OAUTH2_CLIENT_DETAILS.GRANT_TYPES, element.grantTypes) .set(OAUTH2_CLIENT_DETAILS.REDIRECT_URI, element.redirectUri) - .set(OAUTH2_CLIENT_DETAILS.SCOPE, element.scope) + .set(OAUTH2_CLIENT_DETAILS.SCOPE, element.scope.joinToString(";")) .executeAsync() } @@ -72,7 +72,7 @@ class AuthClientRepository( clientSecret = record.clientSecret!!, grantTypes = record.grantTypes!!, redirectUri = record.redirectUri, - scope = record.scope, + scope = Scope.fromString(record.scope ?: "", ";"), ) } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt index ad92a3b..678f00e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt @@ -13,16 +13,12 @@ import org.jooq.exception.DataAccessException class AuthGroupRepository(private val database: Database) : Repository { override suspend fun getAll(): List { - return database.context.selectFrom(OAUTH2_GROUPS) - .asFlow() - .toCollection(mutableListOf()) + return database.context.selectFrom(OAUTH2_GROUPS).asFlow().toCollection(mutableListOf()) .map { mapRecordToGroup(it) } } override suspend fun find(identifier: String): OAuthGroup? { - return database.context.selectFrom(OAUTH2_GROUPS) - .where(OAUTH2_GROUPS.GROUP_NAME.eq(identifier)) - .limit(1) + return database.context.selectFrom(OAUTH2_GROUPS).where(OAUTH2_GROUPS.GROUP_NAME.eq(identifier)).limit(1) .awaitFirstOrNull()?.let { mapRecordToGroup(it) } } @@ -30,23 +26,18 @@ class AuthGroupRepository(private val database: Database) : Repository { + + override suspend fun getAll(): List { + return database.context.selectFrom( + OAUTH2_USERS + ) + .asFlow().toCollection(mutableListOf()).map { mapRecordToUser(it) } + } + + override suspend fun find(identifier: String): OAuthUser? { + return database.context.selectFrom( + OAUTH2_USERS + ) + .where(OAUTH2_USERS.USER_ID.eq(identifier)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToUser(it) } + } + + suspend fun findByName(identifier: String): OAuthUser? { + return database.context.selectFrom( + OAUTH2_USERS + ) + .where(OAUTH2_USERS.USERNAME.eq(identifier)) + .limit(1) + .awaitFirstOrNull()?.let { mapRecordToUser(it) } + } + + + override fun save(element: OAuthUser) { + database.context.insertInto( + OAUTH2_USERS, + + OAUTH2_USERS.USER_ID, + OAUTH2_USERS.SCOPES, + OAUTH2_USERS.USERNAME, + OAUTH2_USERS.HASHED_PASSWORD, + ).values( + element.userId, + element.scopes.joinToString(";"), + element.username, + element.hashedPassword, + ).onDuplicateKeyUpdate() + .set(OAUTH2_USERS.USER_ID, element.userId) + .set(OAUTH2_USERS.SCOPES, element.scopes.joinToString(";")) + .set(OAUTH2_USERS.USERNAME, element.username) + .set(OAUTH2_USERS.HASHED_PASSWORD, element.hashedPassword) + .executeAsync() + database.context.deleteFrom(OAUTH2_USER_GROUPS).where(OAUTH2_USER_GROUPS.USER_ID.eq(element.userId)) + .executeAsync() + element.groups.forEach { + database.context.insertInto( + OAUTH2_USER_GROUPS, + + OAUTH2_USER_GROUPS.USER_ID, + OAUTH2_USER_GROUPS.GROUP_NAME, + ).values( + element.userId, + it.name, + ).onConflictDoNothing().executeAsync() + } + } + + override suspend fun delete(element: OAuthUser): Boolean { + return withContext(Dispatchers.IO) { + try { + database.context.deleteFrom(OAUTH2_USERS) + .where(OAUTH2_USERS.USER_ID.eq(element.userId)) + .execute() + return@withContext true + } catch (e: DataAccessException) { + return@withContext false + } + } + } + + private suspend fun mapRecordToUser( + record: Oauth2UsersRecord, + ): OAuthUser { + val token = getToken(record.userId!!) + val groups = getGroups(record.userId!!) + return OAuthUser( + scopes = Scope.fromString(record.scopes ?: ";"), + userId = record.userId!!, + username = record.username!!, + hashedPassword = record.hashedPassword!!, + token = token, + groups = groups + ) + } + + private suspend fun getToken(userId: String): OAuthToken? { + return database.context.selectFrom(OAUTH2_TOKENS).where(OAUTH2_TOKENS.USER_ID.eq(userId)).limit(1) + .awaitFirstOrNull() + ?.let { AuthTokenRepository.mapRecordToToken(it) } + } + + private suspend fun getGroups(userId: String): List { + return database.context.select(OAUTH2_USER_GROUPS, OAUTH2_USER_GROUPS.oauth2Groups()).from(OAUTH2_USER_GROUPS) + .where(OAUTH2_USER_GROUPS.USER_ID.eq(userId)) + .asFlow().toCollection(mutableListOf()).map { + if (it != null) { + return@map AuthGroupRepository.mapRecordToGroup(it.component2()) + } + return@map null + }.filterNotNull() + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt index 694dc4c..988b4b4 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -1,34 +1,61 @@ package app.simplecloud.controller.runtime.oauth +import app.simplecloud.controller.shared.auth.JwtHandler +import com.nimbusds.jwt.JWTClaimsSet import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import java.util.* class AuthenticationHandler( - private val groupRepository: AuthGroupRepository + private val groupRepository: AuthGroupRepository, + private val userRepository: AuthUserRepository, + private val tokenRepository: AuthTokenRepository, + private val jwtHandler: JwtHandler, ) { + + private suspend fun checkScope(call: RoutingCall, scope: String): Boolean { + val claims = call.receive() + val providedScope = Scope.fromString(claims.claims["scope"].toString()) + val requiredScope = Scope.fromString(scope) + if (!Scope.validate(requiredScope, providedScope)) { + call.respond(HttpStatusCode.Unauthorized) + return false + } + return true + } + suspend fun saveGroup(call: RoutingCall) { - //TODO: permission check val params = call.receiveParameters() val groupName = params["group_name"] if (groupName == null) { call.respond(HttpStatusCode.BadRequest, "You must specify a group name") return } - val scopes = params["scopes"]?.split(";") ?: emptyList() + if (!checkScope(call, "simplecloud.auth.group.save.$groupName")) { + return + } + val scopes = Scope.fromString(params["scopes"] ?: "") + if (!checkScope(call, scopes.joinToString(" "))) { + return + } groupRepository.save(OAuthGroup(scopes, groupName)) - call.respond("Group successfully updated") + call.respond("Group successfully saved") } suspend fun getGroup(call: RoutingCall) { - //TODO: permission check val group = loadGroup(call) ?: return + if (!checkScope(call, "simplecloud.auth.group.get.${group.name}")) { + return + } call.respond(mapOf("group_name" to group.name, "scope" to group.scopes.joinToString(" "))) } suspend fun getGroups(call: RoutingCall) { - //TODO: permission check + if (!checkScope(call, "simplecloud.auth.group.get.*")) { + return + } val groups = groupRepository.getAll() call.respond(listOf(groups.map { mapOf( @@ -39,8 +66,10 @@ class AuthenticationHandler( } suspend fun deleteGroup(call: RoutingCall) { - //TODO: permission check val group = loadGroup(call) ?: return + if (!checkScope(call, "simplecloud.auth.group.delete.${group.name}")) { + return + } groupRepository.delete(group) call.respond("Group successfully deleted") } @@ -60,22 +89,142 @@ class AuthenticationHandler( return group } - - suspend fun createUser() { - //TODO: permission check + suspend fun saveUser(call: RoutingCall) { + if (!checkScope(call, "simplecloud.auth.user.save")) { + return + } + val params = call.receiveParameters() + val username = params["user_name"] + val password = params["password"] + val groups = (params["groups"] ?: "").split(" ") + val scope = Scope.fromString(params["scope"] ?: "") + if (username == null || password == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a username or password") + return + } + val existing = userRepository.findByName(username) + val parsedGroups = groups.mapNotNull { group -> groupRepository.find(group) } + val updated = OAuthUser( + userId = existing?.userId ?: UUID.randomUUID().toString(), + groups = parsedGroups, + username = username, + scopes = scope, + hashedPassword = PasswordEncoder.hashPassword(password) + ) + userRepository.save(updated) + call.respond("User successfully saved") } - suspend fun updateUser() { - //TODO: permission check - + suspend fun getUser(call: RoutingCall) { + val params = call.receiveParameters() + val username = params["username"] + if (username == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a user id") + return + } + if (!checkScope(call, "simplecloud.auth.user.get.$username")) { + return + } + val user = userRepository.findByName(username) + if(user == null) { + call.respond(HttpStatusCode.NotFound, "User not found") + return + } + call.respond(mapOf( + "user_id" to user.userId, + "username" to user.username, + "scope" to user.scopes.joinToString(" "), + "groups" to user.groups.joinToString(" ") { group -> group.name } + )) } - suspend fun deleteUser() { - //TODO: permission check - + suspend fun getUsers(call: RoutingCall) { + if (!checkScope(call, "simplecloud.auth.user.get.*")) { + return + } + val users = userRepository.getAll() + call.respond(listOf(users.map { + mapOf( + "user_id" to it.userId, + "username" to it.username, + "scope" to it.scopes.joinToString(" "), + "groups" to it.groups.joinToString(" ") { group -> group.name } + ) + }).flatten()) } - suspend fun login() { + suspend fun deleteUser(call: RoutingCall) { + if (!checkScope(call, "simplecloud.auth.user.delete")) { + return + } + val params = call.receiveParameters() + val userId = params["user_id"] + if (userId == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a user id") + return + } + val user = userRepository.find(userId) + if (user == null) { + call.respond(HttpStatusCode.NotFound, "User not found") + return + } + userRepository.delete(user) + call.respond("User successfully deleted") + } + suspend fun login(call: RoutingCall) { + val params = call.receiveParameters() + val username = params["user_name"] + val password = params["password"] + if (username == null || password == null) { + call.respond(HttpStatusCode.BadRequest, "You must specify a username and password") + return + } + val user = userRepository.findByName(username) + if (user == null) { + call.respond(HttpStatusCode.Unauthorized, "Invalid username or password") + return + } + if (!PasswordEncoder.verifyPassword(password, user.hashedPassword)) { + call.respond(HttpStatusCode.Unauthorized, "Invalid username or password") + return + } + val token = tokenRepository.findByUserId(user.userId) + if (token?.expiresIn != null && token.expiresIn > 0) { + call.respond( + mapOf( + "access_token" to token.accessToken, + "scope" to token.scope, + "exp" to token.expiresIn, + ) + ) + return + } + val combinedScopes = mutableListOf() + combinedScopes.addAll(user.scopes) + user.groups.forEach { + combinedScopes.addAll(it.scopes) + } + val jwtToken = jwtHandler.generateJwtSigned( + user.userId, + 3600, + Scope.fromString(combinedScopes.joinToString(" ")).joinToString(" ") + ) + val newToken = OAuthToken( + id = UUID.randomUUID().toString(), + userId = user.userId, + accessToken = jwtToken, + expiresIn = 3600, + scope = combinedScopes.joinToString(" ") + ) + call.respond( + mapOf( + "access_token" to newToken.accessToken, + "scope" to newToken.scope, + "exp" to newToken.expiresIn, + "user_id" to newToken.userId, + "client_id" to newToken.clientId + ) + ) } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt index 5f2d3f7..8e408bf 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt @@ -16,7 +16,6 @@ class AuthorizationHandler( private val jwtHandler: JwtHandler, private val flowData: MutableMap> ) { - suspend fun registerClient(call: RoutingCall) { val params = call.receiveParameters() val providedMasterToken = params["master_token"] @@ -24,15 +23,20 @@ class AuthorizationHandler( call.respond(HttpStatusCode.Forbidden, "Invalid master token") return } + val clientId = params["client_id"] + if(clientId == null) { + call.respond(HttpStatusCode.BadRequest, "Client id is required") + return + } val redirectUri = params["redirect_uri"] val grantTypes = params["grant_types"] if (grantTypes == null) { call.respond(HttpStatusCode.BadRequest, "Invalid grant_types") return } - val scope = params["scope"] - val clientId = "client-${UUID.randomUUID().toString().replace("-", "").substring(0, 8)}" - val clientSecret = "secret-${UUID.randomUUID().toString().replace("-", "")}" + val scope = Scope.fromString(params["scope"] ?: "") + val providedSecret = params["client_secret"] + val clientSecret = providedSecret ?: "secret-${UUID.randomUUID().toString().replace("-", "")}" val client = OAuthClient(clientId, clientSecret, redirectUri, grantTypes, scope) clientRepository.save(client) call.respond(mapOf("client_id" to clientId, "client_secret" to clientSecret)) @@ -74,7 +78,7 @@ class AuthorizationHandler( return } - if (client.scope != null && !client.scope.contains(scope)) { + if (!client.scope.contains(scope)) { call.respond(HttpStatusCode.BadRequest, "This scope is not supported by the client") return } @@ -124,7 +128,7 @@ class AuthorizationHandler( val token = OAuthToken( id = UUID.randomUUID().toString(), clientId = clientId, - accessToken = jwtHandler.generateJwt( + accessToken = jwtHandler.generateJwtSigned( clientId, expiresIn = 3600, scope = flowData[code]?.get(2)!! @@ -138,6 +142,8 @@ class AuthorizationHandler( "access_token" to token.accessToken, "scope" to token.scope, "exp" to (token.expiresIn ?: -1), + "user_id" to token.userId, + "client_id" to token.clientId ) ) return @@ -148,11 +154,12 @@ class AuthorizationHandler( ) return } else if (client.grantTypes.contains("client_credentials")) { + val scope = client.scope.ifEmpty { listOf("*") } val token = OAuthToken( id = UUID.randomUUID().toString(), clientId = clientId, - accessToken = jwtHandler.generateJwt(clientId, scope = client.scope ?: "*"), - scope = client.scope ?: "*" + accessToken = jwtHandler.generateJwtSigned(clientId, scope = scope.joinToString(" ")), + scope = scope.joinToString(" ") ) tokenRepository.save(token) call.respond( diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt index a65f164..564fc3d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt @@ -5,5 +5,5 @@ data class OAuthClient( val clientSecret: String, val redirectUri: String? = null, val grantTypes: String, - val scope: String? = null, + val scope: List = emptyList(), ) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index 0a0516f..7256fc6 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -5,15 +5,12 @@ import app.simplecloud.controller.runtime.launcher.ControllerStartCommand import app.simplecloud.controller.shared.auth.JwtHandler import app.simplecloud.controller.shared.auth.OAuthIntrospector import com.fasterxml.jackson.databind.SerializationFeature -import com.nimbusds.jwt.JWTClaimsSet -import io.ktor.client.* import io.ktor.serialization.jackson.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.response.* import io.ktor.server.routing.* class OAuthServer(private val args: ControllerStartCommand, database: Database) { @@ -22,6 +19,8 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) private val jwtHandler = JwtHandler(secret, issuer) private val pkceHandler = PKCEHandler() private val clientRepository = AuthClientRepository(database) + private val groupRepository = AuthGroupRepository(database) + private val userRepository = AuthUserRepository(database) private val tokenRepository = AuthTokenRepository(database) //code to client_id, code_challenge and scope (this is in memory because it is only in use temporary) @@ -30,6 +29,8 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) private val authorizationHandler = AuthorizationHandler(secret, clientRepository, tokenRepository, pkceHandler, jwtHandler, flowData) + private val authenticationHandler = AuthenticationHandler(groupRepository, userRepository, tokenRepository, jwtHandler) + private val introspector = OAuthIntrospector(secret, issuer) @@ -75,8 +76,42 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) // AUTHENTICATION authenticate { - get("/test_protection") { - call.respond(call.principal() ?: "Claims not found") + // Save group endpoint + put("/group") { + authenticationHandler.saveGroup(call) + } + // Get group endpoint + get("/group") { + authenticationHandler.getGroup(call) + } + // Delete group endpoint + delete("/group") { + authenticationHandler.deleteGroup(call) + } + // Get all groups endpoint + get("/groups") { + authenticationHandler.getGroups(call) + } + + put("/user") { + authenticationHandler.saveUser(call) + } + + get("/user") { + authenticationHandler.getUser(call) + } + + get("/users") { + authenticationHandler.getUsers(call) + } + + delete("/user") { + authenticationHandler.deleteUser(call) + } + + //Login endpoint + post("/login") { + authenticationHandler.login(call) } } } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt index 95032fc..1d54912 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt @@ -2,8 +2,9 @@ package app.simplecloud.controller.runtime.oauth data class OAuthToken( val id: String, - val clientId: String, + val clientId: String? = null, val accessToken: String, val scope: String, - val expiresIn: Int? = null + val expiresIn: Int? = null, + val userId: String? = null ) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt index b5c34f3..265ac47 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt @@ -6,5 +6,5 @@ data class OAuthUser( val userId: String, val username: String, val hashedPassword: String, - val tokenId: String? = null, + val token: OAuthToken? = null, ) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PasswordEncoder.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PasswordEncoder.kt new file mode 100644 index 0000000..4406e85 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/PasswordEncoder.kt @@ -0,0 +1,15 @@ +package app.simplecloud.controller.runtime.oauth + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder + +object PasswordEncoder { + fun hashPassword(password: String): String { + val passwordEncoder = BCryptPasswordEncoder() + return passwordEncoder.encode(password) + } + + fun verifyPassword(password: String, hashedPassword: String): Boolean { + val passwordEncoder = BCryptPasswordEncoder() + return passwordEncoder.matches(password, hashedPassword) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt new file mode 100644 index 0000000..26000ed --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt @@ -0,0 +1,33 @@ +package app.simplecloud.controller.runtime.oauth + +object Scope { + + private fun toRegex(scope: String): String { + return "^${scope.trim().replace(".", "\\.").replace("*", ".+")}$" + } + + fun fromString(scope: String, delimiter: String = " "): List { + val added = mutableListOf() + scope.split(delimiter).distinct().forEach { toParse -> + if (added.any { Regex(toRegex(it)).matches(toParse) }) return@forEach + val regex = Regex(toRegex(toParse)) + added.toList().forEach { present -> + if(regex.matches(present)) { + added.remove(present) + } + } + added.add(toParse) + } + return added + } + + fun validate(requiredScope: List, providedScope: List): Boolean { + providedScope.forEach { provided -> + val regex = Regex(toRegex(provided)) + if (!requiredScope.any { required -> regex.matches(required) }) { + return false + } + } + return true + } +} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt index ec38e16..c418a42 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt @@ -1,10 +1,12 @@ package app.simplecloud.controller.shared +import com.nimbusds.jwt.JWTClaimsSet +import io.grpc.Context import io.grpc.Metadata object MetadataKeys { val AUTH_SECRET_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) - val SCOPES = Metadata.Key.of("Scopes", Metadata.ASCII_STRING_MARSHALLER) + val CLAIMS = Context.key("claims") } \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt index 93c0276..b893b09 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt @@ -11,7 +11,11 @@ class AuthSecretInterceptor( authPort: Int, ) : ServerInterceptor { - private val oAuthIntrospector = OAuthIntrospector(secretKey, "http://$authHost:$authPort") + private val issuer = "http://$authHost:$authPort" + + private val masterToken = JwtHandler(secretKey, issuer).generateJwt("internal", null, "*") + + private val oAuthIntrospector = OAuthIntrospector(secretKey, issuer) override fun interceptCall( call: ServerCall, @@ -25,8 +29,8 @@ class AuthSecretInterceptor( } if (this.secretKey == secretKey) { - headers.put(MetadataKeys.SCOPES, "*") - return Contexts.interceptCall(Context.current(), call, headers, next) + val forked = Context.current().withValue(MetadataKeys.CLAIMS, masterToken.jwtClaimsSet) + return Contexts.interceptCall(forked, call, headers, next) } return runBlocking { val oAuthResult = oAuthIntrospector.introspect(secretKey) @@ -34,8 +38,8 @@ class AuthSecretInterceptor( call.close(Status.UNAUTHENTICATED, headers) return@runBlocking object : ServerCall.Listener() {} } - headers.put(MetadataKeys.SCOPES, oAuthResult.getClaim("scope").toString()) - return@runBlocking Contexts.interceptCall(Context.current(), call, headers, next) + val forked = Context.current().withValue(MetadataKeys.CLAIMS, oAuthResult) + return@runBlocking Contexts.interceptCall(forked, call, headers, next) } } diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt index 90c6394..61c8cfc 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt @@ -14,17 +14,27 @@ class JwtHandler(private val secret: String, private val issuer: String) { * Generates a jwt token * @param subject the subject to sign * @param expiresIn time span in seconds or null if it should not expire - * @return the JWT token as a string + * @return the JWT token */ - fun generateJwt(subject: String, expiresIn: Int? = null, scope: String = ""): String { - val signer = MACSigner(secret.toByteArray()) + fun generateJwt(subject: String, expiresIn: Int? = null, scope: String = ""): SignedJWT { val claimsSet = JWTClaimsSet.Builder() .subject(subject) .claim("scope", scope) .issuer(issuer) if (expiresIn != null) claimsSet.expirationTime(Date(System.currentTimeMillis() + expiresIn * 1000L)) - val signedJWT = SignedJWT(JWSHeader(JWSAlgorithm.HS256), claimsSet.build()) + return SignedJWT(JWSHeader(JWSAlgorithm.HS256), claimsSet.build()) + } + + /** + * Generates a signed jwt token + * @param subject the subject to sign + * @param expiresIn time span in seconds or null if it should not expire + * @return the JWT token as a signed string + */ + fun generateJwtSigned(subject: String, expiresIn: Int? = null, scope: String = ""): String { + val signer = MACSigner(secret.toByteArray()) + val signedJWT = generateJwt(subject, expiresIn, scope) signedJWT.sign(signer) return signedJWT.serialize() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aabd1ef..4d81161 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ spotify-completablefutures = "0.3.6" ktor = "3.0.1" nimbus = "9.46" gson = "2.7" +spring-crypto = "6.3.4" [libraries] kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -64,6 +65,8 @@ nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimb gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +spring-crypto = { module = "org.springframework.security:spring-security-crypto", version.ref = "spring-crypto"} + [bundles] From 54f660b87da45afdf3ae017b8356153401a7169f Mon Sep 17 00:00:00 2001 From: dayeeet Date: Sat, 16 Nov 2024 14:10:28 +0100 Subject: [PATCH 25/65] fix: controller property loading --- .../controller/runtime/launcher/ControllerStartCommand.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 97973d4..0f7c972 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -12,6 +12,7 @@ import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.sources.PropertiesValueSource +import com.github.ajalt.clikt.sources.ValueSource import java.io.File import java.nio.file.Path @@ -21,7 +22,7 @@ class ControllerStartCommand( init { context { - valueSource = PropertiesValueSource.from(File("controller.properties")) + valueSource = PropertiesValueSource.from(File("controller.properties"), false, ValueSource.envvarKey()) } } From 5da1501e43f3116dbc29f105439e693a3f6b3499 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 18 Nov 2024 20:36:02 +0100 Subject: [PATCH 26/65] feat: oauth interception --- .../controller/runtime/ControllerRuntime.kt | 50 +++++++++++++------ .../launcher/ControllerStartCommand.kt | 13 ++--- .../controller/runtime/launcher/Launcher.kt | 13 ++--- .../runtime/oauth/AuthClientRepository.kt | 1 + .../runtime/oauth/AuthUserRepository.kt | 1 + .../runtime/oauth/AuthenticationHandler.kt | 13 ++--- .../runtime/oauth/AuthorizationHandler.kt | 14 +++++- .../controller/runtime/oauth/OAuthServer.kt | 2 +- .../controller/shared/MetadataKeys.kt | 3 +- .../shared/auth/AuthSecretInterceptor.kt | 12 +---- .../shared/auth/OAuthIntrospector.kt | 11 ++-- .../controller/shared/auth}/Scope.kt | 2 +- gradle/libs.versions.toml | 2 +- 13 files changed, 84 insertions(+), 53 deletions(-) rename {controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth => controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth}/Scope.kt (95%) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 34598fb..797b228 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -20,7 +20,6 @@ import io.grpc.Server import io.grpc.ServerBuilder import kotlinx.coroutines.* import org.apache.logging.log4j.LogManager -import kotlin.concurrent.thread class ControllerRuntime( private val controllerStartCommand: ControllerStartCommand @@ -47,7 +46,7 @@ class ControllerRuntime( private val server = createGrpcServer() private val pubSubServer = createPubSubGrpcServer() - fun start() { + suspend fun start() { setupDatabase() startAuthServer() startPubSubGrpcServer() @@ -55,13 +54,27 @@ class ControllerRuntime( startReconciler() loadGroups() loadServers() + + suspendCancellableCoroutine { continuation -> + Runtime.getRuntime().addShutdownHook(Thread { + server.shutdown() + continuation.resume(Unit) { cause, _, _ -> + logger.info("Server shutdown due to: $cause") + } + }) + } } private fun startAuthServer() { logger.info("Starting auth server...") - thread { - authServer.start() - logger.info("Auth server stopped.") + CoroutineScope(Dispatchers.Default).launch { + try { + authServer.start() + logger.info("Auth server stopped.") + }catch (e: Exception) { + logger.error("Error in gRPC server", e) + throw e + } } } @@ -83,18 +96,27 @@ class ControllerRuntime( private fun startGrpcServer() { logger.info("Starting gRPC server...") - thread { - server.start() - server.awaitTermination() - logger.info("GRPC Server stopped.") + CoroutineScope(Dispatchers.Default).launch { + try { + server.start() + server.awaitTermination() + } catch (e: Exception) { + logger.error("Error in gRPC server", e) + throw e + } } } private fun startPubSubGrpcServer() { logger.info("Starting pubsub gRPC server...") - thread { - pubSubServer.start() - pubSubServer.awaitTermination() + CoroutineScope(Dispatchers.Default).launch { + try { + pubSubServer.start() + pubSubServer.awaitTermination() + } catch (e: Exception) { + logger.error("Error in gRPC server", e) + throw e + } } } @@ -131,14 +153,14 @@ class ControllerRuntime( ) ) ) - .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret, controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) + .intercept(AuthSecretInterceptor(controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) .build() } private fun createPubSubGrpcServer(): Server { return ServerBuilder.forPort(controllerStartCommand.pubSubGrpcPort) .addService(pubSubService) - .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret, controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) + .intercept(AuthSecretInterceptor(controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) .build() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index ec32876..0a98ae4 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -3,7 +3,7 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.controller.runtime.ControllerRuntime import app.simplecloud.controller.shared.secret.AuthFileSecretFactory import app.simplecloud.metrics.internal.api.MetricsCollector -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.command.SuspendingCliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.defaultLazy @@ -12,16 +12,17 @@ import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.sources.PropertiesValueSource +import com.github.ajalt.clikt.sources.ValueSource import java.io.File import java.nio.file.Path class ControllerStartCommand( - //private val metricsCollector: MetricsCollector? -) : CliktCommand() { + private val metricsCollector: MetricsCollector? +) : SuspendingCliktCommand() { init { context { - valueSource = PropertiesValueSource.from(File("controller.properties")) + valueSource = PropertiesValueSource.from(File("controller.properties"), false, ValueSource.envvarKey()) } } @@ -58,9 +59,9 @@ class ControllerStartCommand( .boolean() .default(true) - override fun run() { + override suspend fun run() { if (trackMetrics) { - //metricsCollector?.start() + metricsCollector?.start() } val controllerRuntime = ControllerRuntime(this) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt index 35a79fd..8a3218a 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt @@ -1,29 +1,30 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.metrics.internal.api.MetricsCollector +import com.github.ajalt.clikt.command.main import org.apache.logging.log4j.LogManager suspend fun main(args: Array) { - /** val metricsCollector = try { + val metricsCollector = try { MetricsCollector.create("controller") } catch (e: Exception) { null - } */ + } configureLog4j( - // metricsCollector + metricsCollector ) ControllerStartCommand( - // metricsCollector + metricsCollector ).main(args) } fun configureLog4j( - // metricsCollector: MetricsCollector? + metricsCollector: MetricsCollector? ) { val globalExceptionHandlerLogger = LogManager.getLogger("GlobalExceptionHandler") Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - //metricsCollector?.recordError(throwable) + metricsCollector?.recordError(throwable) globalExceptionHandlerLogger.error("Uncaught exception in thread ${thread.name}", throwable) } } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt index 84d0cc9..0652ac9 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt @@ -2,6 +2,7 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.shared.auth.Scope import app.simplecloud.controller.shared.db.tables.records.Oauth2ClientDetailsRecord import app.simplecloud.controller.shared.db.tables.references.OAUTH2_CLIENT_DETAILS import kotlinx.coroutines.Dispatchers diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthUserRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthUserRepository.kt index 61e5e1a..21037d0 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthUserRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthUserRepository.kt @@ -2,6 +2,7 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.database.Database +import app.simplecloud.controller.shared.auth.Scope import app.simplecloud.controller.shared.db.tables.records.Oauth2UsersRecord import app.simplecloud.controller.shared.db.tables.references.OAUTH2_TOKENS import app.simplecloud.controller.shared.db.tables.references.OAUTH2_USERS diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt index 988b4b4..7aa34fa 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -1,6 +1,7 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.shared.auth.JwtHandler +import app.simplecloud.controller.shared.auth.Scope import com.nimbusds.jwt.JWTClaimsSet import io.ktor.http.* import io.ktor.server.request.* @@ -126,16 +127,16 @@ class AuthenticationHandler( return } val user = userRepository.findByName(username) - if(user == null) { + if (user == null) { call.respond(HttpStatusCode.NotFound, "User not found") return } call.respond(mapOf( - "user_id" to user.userId, - "username" to user.username, - "scope" to user.scopes.joinToString(" "), - "groups" to user.groups.joinToString(" ") { group -> group.name } - )) + "user_id" to user.userId, + "username" to user.username, + "scope" to user.scopes.joinToString(" "), + "groups" to user.groups.joinToString(" ") { group -> group.name } + )) } suspend fun getUsers(call: RoutingCall) { diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt index 8e408bf..a7b195e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt @@ -1,6 +1,7 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.shared.auth.JwtHandler +import app.simplecloud.controller.shared.auth.Scope import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* @@ -24,7 +25,7 @@ class AuthorizationHandler( return } val clientId = params["client_id"] - if(clientId == null) { + if (clientId == null) { call.respond(HttpStatusCode.BadRequest, "Client id is required") return } @@ -205,7 +206,16 @@ class AuthorizationHandler( call.respond(HttpStatusCode.BadRequest, "Token is missing") return } - + if (token == secret) { + call.respond( + mapOf( + "active" to true, + "scope" to "*", + "exp" to -1, + ), + ) + return + } val authToken = tokenRepository.findByAccessToken(token) if (authToken == null) { call.respond(HttpStatusCode.OK, mapOf("active" to false)) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index 7256fc6..3dc4d4a 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -31,7 +31,7 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) private val authenticationHandler = AuthenticationHandler(groupRepository, userRepository, tokenRepository, jwtHandler) - private val introspector = OAuthIntrospector(secret, issuer) + private val introspector = OAuthIntrospector(issuer) fun start() { diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt index c418a42..d442592 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt @@ -1,12 +1,11 @@ package app.simplecloud.controller.shared -import com.nimbusds.jwt.JWTClaimsSet import io.grpc.Context import io.grpc.Metadata object MetadataKeys { val AUTH_SECRET_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) - val CLAIMS = Context.key("claims") + val SCOPES = Context.key>("Scopes") } \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt index b893b09..080589c 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt @@ -6,16 +6,13 @@ import io.ktor.client.* import kotlinx.coroutines.runBlocking class AuthSecretInterceptor( - private val secretKey: String, authHost: String, authPort: Int, ) : ServerInterceptor { private val issuer = "http://$authHost:$authPort" - private val masterToken = JwtHandler(secretKey, issuer).generateJwt("internal", null, "*") - - private val oAuthIntrospector = OAuthIntrospector(secretKey, issuer) + private val oAuthIntrospector = OAuthIntrospector(issuer) override fun interceptCall( call: ServerCall, @@ -27,18 +24,13 @@ class AuthSecretInterceptor( call.close(Status.UNAUTHENTICATED, headers) return object : ServerCall.Listener() {} } - - if (this.secretKey == secretKey) { - val forked = Context.current().withValue(MetadataKeys.CLAIMS, masterToken.jwtClaimsSet) - return Contexts.interceptCall(forked, call, headers, next) - } return runBlocking { val oAuthResult = oAuthIntrospector.introspect(secretKey) if (oAuthResult == null) { call.close(Status.UNAUTHENTICATED, headers) return@runBlocking object : ServerCall.Listener() {} } - val forked = Context.current().withValue(MetadataKeys.CLAIMS, oAuthResult) + val forked = Context.current().withValue(MetadataKeys.SCOPES, oAuthResult) return@runBlocking Contexts.interceptCall(forked, call, headers, next) } diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt index 27bdc98..5708d3e 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt @@ -2,18 +2,21 @@ package app.simplecloud.controller.shared.auth import com.google.gson.Gson import com.google.gson.JsonObject +import com.google.gson.stream.JsonWriter import com.nimbusds.jwt.JWTClaimsSet import io.ktor.client.* import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* -class OAuthIntrospector(secret: String, private val issuer: String) { +class OAuthIntrospector(private val issuer: String) { private val client = HttpClient() - private val jwtHandler = JwtHandler(secret, issuer) private val gson = Gson() - suspend fun introspect(token: String): JWTClaimsSet? { + /** + * @return list of all scopes issued to this token, or null if the token does not exist + */ + suspend fun introspect(token: String): List? { try { val response = client.submitForm( url = "$issuer/oauth/introspect", @@ -25,7 +28,7 @@ class OAuthIntrospector(secret: String, private val issuer: String) { return if (!response.status.isSuccess() || !body["active"].asBoolean) { null } else { - jwtHandler.decodeJwt(token).jwtClaimsSet + Scope.fromString(body["scope"].asString) } }catch (e: Exception) { return null diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt similarity index 95% rename from controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt rename to controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt index 26000ed..56cb007 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/Scope.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt @@ -1,4 +1,4 @@ -package app.simplecloud.controller.runtime.oauth +package app.simplecloud.controller.shared.auth object Scope { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d81161..404ccd0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ simplecloud-metrics = "1.0.0" jooq = "3.19.3" configurate = "4.1.2" sqlite-jdbc = "3.44.1.0" -clikt = "4.3.0" +clikt = "5.0.1" sonatype-central-portal-publisher = "1.2.3" spotify-completablefutures = "0.3.6" ktor = "3.0.1" From 15b66dbc55b3da623215ae9174b3bc5764966121 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 18 Nov 2024 21:46:25 +0100 Subject: [PATCH 27/65] feat: auth docs --- auth.md | 460 ++++++++++++++++++ .../runtime/oauth/AuthenticationHandler.kt | 4 +- 2 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 auth.md diff --git a/auth.md b/auth.md new file mode 100644 index 0000000..dd797ad --- /dev/null +++ b/auth.md @@ -0,0 +1,460 @@ +# Auth Server API Documentation +This documentation provides docs for every authorization server endpoint. +Internally by the controller and every official droplet, auth tokens are used for authentication. No GRPC request +with invalid auth token is possible. + +# Restricted endpoints +For some endpoints, there are listed authorization scope requirements. +To access these endpoints, you must pass a token in the Authorization header that meets these requirements. + +### Header example +```json + { + "Authorization": "Bearer " + } +``` + +You can gain access to an auth token by using the master token, retrieving a token through user login (`/login`) +and by creating a custom client with the `client_credentials` method and calling the (`/token`) endpoint to log in with this client. + +# API Documentation for Authorization Endpoints + +The `AuthorizationHandler` provides various endpoints for client registration, authorization, token requests, revocation, and introspection. Below is a detailed description of each endpoint. + +--- + +## Register Client + +### Endpoint +`POST /oauth/register_client` + +### Description +Registers a new OAuth client, associating it with a unique client secret, grant types, and scope. + +### Request Parameters +- **`master_token`** (String, required): The master token for authentication (in the `.secrets/auth.secret` file). +- **`client_id`** (String, required): The unique identifier for the client. +- **`redirect_uri`** (String, required): The URI to redirect to after authorization. +- **`grant_types`** (String, required): The grant types supported by the client. +- **`scope`** (String, optional): The scopes the client has access to. +- **`client_secret`** (String, optional): The client secret. If not provided, a random one will be generated. + +### Authorization Scope Required +- None + +### Responses +- **200 OK**: The client was successfully registered. + ```json + { + "client_id": "client_id", + "client_secret": "client_secret" + } + ``` +- **400 Bad Request**: Missing required parameters such as `client_id` or `grant_types`. +- **403 Forbidden**: Invalid master token. + +--- + +## Authorize Request + +### Endpoint +`POST /oauth/authorize` + +### Description +Handles the authorization request for an OAuth client with PKCE (Proof Key for Code Exchange) support. + +### Request Parameters +- **`client_id`** (String, required): The unique identifier for the client. +- **`redirect_uri`** (String, required): The URI to redirect to after authorization. +- **`code_challenge_method`** (String, required): The challenge method. Must be `S256`. +- **`code_challenge`** (String, required): The PKCE code challenge. +- **`scope`** (String, required): The requested scope. + +### Authorization Scope Required +- None + +### Responses +- **200 OK**: Authorization successful. + ```json + { + "redirectUri": "?code=" + } + ``` +- **400 Bad Request**: Missing required parameters such as `client_id`, `redirect_uri`, `scope`, or `code_challenge`. +- **404 Not Found**: Client not found. +- **400 Bad Request**: Invalid challenge or unsupported grant types. + +--- + +## Token Request + +### Endpoint +`POST /oauth/token` + +### Description +Handles the token request to exchange the authorization code for an access token or to generate a client credentials token. + +### Request Parameters +- **`client_id`** (String, required): The unique identifier for the client. +- **`client_secret`** (String, required): The client secret. +- **`code`** (String, required if using authorization code flow): The authorization code. +- **`code_verifier`** (String, required if using PKCE): The PKCE code verifier. + +### Authorization Scope Required +- None + +### Responses +- **200 OK**: Token successfully issued. + ```json + { + "access_token": "access_token", + "scope": "scope", + "exp": "expiration_time", + "user_id": "user_id", + "client_id": "client_id" + } + ``` +- **400 Bad Request**: Missing required parameters such as `client_id` or `client_secret`. +- **404 Not Found**: Client not found. +- **400 Bad Request**: Invalid client secret or unsupported grant type. + +--- + +## Revoke Token + +### Endpoint +`POST /oauth/revoke` + +### Description +Revokes an OAuth token, rendering it inactive. + +### Request Parameters +- **`access_token`** (String, required): The access token to revoke. + +### Authorization Scope Required +- None + +### Responses +- **200 OK**: The token was successfully revoked. +- **400 Bad Request**: Invalid access token. +- **500 Internal Server Error**: Could not delete token. + +--- + +## Introspect Token + +### Endpoint +`POST /oauth/introspect` + +### Description +Introspects a token to verify its validity and return token details. + +### Request Parameters +- **`token`** (String, required): The token to introspect. + +### Authorization Scope Required +- None + +### Responses +- **200 OK**: Token is valid and active. + ```json + { + "active": true, + "token_id": "token_id", + "client_id": "client_id", + "scope": "scope", + "exp": "expiration_time" + } + ``` +- **200 OK**: Token is invalid or expired. + ```json + { + "active": false + } + ``` +- **400 Bad Request**: Token is missing. + +# Authentication Endpoints + +The `AuthenticationHandler` provides various endpoints to manage OAuth groups, users, and tokens. Below is a detailed +description of each endpoint. + +--- + +## Save Group + +### Endpoint + +`PUT /group` + +### Description + +Creates or updates an OAuth group with the specified scopes. + +### Request Parameters + +- **`group_name`** (String, required): Name of the group. +- **`scopes`** (String, optional): Space-separated list of scopes for the group. + +### Authorization Scope Required + +- `simplecloud.auth.group.save.` + +### Responses + +- **200 OK**: Success message. +- **400 Bad Request**: You must specify a group name. +- **401 Unauthorized**: Unauthorized. + +--- + +## Get Group + +### Endpoint + +`GET /group` + +### Description + +Retrieves details of a specific OAuth group. + +### Request Parameters + +- **`group_name`** (String, required): Name of the group to retrieve. + +### Authorization Scope Required + +- `simplecloud.auth.group.get.` + +### Responses + +- **200 OK**: Group details. + ```json + { + "group_name": "example_group", + "scope": "read write" + } + ``` +- **400 Bad Request**: You must specify a group name. +- **404 Not Found**: Group not found. +- **401 Unauthorized**: Unauthorized. + +--- + +## Get All Groups + +### Endpoint + +`GET /groups` + +### Description + +Fetches a list of all OAuth groups. + +### Authorization Scope Required + +- `simplecloud.auth.group.get.*` + +### Responses + +- **200 OK**: List of all groups. + ```json + [ + { + "group_name": "group1", + "scope": "read write" + }, + { + "group_name": "group2", + "scope": "read" + } + ] + ``` +- **401 Unauthorized**: Unauthorized. + +--- + +## Delete Group + +### Endpoint + +`DELETE /group` + +### Description + +Deletes a specific OAuth group. + +### Request Parameters + +- **`group_name`** (String, required): Name of the group to delete. + +### Authorization Scope Required + +- `simplecloud.auth.group.delete.` + +### Responses + +- **200 OK**: Success message. +- **400 Bad Request**: You must specify a group name. +- **404 Not Found**: Group not found. +- **401 Unauthorized**: Unauthorized. + +--- + +## Save User + +### Endpoint + +`PUT /user` + +### Description + +Creates or updates a user with the specified groups and scopes. + +### Request Parameters + +- **`username`** (String, required): The username. +- **`password`** (String, required): The password. +- **`groups`** (String, optional): Space-separated list of groups the user belongs to. +- **`scope`** (String, optional): Space-separated list of scopes for the user. + +### Authorization Scope Required + +- `simplecloud.auth.user.save` + +### Responses + +- **200 OK**: Success message. +- **400 Bad Request**: You must specify a username or password. +- **401 Unauthorized**: Unauthorized. + +--- + +## Get User + +### Endpoint + +`GET /user` + +### Description + +Fetches details of a specific user. + +### Request Parameters + +- **`username`** (String, required): Name of the user to retrieve. + +### Authorization Scope Required + +- `simplecloud.auth.user.get.` + +### Responses + +- **200 OK**: User details. + ```json + { + "user_id": "1234", + "username": "example_user", + "scope": "read write", + "groups": "group1 group2" + } + ``` +- **400 Bad Request**: You must specify a username. +- **404 Not Found**: User not found. +- **401 Unauthorized**: Unauthorized. + +--- + +## Get All Users + +### Endpoint + +`GET /users` + +### Description + +Fetches a list of all users. + +### Authorization Scope Required + +- `simplecloud.auth.user.get.*` + +### Responses + +- **200 OK**: List of all users. + ```json + [ + { + "user_id": "1234", + "username": "user1", + "scope": "read write", + "groups": "group1 group2" + }, + { + "user_id": "5678", + "username": "user2", + "scope": "read", + "groups": "group3" + } + ] + ``` +- **401 Unauthorized**: Unauthorized. + +--- + +## Delete User + +### Endpoint + +`DELETE /user` + +### Description + +Deletes a specific user. + +### Request Parameters + +- **`user_id`** (String, required): The user ID to delete. + +### Authorization Scope Required + +- `simplecloud.auth.user.delete` + +### Responses + +- **200 OK**: Success message. +- **400 Bad Request**: You must specify a user ID. +- **404 Not Found**: User not found. +- **401 Unauthorized**: Unauthorized. + +--- + +## Login + +### Endpoint + +`POST /login` + +### Description + +Authenticates a user and returns an access token if the username and password are valid. + +### Request Parameters + +- **`username`** (String, required): The username. +- **`password`** (String, required): The password. + +### Responses + +- **200 OK**: Access token and user details. + ```json + { + "access_token": "jwt_token", + "scope": "read write", + "exp": 3600, + "user_id": "1234", + "client_id": "abcd" + } + ``` +- **400 Bad Request**: You must specify a username and password. +- **401 Unauthorized**: Invalid username or password. diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt index 7aa34fa..3cf080e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -95,7 +95,7 @@ class AuthenticationHandler( return } val params = call.receiveParameters() - val username = params["user_name"] + val username = params["username"] val password = params["password"] val groups = (params["groups"] ?: "").split(" ") val scope = Scope.fromString(params["scope"] ?: "") @@ -175,7 +175,7 @@ class AuthenticationHandler( suspend fun login(call: RoutingCall) { val params = call.receiveParameters() - val username = params["user_name"] + val username = params["username"] val password = params["password"] if (username == null || password == null) { call.respond(HttpStatusCode.BadRequest, "You must specify a username and password") From 72c5b5a56defa7e0afede87776fbde9198798581 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 18 Nov 2024 21:57:26 +0100 Subject: [PATCH 28/65] refactor: update auth docs --- auth.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/auth.md b/auth.md index dd797ad..99b0009 100644 --- a/auth.md +++ b/auth.md @@ -3,6 +3,25 @@ This documentation provides docs for every authorization server endpoint. Internally by the controller and every official droplet, auth tokens are used for authentication. No GRPC request with invalid auth token is possible. +# Scoping +Scoping is our way to deal with permissions. Scopes are represented in a string, seperated by a whitespace. +They can also be wildcarded (`*`). + +**These scopes:** + - `test.*` + - `other.test` + +**will be passed as:** `test.* other.test` on auth server rest endpoints. + +The scope `*` will grant every permission. + +# Getting scopes in a GRPC context +You can get the scopes that are provided to the current context (but ony if this context uses the v3 controllers `AuthSecretInterceptor`) like this: + +```kt +val scopes = MetadataKeys.SCOPES.get() // List +``` + # Restricted endpoints For some endpoints, there are listed authorization scope requirements. To access these endpoints, you must pass a token in the Authorization header that meets these requirements. From cc8e5bc4eff326481d86f3f01f5c0c79d9798a9e Mon Sep 17 00:00:00 2001 From: David <65951425+dayyeeet@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:59:15 +0100 Subject: [PATCH 29/65] refactor: update auth docs --- auth.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth.md b/auth.md index 99b0009..9462188 100644 --- a/auth.md +++ b/auth.md @@ -4,8 +4,8 @@ Internally by the controller and every official droplet, auth tokens are used fo with invalid auth token is possible. # Scoping -Scoping is our way to deal with permissions. Scopes are represented in a string, seperated by a whitespace. -They can also be wildcarded (`*`). +Scoping is the OAuth way to deal with permissions. Scopes are represented in a string, seperated by a whitespace. +In our case, they can also be wildcarded (`*`). **These scopes:** - `test.*` From d596a2a89e62e25cd7d6afcffd5b478bf7ddd27d Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 2 Dec 2024 17:05:03 +0100 Subject: [PATCH 30/65] feat: add client retrieval functionality --- .../runtime/oauth/AuthorizationHandler.kt | 54 +++++++++++++++++-- .../controller/runtime/oauth/OAuthServer.kt | 5 ++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt index a7b195e..c83f675 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt @@ -3,7 +3,6 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.shared.auth.JwtHandler import app.simplecloud.controller.shared.auth.Scope import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -40,10 +39,55 @@ class AuthorizationHandler( val clientSecret = providedSecret ?: "secret-${UUID.randomUUID().toString().replace("-", "")}" val client = OAuthClient(clientId, clientSecret, redirectUri, grantTypes, scope) clientRepository.save(client) - call.respond(mapOf("client_id" to clientId, "client_secret" to clientSecret)) + call.respond( + mapOf( + "client_id" to clientId, + "client_secret" to clientSecret, + "scope" to client.scope.joinToString(" "), + "grant_types" to client.grantTypes, + "redirect_uri" to client.redirectUri, + ) + ) + } + + suspend fun getClient(call: RoutingCall) { + val params = call.receiveParameters() + val clientId = params["client_id"] + if (clientId == null) { + call.respond(HttpStatusCode.BadRequest, "You must provide a valid client_id") + return + } + val masterToken = params["master_token"] + val clientSecret = params["client_secret"] + if (masterToken == null && clientSecret == null) { + call.respond(HttpStatusCode.BadRequest, "You must provide either a valid master_token or client_secret") + return + } + if (masterToken != null && secret != masterToken) { + call.respond(HttpStatusCode.BadRequest, "You must provide either a valid master_token or client_secret") + return + } + val client = clientRepository.find(clientId) + if (client == null) { + call.respond(HttpStatusCode.BadRequest, "You must provide a valid client_id") + return + } + if (masterToken == null && client.clientSecret != clientSecret) { + call.respond(HttpStatusCode.BadRequest, "You must provide either a valid master_token or client_secret") + return + } + call.respond( + mapOf( + "client_id" to clientId, + "client_secret" to clientSecret, + "scope" to client.scope.joinToString(" "), + "grant_types" to client.grantTypes, + "redirect_uri" to client.redirectUri, + ) + ) } - suspend fun authorizeRequest(call: ApplicationCall) { + suspend fun authorizeRequest(call: RoutingCall) { val params = call.receiveParameters() val clientId = params["client_id"] val redirectUri = params["redirect_uri"] @@ -89,7 +133,7 @@ class AuthorizationHandler( call.respond(mapOf("redirectUri" to "$redirectUri?code=$authorizationCode")) } - suspend fun tokenRequest(call: ApplicationCall) { + suspend fun tokenRequest(call: RoutingCall) { val params = call.receiveParameters() val clientId = params["client_id"] val clientSecret = params["client_secret"] @@ -199,7 +243,7 @@ class AuthorizationHandler( } - suspend fun introspectRequest(call: ApplicationCall) { + suspend fun introspectRequest(call: RoutingCall) { val params = call.receiveParameters() val token = params["token"] if (token == null) { diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index 3dc4d4a..0d7e868 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -56,6 +56,11 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) post("/oauth/register_client") { authorizationHandler.registerClient(call) } + + // Client retrieval endpoint + get("/oauth/client") { + authorizationHandler.getClient(call) + } // Authorization endpoint (simulating authorization code flow) post("/oauth/authorize") { authorizationHandler.authorizeRequest(call) From 8fe8278eef38dd5eaeb856f3a0ecf963d0c0f9b0 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 3 Dec 2024 17:48:44 +0100 Subject: [PATCH 31/65] refactor: migrate common files to droplet-api and fix formatting --- .../controller/runtime/ControllerRuntime.kt | 6 +-- .../runtime/oauth/AuthClientRepository.kt | 3 +- .../runtime/oauth/AuthGroupRepository.kt | 1 + .../runtime/oauth/AuthTokenRepository.kt | 10 ++-- .../runtime/oauth/AuthUserRepository.kt | 5 +- .../runtime/oauth/AuthenticationHandler.kt | 5 +- .../runtime/oauth/AuthorizationHandler.kt | 6 ++- .../controller/runtime/oauth/OAuthClient.kt | 9 ---- .../controller/runtime/oauth/OAuthGroup.kt | 6 --- .../controller/runtime/oauth/OAuthServer.kt | 7 +-- .../controller/runtime/oauth/OAuthToken.kt | 10 ---- .../controller/runtime/oauth/OAuthUser.kt | 10 ---- controller-shared/build.gradle.kts | 5 +- .../controller/shared/MetadataKeys.kt | 11 ---- .../shared/auth/AuthCallCredentials.kt | 24 --------- .../shared/auth/AuthSecretInterceptor.kt | 39 -------------- .../controller/shared/auth/JwtHandler.kt | 54 ------------------- .../shared/auth/OAuthIntrospector.kt | 37 ------------- .../controller/shared/auth/Scope.kt | 33 ------------ .../shared/future/ListenableFutureAdapter.kt | 41 -------------- .../future/ListenableFutureExtension.kt | 8 --- .../shared/secret/AuthFileSecretFactory.kt | 28 ---------- .../shared/secret/SecretGenerator.kt | 15 ------ .../shared/time/ProtoBufTimestamp.kt | 21 -------- gradle/libs.versions.toml | 5 +- 25 files changed, 30 insertions(+), 369 deletions(-) delete mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt delete mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt delete mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt delete mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthCallCredentials.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureAdapter.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureExtension.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/AuthFileSecretFactory.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/SecretGenerator.kt delete mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/time/ProtoBufTimestamp.kt diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 797b228..701d003 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -10,8 +10,8 @@ import app.simplecloud.controller.runtime.reconciler.Reconciler import app.simplecloud.controller.runtime.server.ServerNumericalIdRepository import app.simplecloud.controller.runtime.server.ServerRepository import app.simplecloud.controller.runtime.server.ServerService -import app.simplecloud.controller.shared.auth.AuthCallCredentials -import app.simplecloud.controller.shared.auth.AuthSecretInterceptor +import app.simplecloud.droplet.api.auth.AuthCallCredentials +import app.simplecloud.droplet.api.auth.AuthSecretInterceptor import app.simplecloud.pubsub.PubSubClient import app.simplecloud.pubsub.PubSubService import io.grpc.ManagedChannel @@ -71,7 +71,7 @@ class ControllerRuntime( try { authServer.start() logger.info("Auth server stopped.") - }catch (e: Exception) { + } catch (e: Exception) { logger.error("Error in gRPC server", e) throw e } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt index 0652ac9..359bdf1 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthClientRepository.kt @@ -2,9 +2,10 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.database.Database -import app.simplecloud.controller.shared.auth.Scope import app.simplecloud.controller.shared.db.tables.records.Oauth2ClientDetailsRecord import app.simplecloud.controller.shared.db.tables.references.OAUTH2_CLIENT_DETAILS +import app.simplecloud.droplet.api.auth.OAuthClient +import app.simplecloud.droplet.api.auth.Scope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.reactive.asFlow diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt index 678f00e..c4812c8 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthGroupRepository.kt @@ -4,6 +4,7 @@ import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.database.Database import app.simplecloud.controller.shared.db.tables.records.Oauth2GroupsRecord import app.simplecloud.controller.shared.db.tables.references.OAUTH2_GROUPS +import app.simplecloud.droplet.api.auth.OAuthGroup import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.reactive.asFlow diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt index 6559a6c..661eab0 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthTokenRepository.kt @@ -4,6 +4,7 @@ import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.database.Database import app.simplecloud.controller.shared.db.tables.records.Oauth2TokensRecord import app.simplecloud.controller.shared.db.tables.references.OAUTH2_TOKENS +import app.simplecloud.droplet.api.auth.OAuthToken import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.reactive.asFlow @@ -13,7 +14,7 @@ import org.jooq.exception.DataAccessException import java.time.Duration import java.time.LocalDateTime -class AuthTokenRepository(private val database: Database): Repository { +class AuthTokenRepository(private val database: Database) : Repository { override suspend fun getAll(): List { return database.context.selectFrom(OAUTH2_TOKENS) .asFlow() @@ -57,14 +58,17 @@ class AuthTokenRepository(private val database: Database): Repository 0) { + if (token?.expiresIn != null && token.expiresIn!! > 0) { call.respond( mapOf( "access_token" to token.accessToken, diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt index c83f675..fb80374 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthorizationHandler.kt @@ -1,7 +1,9 @@ package app.simplecloud.controller.runtime.oauth -import app.simplecloud.controller.shared.auth.JwtHandler -import app.simplecloud.controller.shared.auth.Scope +import app.simplecloud.droplet.api.auth.JwtHandler +import app.simplecloud.droplet.api.auth.OAuthClient +import app.simplecloud.droplet.api.auth.OAuthToken +import app.simplecloud.droplet.api.auth.Scope import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt deleted file mode 100644 index 564fc3d..0000000 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthClient.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.controller.runtime.oauth - -data class OAuthClient( - val clientId: String, - val clientSecret: String, - val redirectUri: String? = null, - val grantTypes: String, - val scope: List = emptyList(), -) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt deleted file mode 100644 index 3fe95d6..0000000 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthGroup.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.simplecloud.controller.runtime.oauth - -data class OAuthGroup( - val scopes: List = emptyList(), - val name: String, -) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index 0d7e868..6ff8a2b 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -2,8 +2,8 @@ package app.simplecloud.controller.runtime.oauth import app.simplecloud.controller.runtime.database.Database import app.simplecloud.controller.runtime.launcher.ControllerStartCommand -import app.simplecloud.controller.shared.auth.JwtHandler -import app.simplecloud.controller.shared.auth.OAuthIntrospector +import app.simplecloud.droplet.api.auth.JwtHandler +import app.simplecloud.droplet.api.auth.OAuthIntrospector import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.serialization.jackson.* import io.ktor.server.application.* @@ -29,7 +29,8 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) private val authorizationHandler = AuthorizationHandler(secret, clientRepository, tokenRepository, pkceHandler, jwtHandler, flowData) - private val authenticationHandler = AuthenticationHandler(groupRepository, userRepository, tokenRepository, jwtHandler) + private val authenticationHandler = + AuthenticationHandler(groupRepository, userRepository, tokenRepository, jwtHandler) private val introspector = OAuthIntrospector(issuer) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt deleted file mode 100644 index 1d54912..0000000 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthToken.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.simplecloud.controller.runtime.oauth - -data class OAuthToken( - val id: String, - val clientId: String? = null, - val accessToken: String, - val scope: String, - val expiresIn: Int? = null, - val userId: String? = null -) \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt deleted file mode 100644 index 265ac47..0000000 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthUser.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.simplecloud.controller.runtime.oauth - -data class OAuthUser( - val groups: List = emptyList(), - val scopes: List = emptyList(), - val userId: String, - val username: String, - val hashedPassword: String, - val token: OAuthToken? = null, -) \ No newline at end of file diff --git a/controller-shared/build.gradle.kts b/controller-shared/build.gradle.kts index 334e1f5..4998624 100644 --- a/controller-shared/build.gradle.kts +++ b/controller-shared/build.gradle.kts @@ -1,10 +1,7 @@ dependencies { - api(rootProject.libs.bundles.proto) api(rootProject.libs.simplecloud.pubsub) api(rootProject.libs.bundles.configurate) api(rootProject.libs.clikt) api(rootProject.libs.kotlin.coroutines) - api(libs.bundles.ktor) - api(libs.nimbus.jose.jwt) - implementation(libs.gson) + api(rootProject.libs.simplecloud.droplet.api) } diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt deleted file mode 100644 index d442592..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.simplecloud.controller.shared - -import io.grpc.Context -import io.grpc.Metadata - -object MetadataKeys { - - val AUTH_SECRET_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER) - val SCOPES = Context.key>("Scopes") - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthCallCredentials.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthCallCredentials.kt deleted file mode 100644 index 8ffa976..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthCallCredentials.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.simplecloud.controller.shared.auth - -import app.simplecloud.controller.shared.MetadataKeys -import io.grpc.CallCredentials -import io.grpc.Metadata -import java.util.concurrent.Executor - -class AuthCallCredentials( - private val secretKey: String -): CallCredentials() { - - override fun applyRequestMetadata( - requestInfo: RequestInfo, - appExecutor: Executor, - applier: MetadataApplier - ) { - appExecutor.execute { - val headers = Metadata() - headers.put(MetadataKeys.AUTH_SECRET_KEY, secretKey) - applier.apply(headers) - } - } - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt deleted file mode 100644 index 080589c..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.simplecloud.controller.shared.auth - -import app.simplecloud.controller.shared.MetadataKeys -import io.grpc.* -import io.ktor.client.* -import kotlinx.coroutines.runBlocking - -class AuthSecretInterceptor( - authHost: String, - authPort: Int, -) : ServerInterceptor { - - private val issuer = "http://$authHost:$authPort" - - private val oAuthIntrospector = OAuthIntrospector(issuer) - - override fun interceptCall( - call: ServerCall, - headers: Metadata, - next: ServerCallHandler - ): ServerCall.Listener { - val secretKey = headers.get(MetadataKeys.AUTH_SECRET_KEY) - if (secretKey == null) { - call.close(Status.UNAUTHENTICATED, headers) - return object : ServerCall.Listener() {} - } - return runBlocking { - val oAuthResult = oAuthIntrospector.introspect(secretKey) - if (oAuthResult == null) { - call.close(Status.UNAUTHENTICATED, headers) - return@runBlocking object : ServerCall.Listener() {} - } - val forked = Context.current().withValue(MetadataKeys.SCOPES, oAuthResult) - return@runBlocking Contexts.interceptCall(forked, call, headers, next) - } - - } - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt deleted file mode 100644 index 61c8cfc..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/JwtHandler.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.simplecloud.controller.shared.auth - -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.crypto.MACSigner -import com.nimbusds.jose.crypto.MACVerifier -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT -import java.util.* - -class JwtHandler(private val secret: String, private val issuer: String) { - - /** - * Generates a jwt token - * @param subject the subject to sign - * @param expiresIn time span in seconds or null if it should not expire - * @return the JWT token - */ - fun generateJwt(subject: String, expiresIn: Int? = null, scope: String = ""): SignedJWT { - val claimsSet = JWTClaimsSet.Builder() - .subject(subject) - .claim("scope", scope) - .issuer(issuer) - if (expiresIn != null) - claimsSet.expirationTime(Date(System.currentTimeMillis() + expiresIn * 1000L)) - return SignedJWT(JWSHeader(JWSAlgorithm.HS256), claimsSet.build()) - } - - /** - * Generates a signed jwt token - * @param subject the subject to sign - * @param expiresIn time span in seconds or null if it should not expire - * @return the JWT token as a signed string - */ - fun generateJwtSigned(subject: String, expiresIn: Int? = null, scope: String = ""): String { - val signer = MACSigner(secret.toByteArray()) - val signedJWT = generateJwt(subject, expiresIn, scope) - signedJWT.sign(signer) - return signedJWT.serialize() - } - - /** - * @return Whether the provided token was signed by this handler or not - */ - fun verifyJwt(token: String): Boolean { - val signedJWT = SignedJWT.parse(token) - val verifier = MACVerifier(secret.toByteArray()) - return signedJWT.verify(verifier) - } - - fun decodeJwt(token: String): SignedJWT { - return SignedJWT.parse(token) - } -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt deleted file mode 100644 index 5708d3e..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/OAuthIntrospector.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.simplecloud.controller.shared.auth - -import com.google.gson.Gson -import com.google.gson.JsonObject -import com.google.gson.stream.JsonWriter -import com.nimbusds.jwt.JWTClaimsSet -import io.ktor.client.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* - -class OAuthIntrospector(private val issuer: String) { - private val client = HttpClient() - private val gson = Gson() - - /** - * @return list of all scopes issued to this token, or null if the token does not exist - */ - suspend fun introspect(token: String): List? { - try { - val response = client.submitForm( - url = "$issuer/oauth/introspect", - formParameters = parameters { - append("token", token) - } - ) - val body = gson.fromJson(response.bodyAsText(), JsonObject::class.java) - return if (!response.status.isSuccess() || !body["active"].asBoolean) { - null - } else { - Scope.fromString(body["scope"].asString) - } - }catch (e: Exception) { - return null - } - } -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt deleted file mode 100644 index 56cb007..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/Scope.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.simplecloud.controller.shared.auth - -object Scope { - - private fun toRegex(scope: String): String { - return "^${scope.trim().replace(".", "\\.").replace("*", ".+")}$" - } - - fun fromString(scope: String, delimiter: String = " "): List { - val added = mutableListOf() - scope.split(delimiter).distinct().forEach { toParse -> - if (added.any { Regex(toRegex(it)).matches(toParse) }) return@forEach - val regex = Regex(toRegex(toParse)) - added.toList().forEach { present -> - if(regex.matches(present)) { - added.remove(present) - } - } - added.add(toParse) - } - return added - } - - fun validate(requiredScope: List, providedScope: List): Boolean { - providedScope.forEach { provided -> - val regex = Regex(toRegex(provided)) - if (!requiredScope.any { required -> regex.matches(required) }) { - return false - } - } - return true - } -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureAdapter.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureAdapter.kt deleted file mode 100644 index 013f43d..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.simplecloud.controller.shared.future - -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ForkJoinPool - - -class ListenableFutureAdapter( - val listenableFuture: ListenableFuture -) { - - val completableFuture: CompletableFuture = object : CompletableFuture() { - override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - val cancelled = listenableFuture.cancel(mayInterruptIfRunning) - super.cancel(cancelled) - return cancelled - } - } - - init { - Futures.addCallback(listenableFuture, object : FutureCallback { - override fun onSuccess(result: T) { - completableFuture.complete(result) - } - - override fun onFailure(ex: Throwable) { - completableFuture.completeExceptionally(ex) - } - }, ForkJoinPool.commonPool()) - } - - companion object { - fun toCompletable(listenableFuture: ListenableFuture): CompletableFuture { - val listenableFutureAdapter: ListenableFutureAdapter = ListenableFutureAdapter(listenableFuture) - return listenableFutureAdapter.completableFuture - } - } - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureExtension.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureExtension.kt deleted file mode 100644 index f12685d..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/future/ListenableFutureExtension.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.simplecloud.controller.shared.future - -import com.google.common.util.concurrent.ListenableFuture -import java.util.concurrent.CompletableFuture - -fun ListenableFuture.toCompletable(): CompletableFuture { - return ListenableFutureAdapter.toCompletable(this) -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/AuthFileSecretFactory.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/AuthFileSecretFactory.kt deleted file mode 100644 index bd0d540..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/AuthFileSecretFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.simplecloud.controller.shared.secret - -import java.nio.file.Files -import java.nio.file.Path - -object AuthFileSecretFactory { - - fun loadOrCreate(path: Path): String { - if (!Files.exists(path)) { - return create(path) - } - - return Files.readString(path) - } - - - private fun create(path: Path): String { - val secret = SecretGenerator.generate() - - if (!Files.exists(path)) { - path.parent?.let { Files.createDirectories(it) } - Files.writeString(path, secret) - } - - return secret - } - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/SecretGenerator.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/SecretGenerator.kt deleted file mode 100644 index 8c18bd3..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/SecretGenerator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.simplecloud.controller.shared.secret - -import java.security.SecureRandom -import java.util.* - -object SecretGenerator { - - fun generate(size: Int = 64): String { - val random = SecureRandom() - val bytes = ByteArray(size) - random.nextBytes(bytes) - return Base64.getEncoder().encodeToString(bytes) - } - -} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/time/ProtoBufTimestamp.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/time/ProtoBufTimestamp.kt deleted file mode 100644 index f2c5d39..0000000 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/time/ProtoBufTimestamp.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.simplecloud.controller.shared.time - -import com.google.protobuf.Timestamp -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId - -object ProtoBufTimestamp { - fun toLocalDateTime(timestamp: Timestamp): LocalDateTime { - return LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp.seconds, timestamp.nanos.toLong()), ZoneId.systemDefault()) - } - - fun fromLocalDateTime(localDateTime: LocalDateTime): Timestamp { - val instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant() - - return Timestamp.newBuilder() - .setSeconds(instant.epochSecond) - .setNanos(instant.nano) - .build() - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 404ccd0..63838ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ log4j = "2.20.0" protobuf = "3.25.2" grpc = "1.61.0" grpc-kotlin = "1.4.1" -simplecloud-protospecs = "1.4.1.1.20241108142018.a431612a8826" +droplet-api = "0.0.1-dev.66efe83" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" @@ -36,7 +36,7 @@ grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-ko grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } -simplecloud-protospecs = { module = "build.buf.gen:simplecloud_proto-specs_grpc_kotlin", version.ref = "simplecloud-protospecs" } +simplecloud-droplet-api = { module = "app.simplecloud.droplet.api:droplet-api", version.ref = "droplet-api" } simplecloud-pubsub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simplecloud-pubsub" } simplecloud-metrics = { module = "app.simplecloud:internal-metrics-api", version.ref = "simplecloud-metrics" } @@ -81,7 +81,6 @@ proto = [ "grpc-kotlin-stub", "grpc-protobuf", "grpc-netty-shaded", - "simplecloud-protospecs", ] jooq = [ "jooq", From 22135da5fb811172e82f8251ff9a9dfae9454cd3 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 3 Dec 2024 17:54:34 +0100 Subject: [PATCH 32/65] refactor: fix errors --- .../coroutines/ControllerApiCoroutineImpl.kt | 2 +- .../impl/coroutines/GroupApiCoroutineImpl.kt | 2 +- .../impl/coroutines/ServerApiCoroutineImpl.kt | 2 +- .../impl/future/ControllerApiFutureImpl.kt | 2 +- .../api/impl/future/GroupApiFutureImpl.kt | 4 ++-- .../api/impl/future/ServerApiFutureImpl.kt | 4 ++-- .../launcher/ControllerStartCommand.kt | 2 +- .../runtime/reconciler/GroupReconciler.kt | 2 +- .../runtime/reconciler/Reconciler.kt | 2 +- .../runtime/server/ServerService.kt | 20 +++++++++---------- .../controller/shared/host/ServerHost.kt | 2 +- .../controller/shared/server/Server.kt | 10 +++++----- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ControllerApiCoroutineImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ControllerApiCoroutineImpl.kt index ae4ac39..7c2ca0e 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ControllerApiCoroutineImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ControllerApiCoroutineImpl.kt @@ -3,7 +3,7 @@ package app.simplecloud.controller.api.impl.coroutines import app.simplecloud.controller.api.ControllerApi import app.simplecloud.controller.api.GroupApi import app.simplecloud.controller.api.ServerApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials +import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.pubsub.PubSubClient import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/GroupApiCoroutineImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/GroupApiCoroutineImpl.kt index 091b3aa..5a111c6 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/GroupApiCoroutineImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/GroupApiCoroutineImpl.kt @@ -1,8 +1,8 @@ package app.simplecloud.controller.api.impl.coroutines import app.simplecloud.controller.api.GroupApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials import app.simplecloud.controller.shared.group.Group +import app.simplecloud.droplet.api.auth.AuthCallCredentials import build.buf.gen.simplecloud.controller.v1.* import io.grpc.ManagedChannel diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt index c8e1f3f..c41b314 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt @@ -1,10 +1,10 @@ package app.simplecloud.controller.api.impl.coroutines import app.simplecloud.controller.api.ServerApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials import app.simplecloud.controller.shared.group.Group import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server +import app.simplecloud.droplet.api.auth.AuthCallCredentials import io.grpc.ManagedChannel class ServerApiCoroutineImpl( diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ControllerApiFutureImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ControllerApiFutureImpl.kt index c72b56a..2837267 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ControllerApiFutureImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ControllerApiFutureImpl.kt @@ -3,7 +3,7 @@ package app.simplecloud.controller.api.impl.future import app.simplecloud.controller.api.ControllerApi import app.simplecloud.controller.api.GroupApi import app.simplecloud.controller.api.ServerApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials +import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.pubsub.PubSubClient import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/GroupApiFutureImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/GroupApiFutureImpl.kt index 5119bc6..86f4670 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/GroupApiFutureImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/GroupApiFutureImpl.kt @@ -1,9 +1,9 @@ package app.simplecloud.controller.api.impl.future import app.simplecloud.controller.api.GroupApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials -import app.simplecloud.controller.shared.future.toCompletable import app.simplecloud.controller.shared.group.Group +import app.simplecloud.droplet.api.auth.AuthCallCredentials +import app.simplecloud.droplet.api.future.toCompletable import build.buf.gen.simplecloud.controller.v1.ControllerGroupServiceGrpc import build.buf.gen.simplecloud.controller.v1.CreateGroupRequest import build.buf.gen.simplecloud.controller.v1.DeleteGroupByNameRequest diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt index 65a6a9b..c106d59 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt @@ -1,11 +1,11 @@ package app.simplecloud.controller.api.impl.future import app.simplecloud.controller.api.ServerApi -import app.simplecloud.controller.shared.auth.AuthCallCredentials -import app.simplecloud.controller.shared.future.toCompletable import app.simplecloud.controller.shared.group.Group import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server +import app.simplecloud.droplet.api.auth.AuthCallCredentials +import app.simplecloud.droplet.api.future.toCompletable import io.grpc.ManagedChannel import java.util.concurrent.CompletableFuture diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index 0a98ae4..dccc336 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -1,7 +1,7 @@ package app.simplecloud.controller.runtime.launcher import app.simplecloud.controller.runtime.ControllerRuntime -import app.simplecloud.controller.shared.secret.AuthFileSecretFactory +import app.simplecloud.droplet.api.secret.AuthFileSecretFactory import app.simplecloud.metrics.internal.api.MetricsCollector import com.github.ajalt.clikt.command.SuspendingCliktCommand import com.github.ajalt.clikt.core.context diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt index 039a255..8b562cb 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt @@ -3,9 +3,9 @@ package app.simplecloud.controller.runtime.reconciler import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.runtime.server.ServerNumericalIdRepository import app.simplecloud.controller.runtime.server.ServerRepository -import app.simplecloud.controller.shared.future.toCompletable import app.simplecloud.controller.shared.group.Group import app.simplecloud.controller.shared.server.Server +import app.simplecloud.droplet.api.future.toCompletable import build.buf.gen.simplecloud.controller.v1.* import build.buf.gen.simplecloud.controller.v1.ControllerServerServiceGrpc.ControllerServerServiceFutureStub import kotlinx.coroutines.runBlocking diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/Reconciler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/Reconciler.kt index 27398b9..c114192 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/Reconciler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/Reconciler.kt @@ -4,7 +4,7 @@ import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.runtime.server.ServerNumericalIdRepository import app.simplecloud.controller.runtime.server.ServerRepository -import app.simplecloud.controller.shared.auth.AuthCallCredentials +import app.simplecloud.droplet.api.auth.AuthCallCredentials import build.buf.gen.simplecloud.controller.v1.ControllerServerServiceGrpc import io.grpc.ManagedChannel diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 5f7191f..3cae638 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -2,11 +2,11 @@ package app.simplecloud.controller.runtime.server import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.host.ServerHostRepository -import app.simplecloud.controller.shared.auth.AuthCallCredentials import app.simplecloud.controller.shared.group.Group import app.simplecloud.controller.shared.host.ServerHost import app.simplecloud.controller.shared.server.Server -import app.simplecloud.controller.shared.time.ProtoBufTimestamp +import app.simplecloud.droplet.api.auth.AuthCallCredentials +import app.simplecloud.droplet.api.time.ProtobufTimestamp import app.simplecloud.pubsub.PubSubClient import build.buf.gen.simplecloud.controller.v1.* import io.grpc.Status @@ -86,7 +86,7 @@ class ServerService( pubSubClient.publish( "event", ServerUpdateEvent.newBuilder() - .setUpdatedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setServerBefore(before.toDefinition()).setServerAfter(request.server).build() ) serverRepository.save(server) @@ -111,7 +111,7 @@ class ServerService( pubSubClient.publish( "event", ServerStopEvent.newBuilder() .setServer(request.server) - .setStoppedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setStoppedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setStopCause(ServerStopCause.NATURAL_STOP) .setTerminationMode(ServerTerminationMode.UNKNOWN_MODE) //TODO: Add proto fields to make changing this possible .build() @@ -148,7 +148,7 @@ class ServerService( pubSubClient.publish( "event", ServerStartEvent.newBuilder() .setServer(server) - .setStartedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setStartedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setStartCause(request.startCause) .build() ) @@ -191,8 +191,8 @@ class ServerService( .setMaximumMemory(group.maxMemory) .setServerState(ServerState.PREPARING) .setMaxPlayers(group.maxPlayers) - .setCreatedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) - .setUpdatedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setCreatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setPlayerCount(0) .setUniqueId(UUID.randomUUID().toString().replace("-", "")).putAllCloudProperties( mapOf( @@ -224,7 +224,7 @@ class ServerService( pubSubClient.publish( "event", ServerStopEvent.newBuilder() .setServer(stopped) - .setStoppedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setStoppedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setStopCause(cause) .setTerminationMode(ServerTerminationMode.UNKNOWN_MODE) //TODO: Add proto fields to make changing this possible .build() @@ -245,7 +245,7 @@ class ServerService( serverRepository.save(server) pubSubClient.publish( "event", - ServerUpdateEvent.newBuilder().setUpdatedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + ServerUpdateEvent.newBuilder().setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setServerBefore(serverBefore.toDefinition()).setServerAfter(server.toDefinition()).build() ) return server.toDefinition() @@ -259,7 +259,7 @@ class ServerService( serverRepository.save(server) pubSubClient.publish( "event", - ServerUpdateEvent.newBuilder().setUpdatedAt(ProtoBufTimestamp.fromLocalDateTime(LocalDateTime.now())) + ServerUpdateEvent.newBuilder().setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setServerBefore(serverBefore.toDefinition()).setServerAfter(server.toDefinition()).build() ) return server.toDefinition() diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt index 5e52a29..d7db03c 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt @@ -1,6 +1,6 @@ package app.simplecloud.controller.shared.host -import app.simplecloud.controller.shared.auth.AuthCallCredentials +import app.simplecloud.droplet.api.auth.AuthCallCredentials import build.buf.gen.simplecloud.controller.v1.ServerHostDefinition import build.buf.gen.simplecloud.controller.v1.ServerHostServiceGrpcKt import io.grpc.ManagedChannel diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt index 58f70cf..8e7bbf6 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt @@ -1,6 +1,6 @@ package app.simplecloud.controller.shared.server -import app.simplecloud.controller.shared.time.ProtoBufTimestamp +import app.simplecloud.droplet.api.time.ProtobufTimestamp import build.buf.gen.simplecloud.controller.v1.ServerDefinition import build.buf.gen.simplecloud.controller.v1.ServerState import build.buf.gen.simplecloud.controller.v1.ServerType @@ -40,8 +40,8 @@ data class Server( .setMaxPlayers(maxPlayers) .putAllCloudProperties(properties) .setNumericalId(numericalId) - .setCreatedAt(ProtoBufTimestamp.fromLocalDateTime(createdAt)) - .setUpdatedAt(ProtoBufTimestamp.fromLocalDateTime(updatedAt)) + .setCreatedAt(ProtobufTimestamp.fromLocalDateTime(createdAt)) + .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(updatedAt)) .build() } @@ -83,8 +83,8 @@ data class Server( serverDefinition.playerCount, serverDefinition.cloudPropertiesMap, serverDefinition.serverState, - ProtoBufTimestamp.toLocalDateTime(serverDefinition.createdAt), - ProtoBufTimestamp.toLocalDateTime(serverDefinition.updatedAt), + ProtobufTimestamp.toLocalDateTime(serverDefinition.createdAt), + ProtobufTimestamp.toLocalDateTime(serverDefinition.updatedAt), ) } From d45dc73e40d5278c6eb7757b70c74aa3750f0c03 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 9 Dec 2024 16:15:21 +0100 Subject: [PATCH 33/65] fix: blocking auth server --- .../app/simplecloud/controller/runtime/ControllerRuntime.kt | 1 - .../app/simplecloud/controller/runtime/oauth/OAuthServer.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 701d003..4ba2f16 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -70,7 +70,6 @@ class ControllerRuntime( CoroutineScope(Dispatchers.Default).launch { try { authServer.start() - logger.info("Auth server stopped.") } catch (e: Exception) { logger.error("Error in gRPC server", e) throw e diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index 6ff8a2b..ec95be6 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -121,6 +121,6 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) } } } - }.start(wait = true) + }.start(wait = false) } } \ No newline at end of file From 95b0fba3c85d06bfdc034f9585fae8f74bffc151 Mon Sep 17 00:00:00 2001 From: Kaseax Date: Mon, 9 Dec 2024 20:04:58 +0100 Subject: [PATCH 34/65] refactor: add memory validation on group creation --- .../controller/runtime/group/GroupService.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt index 201933a..8c7ff6e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupService.kt @@ -34,6 +34,11 @@ class GroupService( groupRepository.find(request.group.name) ?: throw StatusException(Status.NOT_FOUND.withDescription("This group does not exist")) val group = Group.fromDefinition(request.group) + + if (group.minMemory > group.maxMemory) { + throw StatusException(Status.INVALID_ARGUMENT.withDescription("Minimum memory must be smaller than maximum memory")) + } + try { groupRepository.save(group) } catch (e: Exception) { @@ -47,6 +52,11 @@ class GroupService( throw StatusException(Status.NOT_FOUND.withDescription("This group already exists")) } val group = Group.fromDefinition(request.group) + + if (group.minMemory > group.maxMemory) { + throw StatusException(Status.INVALID_ARGUMENT.withDescription("Minimum memory must be smaller than maximum memory")) + } + try { groupRepository.save(group) } catch (e: Exception) { From ab473ef42fe167d9601fdb2f83429445cedde790 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Tue, 10 Dec 2024 22:38:04 +0100 Subject: [PATCH 35/65] refactor: bump controller to new droplet api --- gradle/libs.versions.toml | 40 +-------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63838ba..5ce5e2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,10 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -protobuf = "3.25.2" -grpc = "1.61.0" -grpc-kotlin = "1.4.1" -droplet-api = "0.0.1-dev.66efe83" +droplet-api = "0.0.1-dev.27d7043" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" @@ -15,9 +12,6 @@ sqlite-jdbc = "3.44.1.0" clikt = "5.0.1" sonatype-central-portal-publisher = "1.2.3" spotify-completablefutures = "0.3.6" -ktor = "3.0.1" -nimbus = "9.46" -gson = "2.7" spring-crypto = "6.3.4" [libraries] @@ -29,12 +23,6 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } -protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } - -grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } -grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-kotlin" } -grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } -grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } simplecloud-droplet-api = { module = "app.simplecloud.droplet.api:droplet-api", version.ref = "droplet-api" } simplecloud-pubsub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simplecloud-pubsub" } @@ -54,17 +42,6 @@ clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } spotify-completablefutures = { module = "com.spotify:completable-futures", version.ref = "spotify-completablefutures" } -ktor-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } -ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } -ktor-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" } -ktor-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" } -ktor-jackson = { module = "io.ktor:ktor-serialization-jackson", version.ref = "ktor" } -ktor-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } - -nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" } - -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } - spring-crypto = { module = "org.springframework.security:spring-security-crypto", version.ref = "spring-crypto"} @@ -75,13 +52,6 @@ log4j = [ "log4j-api", "log4j-slf4j" ] -proto = [ - "protobuf-kotlin", - "grpc-stub", - "grpc-kotlin-stub", - "grpc-protobuf", - "grpc-netty-shaded", -] jooq = [ "jooq", "jooq-meta", @@ -91,14 +61,6 @@ configurate = [ "configurate-yaml", "configurate-extra-kotlin" ] -ktor = [ - "ktor-core", - "ktor-netty", - "ktor-auth", - "ktor-auth-jwt", - "ktor-content-negotiation", - "ktor-jackson" -] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } From e3e27fcc7b71076202e7ae7e86296221b630b30f Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 11 Dec 2024 10:42:27 +0100 Subject: [PATCH 36/65] fix: coroutines dispatcher --- .../simplecloud/controller/runtime/ControllerRuntime.kt | 9 +++++---- .../controller/runtime/YamlDirectoryRepository.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 4ba2f16..4df9018 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -47,6 +47,7 @@ class ControllerRuntime( private val pubSubServer = createPubSubGrpcServer() suspend fun start() { + logger.info("Starting controller") setupDatabase() startAuthServer() startPubSubGrpcServer() @@ -67,7 +68,7 @@ class ControllerRuntime( private fun startAuthServer() { logger.info("Starting auth server...") - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.IO).launch { try { authServer.start() } catch (e: Exception) { @@ -95,7 +96,7 @@ class ControllerRuntime( private fun startGrpcServer() { logger.info("Starting gRPC server...") - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.IO).launch { try { server.start() server.awaitTermination() @@ -108,7 +109,7 @@ class ControllerRuntime( private fun startPubSubGrpcServer() { logger.info("Starting pubsub gRPC server...") - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.IO).launch { try { pubSubServer.start() pubSubServer.awaitTermination() @@ -170,7 +171,7 @@ class ControllerRuntime( } private fun startReconcilerJob(): Job { - return CoroutineScope(Dispatchers.Default).launch { + return CoroutineScope(Dispatchers.IO).launch { while (isActive) { reconciler.reconcile() delay(2000L) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt index 3197a0c..20759dc 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt @@ -101,7 +101,7 @@ abstract class YamlDirectoryRepository( StandardWatchEventKinds.ENTRY_MODIFY ) - return CoroutineScope(Dispatchers.Default).launch { + return CoroutineScope(Dispatchers.IO).launch { while (isActive) { val key = watchService.take() for (event in key.pollEvents()) { From 87b4f69bcffae8c5674c554f43cd3bf25170a314 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 11 Dec 2024 10:50:09 +0100 Subject: [PATCH 37/65] Revert "fix: blocking auth server" This reverts commit d45dc73e40d5278c6eb7757b70c74aa3750f0c03. --- .../app/simplecloud/controller/runtime/ControllerRuntime.kt | 1 + .../app/simplecloud/controller/runtime/oauth/OAuthServer.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 4df9018..b06e823 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -71,6 +71,7 @@ class ControllerRuntime( CoroutineScope(Dispatchers.IO).launch { try { authServer.start() + logger.info("Auth server stopped.") } catch (e: Exception) { logger.error("Error in gRPC server", e) throw e diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt index ec95be6..6ff8a2b 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/OAuthServer.kt @@ -121,6 +121,6 @@ class OAuthServer(private val args: ControllerStartCommand, database: Database) } } } - }.start(wait = false) + }.start(wait = true) } } \ No newline at end of file From 5b10b1d439b0b1457a56c0496fd306c9acd62e69 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 02:30:41 +0100 Subject: [PATCH 38/65] feat: snapshot creation and ads server initialization --- controller-runtime/build.gradle.kts | 1 + .../controller/runtime/ControllerRuntime.kt | 4 + .../droplet/ControllerDropletService.kt | 7 + .../runtime/droplet/DropletRepository.kt | 34 ++++ .../runtime/envoy/ControlPlaneServer.kt | 23 +++ .../controller/runtime/envoy/DropletCache.kt | 164 ++++++++++++++++++ .../runtime/envoy/SimpleCloudNodeGroup.kt | 22 +++ .../envoy/SimpleCloudResponseTracker.kt | 14 ++ gradle/libs.versions.toml | 7 +- 9 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudNodeGroup.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts index c0b3bc7..d44a354 100644 --- a/controller-runtime/build.gradle.kts +++ b/controller-runtime/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(libs.clikt) implementation(libs.spring.crypto) implementation(libs.spotify.completablefutures) + implementation(libs.envoy.controlplane) } application { diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index b06e823..eedc100 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -1,6 +1,8 @@ package app.simplecloud.controller.runtime import app.simplecloud.controller.runtime.database.DatabaseFactory +import app.simplecloud.controller.runtime.droplet.ControllerDropletService +import app.simplecloud.controller.runtime.droplet.DropletRepository import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.group.GroupService import app.simplecloud.controller.runtime.host.ServerHostRepository @@ -29,6 +31,7 @@ class ControllerRuntime( private val database = DatabaseFactory.createDatabase(controllerStartCommand.databaseUrl) private val authCallCredentials = AuthCallCredentials(controllerStartCommand.authSecret) + private val dropletRepository = DropletRepository() private val groupRepository = GroupRepository(controllerStartCommand.groupPath) private val numericalIdRepository = ServerNumericalIdRepository() private val serverRepository = ServerRepository(database, numericalIdRepository) @@ -154,6 +157,7 @@ class ControllerRuntime( ) ) ) + .addService(ControllerDropletService(dropletRepository)) .intercept(AuthSecretInterceptor(controllerStartCommand.grpcHost, controllerStartCommand.authorizationPort)) .build() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt new file mode 100644 index 0000000..3fe4f75 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt @@ -0,0 +1,7 @@ +package app.simplecloud.controller.runtime.droplet + +import build.buf.gen.simplecloud.controller.v1.ControllerDropletServiceGrpcKt + +class ControllerDropletService(private val dropletRepository: DropletRepository) : + ControllerDropletServiceGrpcKt.ControllerDropletServiceCoroutineImplBase() { +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt new file mode 100644 index 0000000..a24093f --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt @@ -0,0 +1,34 @@ +package app.simplecloud.controller.runtime.droplet + +import app.simplecloud.controller.runtime.Repository +import app.simplecloud.droplet.api.droplet.Droplet + +class DropletRepository : Repository { + + private val currentDroplets = mutableListOf() + + override suspend fun getAll(): List { + return currentDroplets + } + + override suspend fun find(identifier: String): Droplet? { + return currentDroplets.firstOrNull { it.id == identifier } + } + + fun find(type: String, identifier: String): Droplet? { + return currentDroplets.firstOrNull { it.type == type && it.id == identifier } + } + + override fun save(element: Droplet) { + val droplet = find(element.type, element.id) + if (droplet != null) { + currentDroplets[currentDroplets.indexOf(droplet)] = element + return + } + currentDroplets.add(element) + } + + override suspend fun delete(element: Droplet): Boolean { + return currentDroplets.remove(element) + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt new file mode 100644 index 0000000..8f94b1c --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt @@ -0,0 +1,23 @@ +package app.simplecloud.controller.runtime.envoy + +import app.simplecloud.controller.runtime.droplet.DropletRepository +import io.envoyproxy.controlplane.cache.ConfigWatcher +import io.envoyproxy.controlplane.server.V3DiscoveryServer +import io.grpc.ServerBuilder +import org.apache.logging.log4j.LogManager + +class ControlPlaneServer(dropletRepository: DropletRepository) { + private val cache = DropletCache(dropletRepository) + private val server = V3DiscoveryServer(cache.getCache()) + private val logger = LogManager.getLogger(ControlPlaneServer::class) + + fun register(builder: ServerBuilder<*>) { + logger.info("Registering envoy control plane server...") + builder.addService(server.aggregatedDiscoveryServiceImpl) + logger.info("Registered envoy control plane server.") + } + + fun getCache(): ConfigWatcher { + return cache.getCache() + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt new file mode 100644 index 0000000..f8dd799 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -0,0 +1,164 @@ +package app.simplecloud.controller.runtime.envoy + +import app.simplecloud.controller.runtime.droplet.DropletRepository +import com.google.protobuf.Any +import com.google.protobuf.Duration +import com.google.protobuf.UInt32Value +import io.envoyproxy.controlplane.cache.ConfigWatcher +import io.envoyproxy.controlplane.cache.Resources +import io.envoyproxy.controlplane.cache.Watch +import io.envoyproxy.controlplane.cache.XdsRequest +import io.envoyproxy.controlplane.cache.v3.SimpleCache +import io.envoyproxy.controlplane.cache.v3.Snapshot +import io.envoyproxy.envoy.config.cluster.v3.Cluster +import io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig +import io.envoyproxy.envoy.config.core.v3.* +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint +import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints +import io.envoyproxy.envoy.config.listener.v3.Filter +import io.envoyproxy.envoy.config.listener.v3.FilterChain +import io.envoyproxy.envoy.config.listener.v3.Listener +import io.envoyproxy.envoy.config.route.v3.* +import io.envoyproxy.envoy.extensions.filters.http.connect_grpc_bridge.v3.FilterConfig +import io.envoyproxy.envoy.extensions.filters.http.grpc_web.v3.GrpcWeb +import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter +import io.envoyproxy.envoy.extensions.upstreams.http.v3.HttpProtocolOptions +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.* + +class DropletCache(private val dropletRepository: DropletRepository) { + private val cache = SimpleCache(SimpleCloudNodeGroup()) + private var watch: Watch = cache.createWatch( + true, + XdsRequest.create( + DiscoveryRequest.newBuilder().setNode(Node.getDefaultInstance()) + .setTypeUrl(Resources.V3.ENDPOINT_TYPE_URL).addResourceNames("none").build() + ), + Collections.emptySet(), + SimpleCloudResponseTracker() + ) + + init { + CoroutineScope(Dispatchers.IO).launch { + update() + } + } + + suspend fun update() { + //Gets the simplecloud group, if not found throws an error + val group = cache.groups().firstOrNull() ?: throw IllegalArgumentException("Group not found") + cache.setSnapshot( + group, + Snapshot.create( + createClusters(), + listOf(), + createListeners(), + listOf(), + listOf(), + UUID.randomUUID().toString() + ) + ) + } + + fun stopWatch() { + watch.cancel() + } + + private suspend fun createListeners(): List { + return dropletRepository.getAll().map { + Listener.newBuilder().setName("${it.type}-${it.id}").setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setProtocol(SocketAddress.Protocol.TCP).setAddress("0.0.0.0") + .setPortValue(it.envoyPort) + ) + ).setDefaultFilterChain(listenerFilterChain("${it.type}-${it.id}")).build() + } + } + + private suspend fun createClusters(): List { + return dropletRepository.getAll().map { + Cluster.newBuilder().setName("${it.type}-${it.id}").setConnectTimeout(Duration.newBuilder().setSeconds(5)) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig(ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) + ) + .setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN) + .setLoadAssignment( + ClusterLoadAssignment.newBuilder().setClusterName("${it.type}-${it.id}") + .addEndpoints( + LocalityLbEndpoints.newBuilder() + .addLbEndpoints( + LbEndpoint.newBuilder().setEndpoint( + Endpoint.newBuilder() + .setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setPortValue(it.port).setAddress(it.host) + ) + ) + ) + ) + ) + ).putTypedExtensionProtocolOptions( + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions", Any.pack( + HttpProtocolOptions.newBuilder().setExplicitHttpConfig( + HttpProtocolOptions.ExplicitHttpConfig.newBuilder().setHttp2ProtocolOptions( + Http2ProtocolOptions.newBuilder().setMaxConcurrentStreams( + UInt32Value.of(100) + ) + ) + ).build() + ) + ) + .build() + } + } + + private fun listenerFilterChain(cluster: String): FilterChain.Builder { + return FilterChain.newBuilder() + .addFilters( + Filter.newBuilder().setName("envoy.filters.network.http_connection_manager") + .setTypedConfig( + Any.pack( + HttpConnectionManager.newBuilder().setStatPrefix("ingress_http") + .setCodecType(HttpConnectionManager.CodecType.AUTO) + .setRouteConfig( + RouteConfiguration.newBuilder().setName("local_route") + .addVirtualHosts( + VirtualHost.newBuilder().setName("local_service").addDomains("*") + .addRoutes( + Route.newBuilder().setRoute( + RouteAction.newBuilder().setCluster(cluster) + .setTimeout(Duration.newBuilder().setSeconds(0).setNanos(0)) + ).setMatch(RouteMatch.newBuilder().setPrefix("/")) + ) + ) + ).addHttpFilters( + HttpFilter.newBuilder().setName("envoy.filters.http.connect_grpc_bridge") + .setTypedConfig(Any.pack(FilterConfig.getDefaultInstance())) + ).addHttpFilters( + HttpFilter.newBuilder().setName("envoy.filters.http.grpc_web") + .setTypedConfig(Any.pack(GrpcWeb.getDefaultInstance())) + ).addHttpFilters( + HttpFilter.newBuilder().setName("envoy.filters.http.router") + .setTypedConfig(Any.pack(Router.getDefaultInstance())) + ) + + .build() + ) + ) + ) + } + + fun getCache(): ConfigWatcher { + return cache + } + +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudNodeGroup.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudNodeGroup.kt new file mode 100644 index 0000000..b2d02fc --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudNodeGroup.kt @@ -0,0 +1,22 @@ +package app.simplecloud.controller.runtime.envoy + +import io.envoyproxy.controlplane.cache.NodeGroup +import io.envoyproxy.envoy.config.core.v3.Node + +/** + * SimpleCloud only uses one envoy node. That's why we can just + * have one node in the nodegroup. + */ +class SimpleCloudNodeGroup : NodeGroup { + + companion object { + const val GROUP = "simplecloud" + } + + override fun hash(node: Node?): String { + if (node == null) { + throw IllegalArgumentException("Null node") + } + return GROUP + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt new file mode 100644 index 0000000..aea5221 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt @@ -0,0 +1,14 @@ +package app.simplecloud.controller.runtime.envoy + +import io.envoyproxy.controlplane.cache.Response +import java.util.* +import java.util.function.Consumer + + +class SimpleCloudResponseTracker : Consumer { + private val responses: LinkedList = LinkedList() + + override fun accept(response: Response) { + responses.add(response) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ce5e2d..a0b906c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.27d7043" +droplet-api = "0.0.1-dev.7e049d6" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" @@ -13,6 +13,7 @@ clikt = "5.0.1" sonatype-central-portal-publisher = "1.2.3" spotify-completablefutures = "0.3.6" spring-crypto = "6.3.4" +envoy = "1.0.46" [libraries] kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -42,8 +43,8 @@ clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } spotify-completablefutures = { module = "com.spotify:completable-futures", version.ref = "spotify-completablefutures" } -spring-crypto = { module = "org.springframework.security:spring-security-crypto", version.ref = "spring-crypto"} - +spring-crypto = { module = "org.springframework.security:spring-security-crypto", version.ref = "spring-crypto" } +envoy-controlplane = { module = "io.envoyproxy.controlplane:server", version.ref = "envoy" } [bundles] From a328b44519bbea0b0dba6a00813665714930b08a Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 12:46:31 +0100 Subject: [PATCH 39/65] feat: implement droplet repository, integrate controlplane server --- .../controller/runtime/ControllerRuntime.kt | 8 + .../droplet/ControllerDropletService.kt | 46 ++++- .../runtime/droplet/DropletRepository.kt | 27 ++- .../runtime/envoy/ControlPlaneServer.kt | 46 +++-- .../controller/runtime/envoy/DropletCache.kt | 160 ++++++++++-------- .../envoy/SimpleCloudResponseTracker.kt | 14 -- .../launcher/ControllerStartCommand.kt | 5 + 7 files changed, 205 insertions(+), 101 deletions(-) delete mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index eedc100..ae4e5c8 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -3,6 +3,7 @@ package app.simplecloud.controller.runtime import app.simplecloud.controller.runtime.database.DatabaseFactory import app.simplecloud.controller.runtime.droplet.ControllerDropletService import app.simplecloud.controller.runtime.droplet.DropletRepository +import app.simplecloud.controller.runtime.envoy.ControlPlaneServer import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.group.GroupService import app.simplecloud.controller.runtime.host.ServerHostRepository @@ -37,6 +38,7 @@ class ControllerRuntime( private val serverRepository = ServerRepository(database, numericalIdRepository) private val hostRepository = ServerHostRepository() private val pubSubService = PubSubService() + private val controlPlaneServer = ControlPlaneServer(controllerStartCommand, dropletRepository) private val authServer = OAuthServer(controllerStartCommand, database) private val reconciler = Reconciler( groupRepository, @@ -53,6 +55,7 @@ class ControllerRuntime( logger.info("Starting controller") setupDatabase() startAuthServer() + startControlPlaneServer() startPubSubGrpcServer() startGrpcServer() startReconciler() @@ -83,6 +86,11 @@ class ControllerRuntime( } + private fun startControlPlaneServer() { + logger.info("Starting envoy control plane...") + controlPlaneServer.start() + } + private fun setupDatabase() { logger.info("Setting up database...") database.setup() diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt index 3fe4f75..ebca099 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt @@ -1,7 +1,51 @@ package app.simplecloud.controller.runtime.droplet -import build.buf.gen.simplecloud.controller.v1.ControllerDropletServiceGrpcKt +import app.simplecloud.droplet.api.droplet.Droplet +import build.buf.gen.simplecloud.controller.v1.* +import io.grpc.Status +import io.grpc.StatusException class ControllerDropletService(private val dropletRepository: DropletRepository) : ControllerDropletServiceGrpcKt.ControllerDropletServiceCoroutineImplBase() { + override suspend fun getDroplet(request: GetDropletRequest): GetDropletResponse { + val droplet = dropletRepository.find(request.type, request.id) + ?: throw StatusException(Status.NOT_FOUND.withDescription("This Droplet does not exist")) + return getDropletResponse { this.definition = droplet.toDefinition() } + } + + override suspend fun getAllDroplets(request: GetAllDropletsRequest): GetAllDropletsResponse { + val allDroplets = dropletRepository.getAll() + return getAllDropletsResponse { + definition.addAll(allDroplets.map { it.toDefinition() }) + } + } + + override suspend fun getDropletsByType(request: GetDropletsByTypeRequest): GetDropletsByTypeResponse { + val type = request.type + val typedDroplets = dropletRepository.getAll().filter { it.type == type } + return getDropletsByTypeResponse { + definition.addAll(typedDroplets.map { it.toDefinition() }) + } + } + + override suspend fun registerDroplet(request: RegisterDropletRequest): RegisterDropletResponse { + dropletRepository.find(request.definition.type, request.definition.id) + ?: throw StatusException(Status.NOT_FOUND.withDescription("This Droplet does not exist")) + val droplet = Droplet.fromDefinition(request.definition) + + try { + dropletRepository.save(droplet) + } catch (e: Exception) { + throw StatusException(Status.INTERNAL.withDescription("Error whilst updating Droplet").withCause(e)) + } + return registerDropletResponse { this.definition = droplet.toDefinition() } + } + + override suspend fun unregisterDroplet(request: UnregisterDropletRequest): UnregisterDropletResponse { + val droplet = dropletRepository.find(request.id) + ?: throw StatusException(Status.NOT_FOUND.withDescription("This Droplet does not exist")) + val deleted = dropletRepository.delete(droplet) + if (!deleted) throw StatusException(Status.NOT_FOUND.withDescription("Could not delete this Droplet")) + return unregisterDropletResponse { } + } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt index a24093f..351c45d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt @@ -1,11 +1,16 @@ package app.simplecloud.controller.runtime.droplet import app.simplecloud.controller.runtime.Repository +import app.simplecloud.controller.runtime.envoy.DropletCache import app.simplecloud.droplet.api.droplet.Droplet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class DropletRepository : Repository { private val currentDroplets = mutableListOf() + private val dropletCache = DropletCache(this) override suspend fun getAll(): List { return currentDroplets @@ -20,15 +25,31 @@ class DropletRepository : Repository { } override fun save(element: Droplet) { + val updated = managePortRange(element) val droplet = find(element.type, element.id) if (droplet != null) { - currentDroplets[currentDroplets.indexOf(droplet)] = element + currentDroplets[currentDroplets.indexOf(droplet)] = updated return } - currentDroplets.add(element) + currentDroplets.add(updated) + CoroutineScope(Dispatchers.IO).launch { + dropletCache.update() + } + } + + private fun managePortRange(element: Droplet): Droplet { + if (!currentDroplets.any { it.envoyPort == element.envoyPort }) return element + return managePortRange(element.copy(envoyPort = element.envoyPort + 1)) } override suspend fun delete(element: Droplet): Boolean { - return currentDroplets.remove(element) + val found = find(element.type, element.id) ?: return false + if (!currentDroplets.remove(found)) return false + dropletCache.update() + return true + } + + fun getAsDropletCache(): DropletCache { + return dropletCache } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt index 8f94b1c..b0b8f85 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt @@ -1,23 +1,49 @@ package app.simplecloud.controller.runtime.envoy import app.simplecloud.controller.runtime.droplet.DropletRepository -import io.envoyproxy.controlplane.cache.ConfigWatcher +import app.simplecloud.controller.runtime.launcher.ControllerStartCommand +import app.simplecloud.droplet.api.droplet.Droplet import io.envoyproxy.controlplane.server.V3DiscoveryServer import io.grpc.ServerBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager -class ControlPlaneServer(dropletRepository: DropletRepository) { - private val cache = DropletCache(dropletRepository) - private val server = V3DiscoveryServer(cache.getCache()) +class ControlPlaneServer(private val args: ControllerStartCommand, private val dropletRepository: DropletRepository) { + private val server = V3DiscoveryServer(dropletRepository.getAsDropletCache().getCache()) private val logger = LogManager.getLogger(ControlPlaneServer::class) - fun register(builder: ServerBuilder<*>) { - logger.info("Registering envoy control plane server...") - builder.addService(server.aggregatedDiscoveryServiceImpl) - logger.info("Registered envoy control plane server.") + fun start() { + val serverBuilder = ServerBuilder.forPort(args.envoyDiscoveryPort) + register(serverBuilder) + val server = serverBuilder.build() + CoroutineScope(Dispatchers.IO).launch { + try { + server.start() + server.awaitTermination() + } catch (e: Exception) { + logger.warn("Error in envoy control server server", e) + throw e + } + } + registerSelf() + } + + private fun registerSelf() { + dropletRepository.save( + Droplet( + type = "controller", + id = "internal-controller", + host = args.grpcHost, + port = args.grpcPort, + envoyPort = 8080, + ) + ) } - fun getCache(): ConfigWatcher { - return cache.getCache() + private fun register(builder: ServerBuilder<*>) { + logger.info("Registering envoy ADS...") + builder.addService(server.aggregatedDiscoveryServiceImpl) } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt index f8dd799..5af9cc3 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -1,13 +1,11 @@ package app.simplecloud.controller.runtime.envoy import app.simplecloud.controller.runtime.droplet.DropletRepository +import app.simplecloud.droplet.api.droplet.Droplet import com.google.protobuf.Any import com.google.protobuf.Duration import com.google.protobuf.UInt32Value import io.envoyproxy.controlplane.cache.ConfigWatcher -import io.envoyproxy.controlplane.cache.Resources -import io.envoyproxy.controlplane.cache.Watch -import io.envoyproxy.controlplane.cache.XdsRequest import io.envoyproxy.controlplane.cache.v3.SimpleCache import io.envoyproxy.controlplane.cache.v3.Snapshot import io.envoyproxy.envoy.config.cluster.v3.Cluster @@ -27,101 +25,117 @@ import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter import io.envoyproxy.envoy.extensions.upstreams.http.v3.HttpProtocolOptions -import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager import java.util.* +/** + * This class handles the remapping of the [DropletRepository] to a [SimpleCache] of [Snapshot]s, which are used by the envoy ADS service. + */ class DropletCache(private val dropletRepository: DropletRepository) { private val cache = SimpleCache(SimpleCloudNodeGroup()) - private var watch: Watch = cache.createWatch( - true, - XdsRequest.create( - DiscoveryRequest.newBuilder().setNode(Node.getDefaultInstance()) - .setTypeUrl(Resources.V3.ENDPOINT_TYPE_URL).addResourceNames("none").build() - ), - Collections.emptySet(), - SimpleCloudResponseTracker() - ) + private val logger = LogManager.getLogger(DropletRepository::class.java) - init { - CoroutineScope(Dispatchers.IO).launch { - update() + //Create a new Snapshot by the droplet repository's data + suspend fun update() { + logger.info("Detected new droplets in DropletRepository, adding to ADS...") + val clusters = mutableListOf() + val listeners = mutableListOf() + // This should not be needed as the load assignment is present in the cluster itself + // val clas = mutableListOf() + + dropletRepository.getAll().forEach { + clusters.add(createCluster(it)) + listeners.add(createListener(it)) + //This should also not be needed + //clas.add(createCLA(it)) } - } - suspend fun update() { - //Gets the simplecloud group, if not found throws an error - val group = cache.groups().firstOrNull() ?: throw IllegalArgumentException("Group not found") cache.setSnapshot( - group, + SimpleCloudNodeGroup.GROUP, Snapshot.create( - createClusters(), - listOf(), - createListeners(), - listOf(), - listOf(), - UUID.randomUUID().toString() + clusters, + listOf(), // clas, + listeners, + listOf(), //I think we don't have to configure routes + listOf(), //TODO: We don't yet need secrets, but definitely in the future + UUID.randomUUID() + .toString() //This can be anything, used internally for versioning. THIS HAS TO BE DIFFERENT FOR EVERY SNAPSHOT ) ) } - fun stopWatch() { - watch.cancel() + //Creates endpoints users can connect with later + private fun createListener(it: Droplet): Listener { + return Listener.newBuilder().setName("${it.type}-${it.id}").setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setProtocol(SocketAddress.Protocol.TCP).setAddress("0.0.0.0") + .setPortValue(it.envoyPort) + ) + ).setDefaultFilterChain(createListenerFilterChain("${it.type}-${it.id}")).build() + } - private suspend fun createListeners(): List { - return dropletRepository.getAll().map { - Listener.newBuilder().setName("${it.type}-${it.id}").setAddress( - Address.newBuilder().setSocketAddress( - SocketAddress.newBuilder().setProtocol(SocketAddress.Protocol.TCP).setAddress("0.0.0.0") - .setPortValue(it.envoyPort) + //Creates load assignments for new droplets (I don't yet know if they need to be called every time?) + private fun createCLA(it: Droplet): ClusterLoadAssignment { + return ClusterLoadAssignment.newBuilder().setClusterName("${it.type}-${it.id}") + .addEndpoints( + LocalityLbEndpoints.newBuilder().addLbEndpoints( + LbEndpoint.newBuilder().setEndpoint( + Endpoint.newBuilder() + .setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setPortValue(it.port).setAddress(it.host) + .setProtocol(SocketAddress.Protocol.TCP) + ) + ) + ) ) - ).setDefaultFilterChain(listenerFilterChain("${it.type}-${it.id}")).build() - } + ) + .build() } - private suspend fun createClusters(): List { - return dropletRepository.getAll().map { - Cluster.newBuilder().setName("${it.type}-${it.id}").setConnectTimeout(Duration.newBuilder().setSeconds(5)) - .setType(Cluster.DiscoveryType.EDS) - .setEdsClusterConfig( - EdsClusterConfig.newBuilder() - .setEdsConfig(ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - ) - .setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN) - .setLoadAssignment( - ClusterLoadAssignment.newBuilder().setClusterName("${it.type}-${it.id}") - .addEndpoints( - LocalityLbEndpoints.newBuilder() - .addLbEndpoints( - LbEndpoint.newBuilder().setEndpoint( - Endpoint.newBuilder() - .setAddress( - Address.newBuilder().setSocketAddress( - SocketAddress.newBuilder().setPortValue(it.port).setAddress(it.host) - ) + //Creates clusters listening to droplets + private fun createCluster(it: Droplet): Cluster { + return Cluster.newBuilder().setName("${it.type}-${it.id}") + .setConnectTimeout(Duration.newBuilder().setSeconds(5)) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig(ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) + ) + .setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN) + .setLoadAssignment( + ClusterLoadAssignment.newBuilder().setClusterName("${it.type}-${it.id}") + .addEndpoints( + LocalityLbEndpoints.newBuilder() + .addLbEndpoints( + LbEndpoint.newBuilder().setEndpoint( + Endpoint.newBuilder() + .setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setPortValue(it.port).setAddress(it.host) ) - ) - ) - ) - ).putTypedExtensionProtocolOptions( - "envoy.extensions.upstreams.http.v3.HttpProtocolOptions", Any.pack( - HttpProtocolOptions.newBuilder().setExplicitHttpConfig( - HttpProtocolOptions.ExplicitHttpConfig.newBuilder().setHttp2ProtocolOptions( - Http2ProtocolOptions.newBuilder().setMaxConcurrentStreams( - UInt32Value.of(100) + ) ) ) - ).build() ) + ).putTypedExtensionProtocolOptions( + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions", Any.pack( + HttpProtocolOptions.newBuilder().setExplicitHttpConfig( + HttpProtocolOptions.ExplicitHttpConfig.newBuilder().setHttp2ProtocolOptions( + Http2ProtocolOptions.newBuilder().setMaxConcurrentStreams( + UInt32Value.of(100) + ) + ) + ).build() ) - .build() - } + ) + .build() + } - private fun listenerFilterChain(cluster: String): FilterChain.Builder { + //Creates a filter chain that remaps http to grpc + private fun createListenerFilterChain(cluster: String): FilterChain.Builder { return FilterChain.newBuilder() .addFilters( Filter.newBuilder().setName("envoy.filters.network.http_connection_manager") diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt deleted file mode 100644 index aea5221..0000000 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/SimpleCloudResponseTracker.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.simplecloud.controller.runtime.envoy - -import io.envoyproxy.controlplane.cache.Response -import java.util.* -import java.util.function.Consumer - - -class SimpleCloudResponseTracker : Consumer { - private val responses: LinkedList = LinkedList() - - override fun accept(response: Response) { - responses.add(response) - } -} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index dccc336..b972c8d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -45,6 +45,11 @@ class ControllerStartCommand( envvar = "AUTHORIZATION_PORT" ).int().default(5818) + val envoyDiscoveryPort: Int by option( + help = "Authorization port (default: 5814)", + envvar = "ENVOY_DISCOVERY_PORT" + ).int().default(5814) + private val authSecretPath: Path by option( help = "Path to auth secret file (default: .auth.secret)", envvar = "AUTH_SECRET_PATH" From 6395bbfcefcf6b78436b88e2a8f070376eb8cce0 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 13:08:24 +0100 Subject: [PATCH 40/65] feat: envoy bootstrap, doc reference --- .../runtime/envoy/ControlPlaneServer.kt | 5 ++- .../controller/runtime/envoy/DropletCache.kt | 2 +- envoy-bootstrap.yaml | 43 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 envoy-bootstrap.yaml diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt index b0b8f85..94a9d07 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt @@ -10,9 +10,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager +/** + * @see ADS Documentation + */ class ControlPlaneServer(private val args: ControllerStartCommand, private val dropletRepository: DropletRepository) { private val server = V3DiscoveryServer(dropletRepository.getAsDropletCache().getCache()) - private val logger = LogManager.getLogger(ControlPlaneServer::class) + private val logger = LogManager.getLogger(ControlPlaneServer::class.java) fun start() { val serverBuilder = ServerBuilder.forPort(args.envoyDiscoveryPort) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt index 5af9cc3..3564e58 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -33,7 +33,7 @@ import java.util.* */ class DropletCache(private val dropletRepository: DropletRepository) { private val cache = SimpleCache(SimpleCloudNodeGroup()) - private val logger = LogManager.getLogger(DropletRepository::class.java) + private val logger = LogManager.getLogger(DropletCache::class.java) //Create a new Snapshot by the droplet repository's data suspend fun update() { diff --git a/envoy-bootstrap.yaml b/envoy-bootstrap.yaml new file mode 100644 index 0000000..39f56db --- /dev/null +++ b/envoy-bootstrap.yaml @@ -0,0 +1,43 @@ +node: + cluster: simplecloud + id: simplecloud + +dynamic_resources: + ads_config: + api_type: GRPC + grpc_services: + - envoy_grpc: + cluster_name: ads_cluster + cds_config: + ads: {} + lds_config: + ads: {} + +static_resources: + clusters: + - name: ads_cluster + type: STRICT_DNS + load_assignment: + cluster_name: ads_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 5814 + # It is recommended to configure either HTTP/2 or TCP keepalives in order to detect + # connection issues, and allow Envoy to reconnect. TCP keepalive is less expensive, but + # may be inadequate if there is a TCP proxy between Envoy and the management server. + # HTTP/2 keepalive is slightly more expensive, but may detect issues through more types + # of intermediate proxies. + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: + connection_keepalive: + interval: 30s + timeout: 5s + upstream_connection_options: + tcp_keepalive: {} \ No newline at end of file From aa77e2d49df042c0959bd29d1762829b8d9600c0 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 13:47:33 +0100 Subject: [PATCH 41/65] fix: add cla, force v3 transport version --- .../controller/runtime/envoy/DropletCache.kt | 8 +++----- test-envoy/docker-compose.yml | 10 ++++++++++ .../envoy-bootstrap.yaml | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 test-envoy/docker-compose.yml rename envoy-bootstrap.yaml => test-envoy/envoy-bootstrap.yaml (95%) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt index 3564e58..35cb3cc 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -40,21 +40,19 @@ class DropletCache(private val dropletRepository: DropletRepository) { logger.info("Detected new droplets in DropletRepository, adding to ADS...") val clusters = mutableListOf() val listeners = mutableListOf() - // This should not be needed as the load assignment is present in the cluster itself - // val clas = mutableListOf() + val clas = mutableListOf() dropletRepository.getAll().forEach { clusters.add(createCluster(it)) listeners.add(createListener(it)) - //This should also not be needed - //clas.add(createCLA(it)) + clas.add(createCLA(it)) } cache.setSnapshot( SimpleCloudNodeGroup.GROUP, Snapshot.create( clusters, - listOf(), // clas, + clas, listeners, listOf(), //I think we don't have to configure routes listOf(), //TODO: We don't yet need secrets, but definitely in the future diff --git a/test-envoy/docker-compose.yml b/test-envoy/docker-compose.yml new file mode 100644 index 0000000..63007ce --- /dev/null +++ b/test-envoy/docker-compose.yml @@ -0,0 +1,10 @@ +services: + envoy: + network_mode: "host" + image: envoyproxy/envoy:v1.28.0 + ports: + - "8080:8080" + volumes: + - ./envoy-bootstrap.yaml:/etc/envoy/envoy.yaml + environment: + - loglevel=debug \ No newline at end of file diff --git a/envoy-bootstrap.yaml b/test-envoy/envoy-bootstrap.yaml similarity index 95% rename from envoy-bootstrap.yaml rename to test-envoy/envoy-bootstrap.yaml index 39f56db..3bd962a 100644 --- a/envoy-bootstrap.yaml +++ b/test-envoy/envoy-bootstrap.yaml @@ -5,6 +5,7 @@ node: dynamic_resources: ads_config: api_type: GRPC + transport_api_version: V3 grpc_services: - envoy_grpc: cluster_name: ads_cluster @@ -16,6 +17,7 @@ dynamic_resources: static_resources: clusters: - name: ads_cluster + lb_policy: ROUND_ROBIN type: STRICT_DNS load_assignment: cluster_name: ads_cluster From 6fdcf98fbfa166161bc0a074c39b8f1f97d01003 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 13:58:05 +0100 Subject: [PATCH 42/65] refactor: update comments --- .../app/simplecloud/controller/runtime/envoy/DropletCache.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt index 35cb3cc..b95e9e8 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -54,8 +54,8 @@ class DropletCache(private val dropletRepository: DropletRepository) { clusters, clas, listeners, - listOf(), //I think we don't have to configure routes - listOf(), //TODO: We don't yet need secrets, but definitely in the future + listOf(), // We don't need routes + listOf(), // We don't need secrets UUID.randomUUID() .toString() //This can be anything, used internally for versioning. THIS HAS TO BE DIFFERENT FOR EVERY SNAPSHOT ) From 46944a0c68d320779c7477aa7aef1723442724fa Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 18:43:02 +0100 Subject: [PATCH 43/65] refactor: minimize envoy image --- test-envoy/docker-compose.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test-envoy/docker-compose.yml b/test-envoy/docker-compose.yml index 63007ce..585ac11 100644 --- a/test-envoy/docker-compose.yml +++ b/test-envoy/docker-compose.yml @@ -1,10 +1,6 @@ services: envoy: network_mode: "host" - image: envoyproxy/envoy:v1.28.0 - ports: - - "8080:8080" + image: envoyproxy/envoy:v1.31.4 volumes: - - ./envoy-bootstrap.yaml:/etc/envoy/envoy.yaml - environment: - - loglevel=debug \ No newline at end of file + - ./envoy-bootstrap.yaml:/etc/envoy/envoy.yaml \ No newline at end of file From e6c9f03fd3596cec5412811e2f3b214bf6d6a01a Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 19:56:26 +0100 Subject: [PATCH 44/65] refactor: minimize envoy image --- .../app/simplecloud/controller/runtime/server/ServerService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 3cae638..405811f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -27,6 +27,7 @@ class ServerService( private val logger = LogManager.getLogger(ServerService::class.java) + @Deprecated("This method will be removed soon. Please use DropletService#registerDroplet") override suspend fun attachServerHost(request: AttachServerHostRequest): ServerHostDefinition { val serverHost = ServerHost.fromDefinition(request.serverHost, authCallCredentials) try { From 8f05a9de2f8f4a4646743c3bcd6f63a99a2437c5 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 20:09:26 +0100 Subject: [PATCH 45/65] fix: droplet registration --- .../controller/runtime/droplet/ControllerDropletService.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt index ebca099..4aead6f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt @@ -29,14 +29,12 @@ class ControllerDropletService(private val dropletRepository: DropletRepository) } override suspend fun registerDroplet(request: RegisterDropletRequest): RegisterDropletResponse { - dropletRepository.find(request.definition.type, request.definition.id) - ?: throw StatusException(Status.NOT_FOUND.withDescription("This Droplet does not exist")) val droplet = Droplet.fromDefinition(request.definition) - try { + dropletRepository.delete(droplet) dropletRepository.save(droplet) } catch (e: Exception) { - throw StatusException(Status.INTERNAL.withDescription("Error whilst updating Droplet").withCause(e)) + throw StatusException(Status.INTERNAL.withDescription("Error whilst registering Droplet").withCause(e)) } return registerDropletResponse { this.definition = droplet.toDefinition() } } From 16f91aa33c2aff9e768171b840ee83adda50d23b Mon Sep 17 00:00:00 2001 From: dayeeet Date: Mon, 16 Dec 2024 23:42:41 +0100 Subject: [PATCH 46/65] fix: include reattaching into serverhost registration --- .../controller/runtime/ControllerRuntime.kt | 7 +++- .../runtime/droplet/DropletRepository.kt | 31 ++++++++++++++- .../runtime/server/ServerHostAttacher.kt | 38 +++++++++++++++++++ .../runtime/server/ServerService.kt | 27 ++++--------- 4 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerHostAttacher.kt diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index ae4e5c8..29e167c 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -10,6 +10,7 @@ import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.runtime.launcher.ControllerStartCommand import app.simplecloud.controller.runtime.oauth.OAuthServer import app.simplecloud.controller.runtime.reconciler.Reconciler +import app.simplecloud.controller.runtime.server.ServerHostAttacher import app.simplecloud.controller.runtime.server.ServerNumericalIdRepository import app.simplecloud.controller.runtime.server.ServerRepository import app.simplecloud.controller.runtime.server.ServerService @@ -32,11 +33,12 @@ class ControllerRuntime( private val database = DatabaseFactory.createDatabase(controllerStartCommand.databaseUrl) private val authCallCredentials = AuthCallCredentials(controllerStartCommand.authSecret) - private val dropletRepository = DropletRepository() private val groupRepository = GroupRepository(controllerStartCommand.groupPath) private val numericalIdRepository = ServerNumericalIdRepository() private val serverRepository = ServerRepository(database, numericalIdRepository) private val hostRepository = ServerHostRepository() + private val serverHostAttacher = ServerHostAttacher(hostRepository, serverRepository) + private val dropletRepository = DropletRepository(authCallCredentials, serverHostAttacher, hostRepository) private val pubSubService = PubSubService() private val controlPlaneServer = ControlPlaneServer(controllerStartCommand, dropletRepository) private val authServer = OAuthServer(controllerStartCommand, database) @@ -162,7 +164,8 @@ class ControllerRuntime( controllerStartCommand.grpcHost, controllerStartCommand.pubSubGrpcPort, authCallCredentials - ) + ), + serverHostAttacher ) ) .addService(ControllerDropletService(dropletRepository)) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt index 351c45d..9e7c06d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt @@ -2,12 +2,21 @@ package app.simplecloud.controller.runtime.droplet import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.envoy.DropletCache +import app.simplecloud.controller.runtime.host.ServerHostRepository +import app.simplecloud.controller.runtime.server.ServerHostAttacher +import app.simplecloud.controller.shared.host.ServerHost +import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.droplet.api.droplet.Droplet +import build.buf.gen.simplecloud.controller.v1.ServerHostServiceGrpcKt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class DropletRepository : Repository { +class DropletRepository( + private val authCallCredentials: AuthCallCredentials, + private val serverHostAttacher: ServerHostAttacher, + private val serverHostRepository: ServerHostRepository, +) : Repository { private val currentDroplets = mutableListOf() private val dropletCache = DropletCache(this) @@ -29,11 +38,27 @@ class DropletRepository : Repository { val droplet = find(element.type, element.id) if (droplet != null) { currentDroplets[currentDroplets.indexOf(droplet)] = updated + postUpdate(updated) return } currentDroplets.add(updated) + postUpdate(updated) + } + + private fun postUpdate(droplet: Droplet) { CoroutineScope(Dispatchers.IO).launch { dropletCache.update() + if (droplet.type != "serverhost") return@launch + serverHostAttacher.attach( + ServerHost( + droplet.id, droplet.host, droplet.port, ServerHostServiceGrpcKt.ServerHostServiceCoroutineStub( + ServerHost.createChannel( + droplet.host, + droplet.port + ) + ).withCallCredentials(authCallCredentials) + ) + ) } } @@ -46,6 +71,10 @@ class DropletRepository : Repository { val found = find(element.type, element.id) ?: return false if (!currentDroplets.remove(found)) return false dropletCache.update() + if (element.type == "serverhost") { + val host = serverHostRepository.findServerHostById(element.id) ?: return true + serverHostRepository.delete(host) + } return true } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerHostAttacher.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerHostAttacher.kt new file mode 100644 index 0000000..2c470e8 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerHostAttacher.kt @@ -0,0 +1,38 @@ +package app.simplecloud.controller.runtime.server + +import app.simplecloud.controller.runtime.host.ServerHostRepository +import app.simplecloud.controller.shared.host.ServerHost +import app.simplecloud.controller.shared.server.Server +import io.grpc.Status +import io.grpc.StatusException +import kotlinx.coroutines.coroutineScope +import org.apache.logging.log4j.LogManager + +class ServerHostAttacher( + private val hostRepository: ServerHostRepository, + private val serverRepository: ServerRepository +) { + + private val logger = LogManager.getLogger(ServerHostAttacher::class.java) + + suspend fun attach(serverHost: ServerHost) { + hostRepository.delete(serverHost) + hostRepository.save(serverHost) + logger.info("Successfully registered ServerHost ${serverHost.id}.") + + coroutineScope { + serverRepository.findServersByHostId(serverHost.id).forEach { server -> + logger.info("Reattaching Server ${server.uniqueId} of group ${server.group}...") + try { + val result = serverHost.stub?.reattachServer(server.toDefinition()) + ?: throw StatusException(Status.INTERNAL.withDescription("Could not reattach server, is the host misconfigured?")) + serverRepository.save(Server.fromDefinition(result)) + logger.info("Success!") + } catch (e: Exception) { + logger.error("Server was found to be offline, unregistering...") + serverRepository.delete(server) + } + } + } + } +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 405811f..bda363f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -11,7 +11,6 @@ import app.simplecloud.pubsub.PubSubClient import build.buf.gen.simplecloud.controller.v1.* import io.grpc.Status import io.grpc.StatusException -import kotlinx.coroutines.coroutineScope import org.apache.logging.log4j.LogManager import java.time.LocalDateTime import java.util.* @@ -23,6 +22,7 @@ class ServerService( private val groupRepository: GroupRepository, private val authCallCredentials: AuthCallCredentials, private val pubSubClient: PubSubClient, + private val serverHostAttacher: ServerHostAttacher, ) : ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineImplBase() { private val logger = LogManager.getLogger(ServerService::class.java) @@ -31,25 +31,9 @@ class ServerService( override suspend fun attachServerHost(request: AttachServerHostRequest): ServerHostDefinition { val serverHost = ServerHost.fromDefinition(request.serverHost, authCallCredentials) try { - hostRepository.delete(serverHost) - hostRepository.save(serverHost) + serverHostAttacher.attach(serverHost) } catch (e: Exception) { - throw StatusException(Status.INTERNAL.withDescription("Could not save serverhost").withCause(e)) - } - logger.info("Successfully registered ServerHost ${serverHost.id}.") - - coroutineScope { - serverRepository.findServersByHostId(serverHost.id).forEach { server -> - logger.info("Reattaching Server ${server.uniqueId} of group ${server.group}...") - try { - val result = serverHost.stub?.reattachServer(server.toDefinition()) ?: throw StatusException(Status.INTERNAL.withDescription("Could not reattach server, is the host misconfigured?")) - serverRepository.save(Server.fromDefinition(result)) - logger.info("Success!") - } catch (e: Exception) { - logger.error("Server was found to be offline, unregistering...") - serverRepository.delete(server) - } - } + throw StatusException(Status.INTERNAL.withDescription("Could not attach serverhost").withCause(e)) } return serverHost.toDefinition() } @@ -214,7 +198,10 @@ class ServerService( } } - private suspend fun stopServer(server: ServerDefinition, cause: ServerStopCause = ServerStopCause.NATURAL_STOP): ServerDefinition { + private suspend fun stopServer( + server: ServerDefinition, + cause: ServerStopCause = ServerStopCause.NATURAL_STOP + ): ServerDefinition { val host = hostRepository.findServerHostById(server.hostId) ?: throw Status.NOT_FOUND .withDescription("No server host was found matching this server.") From 905a98eb6c8c18c54dd0426be252b88474b1e5d9 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 17 Dec 2024 22:48:26 +0100 Subject: [PATCH 47/65] feat: kotlin dsl --- .../api/dsl/builders/GroupBuilders.kt | 68 +++++++++ .../api/dsl/builders/PropertyBuilder.kt | 28 ++++ .../api/dsl/builders/ServerBuilders.kt | 51 +++++++ .../dsl/extensions/ControllerExtensions.kt | 32 ++++ .../api/dsl/extensions/GroupApiExtensions.kt | 96 ++++++++++++ .../api/dsl/extensions/GroupExtensions.kt | 18 +++ .../api/dsl/extensions/ServerApiExtensions.kt | 140 ++++++++++++++++++ .../api/dsl/extensions/ServerExtensions.kt | 54 +++++++ .../controller/api/dsl/markers/DslMarkers.kt | 13 ++ .../controller/api/dsl/scopes/GroupScope.kt | 97 ++++++++++++ .../controller/api/dsl/scopes/ServerScope.kt | 26 ++++ 11 files changed, 623 insertions(+) create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/GroupBuilders.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/PropertyBuilder.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/ServerBuilders.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ControllerExtensions.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupApiExtensions.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupExtensions.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerApiExtensions.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerExtensions.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/markers/DslMarkers.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/GroupScope.kt create mode 100644 controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/ServerScope.kt diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/GroupBuilders.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/GroupBuilders.kt new file mode 100644 index 0000000..97758a6 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/GroupBuilders.kt @@ -0,0 +1,68 @@ +package app.simplecloud.controller.api.dsl.builders + +import app.simplecloud.controller.api.dsl.markers.GroupDsl +import app.simplecloud.controller.shared.group.Group +import build.buf.gen.simplecloud.controller.v1.ServerType + +@GroupDsl +class GroupCreateBuilder { + var name: String = "" + var type: ServerType = ServerType.UNKNOWN_SERVER + var minMemory: Long = 0 + var maxMemory: Long = 0 + var startPort: Long = 0 + var minOnlineCount: Long = 0 + var maxOnlineCount: Long = 0 + var maxPlayers: Long = 0 + var newServerPlayerRatio: Long = -1 + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + internal fun build() = Group( + name = name, + type = type, + minMemory = minMemory, + maxMemory = maxMemory, + startPort = startPort, + minOnlineCount = minOnlineCount, + maxOnlineCount = maxOnlineCount, + maxPlayers = maxPlayers, + newServerPlayerRatio = newServerPlayerRatio, + properties = properties + ) +} + +@GroupDsl +class GroupUpdateBuilder { + private var group: Group? = null + private val properties = mutableMapOf() + + fun fromGroup(existingGroup: Group) { + group = existingGroup + properties.putAll(existingGroup.properties) + } + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + internal fun build(): Group { + requireNotNull(group) { "Group must be set using fromGroup()" } + return group!!.copy(properties = properties) + } +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/PropertyBuilder.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/PropertyBuilder.kt new file mode 100644 index 0000000..5bb56b0 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/PropertyBuilder.kt @@ -0,0 +1,28 @@ +package app.simplecloud.controller.api.dsl.builders + +import app.simplecloud.controller.api.dsl.markers.PropertyDsl + +@PropertyDsl +class PropertyBuilder { + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + fun property(key: String, value: String) { + properties[key] = value + } + + fun properties(vararg pairs: Pair) { + properties.putAll(pairs) + } + + internal fun build(): Map = properties.toMap() +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/ServerBuilders.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/ServerBuilders.kt new file mode 100644 index 0000000..031fd2c --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/builders/ServerBuilders.kt @@ -0,0 +1,51 @@ +package app.simplecloud.controller.api.dsl.builders + +import app.simplecloud.controller.api.dsl.markers.ServerDsl +import build.buf.gen.simplecloud.controller.v1.ServerStartCause +import build.buf.gen.simplecloud.controller.v1.ServerState +import build.buf.gen.simplecloud.controller.v1.ServerStopCause + +@ServerDsl +class ServerStartBuilder { + var startCause: ServerStartCause = ServerStartCause.API_START + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + fun getProperties(): Map = properties.toMap() +} + +@ServerDsl +class ServerStopBuilder { + var stopCause: ServerStopCause = ServerStopCause.API_STOP +} + +@ServerDsl +class ServerStateBuilder { + var state: ServerState = ServerState.UNKNOWN_STATE +} + +@ServerDsl +class ServerPropertyBuilder { + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + internal fun build(): Map = properties.toMap() +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ControllerExtensions.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ControllerExtensions.kt new file mode 100644 index 0000000..9b00a1c --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ControllerExtensions.kt @@ -0,0 +1,32 @@ +package app.simplecloud.controller.api.dsl.extensions + +import app.simplecloud.controller.api.ControllerApi +import app.simplecloud.controller.api.dsl.markers.ControllerDsl +import app.simplecloud.controller.api.dsl.scopes.GroupScope +import app.simplecloud.controller.api.dsl.scopes.ServerScope +import kotlinx.coroutines.coroutineScope + +@ControllerDsl +class ControllerApiScope(private val api: ControllerApi.Coroutine) { + suspend fun groups(block: suspend GroupScope.() -> Unit) { + GroupScope(api.getGroups()).block() + } + + suspend fun servers(block: suspend ServerScope.() -> Unit) { + ServerScope(api.getServers()).block() + } +} + +suspend fun controllerApiScope(block: suspend ControllerApiScope.() -> Unit) { + val api = ControllerApi.createCoroutineApi() + controllerApiScope(api, block) +} + +suspend fun controllerApiScope( + api: ControllerApi.Coroutine, + block: suspend ControllerApiScope.() -> Unit +) { + coroutineScope { + ControllerApiScope(api).block() + } +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupApiExtensions.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupApiExtensions.kt new file mode 100644 index 0000000..9dd6c27 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupApiExtensions.kt @@ -0,0 +1,96 @@ +package app.simplecloud.controller.api.dsl.extensions + +import app.simplecloud.controller.api.GroupApi +import app.simplecloud.controller.api.dsl.markers.GroupDsl +import app.simplecloud.controller.shared.group.Group +import build.buf.gen.simplecloud.controller.v1.ServerType + +@GroupDsl +class GroupCreateBuilder { + var name: String = "" + var type: ServerType = ServerType.UNKNOWN_SERVER + var minMemory: Long = 0 + var maxMemory: Long = 0 + var startPort: Long = 0 + var minOnlineCount: Long = 0 + var maxOnlineCount: Long = 0 + var maxPlayers: Long = 0 + var newServerPlayerRatio: Long = -1 + private val properties = mutableMapOf() + + fun properties(block: ServerPropertyBuilder.() -> Unit) { + val builder = ServerPropertyBuilder().apply(block) + properties.putAll(builder.build()) + } + + internal fun build() = Group( + name = name, + type = type, + minMemory = minMemory, + maxMemory = maxMemory, + startPort = startPort, + minOnlineCount = minOnlineCount, + maxOnlineCount = maxOnlineCount, + maxPlayers = maxPlayers, + newServerPlayerRatio = newServerPlayerRatio, + properties = properties + ) +} + +@GroupDsl +class GroupUpdateBuilder { + private var group: Group? = null + private val properties = mutableMapOf() + + fun fromGroup(existingGroup: Group) { + group = existingGroup + properties.putAll(existingGroup.properties) + } + + fun properties(block: ServerPropertyBuilder.() -> Unit) { + val builder = ServerPropertyBuilder().apply(block) + properties.putAll(builder.build()) + } + + internal fun build(): Group { + requireNotNull(group) { "Group must be set using fromGroup()" } + return group!!.copy(properties = properties) + } +} + +suspend fun GroupApi.Coroutine.create(block: GroupCreateBuilder.() -> Unit): Group { + val builder = GroupCreateBuilder().apply(block) + return createGroup(builder.build()) +} + +suspend fun GroupApi.Coroutine.update(block: GroupUpdateBuilder.() -> Unit): Group { + val builder = GroupUpdateBuilder().apply(block) + return updateGroup(builder.build()) +} + +suspend fun GroupApi.Coroutine.delete( + name: String, + block: (Group) -> Unit = {} +) { + block(deleteGroup(name)) +} + +suspend fun GroupApi.Coroutine.getGroup( + name: String, + block: (Group) -> Unit = {} +) { + block(getGroupByName(name)) +} + +suspend fun GroupApi.Coroutine.getAllGroups( + block: (List) -> Unit = {} +) { + block(getAllGroups()) +} + +suspend fun GroupApi.Coroutine.getGroupsByType( + type: ServerType, + block: (List) -> Unit = {} +) { + block(getGroupsByType(type)) +} \ No newline at end of file diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupExtensions.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupExtensions.kt new file mode 100644 index 0000000..a586636 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/GroupExtensions.kt @@ -0,0 +1,18 @@ +package app.simplecloud.controller.api.dsl.extensions + +import app.simplecloud.controller.api.GroupApi +import app.simplecloud.controller.shared.group.Group + +suspend fun GroupApi.Coroutine.createGroup( + block: GroupCreateBuilder.() -> Unit +): Group { + val builder = GroupCreateBuilder().apply(block) + return createGroup(builder.build()) +} + +suspend fun GroupApi.Coroutine.updateGroup( + block: GroupUpdateBuilder.() -> Unit +): Group { + val builder = GroupUpdateBuilder().apply(block) + return updateGroup(builder.build()) +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerApiExtensions.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerApiExtensions.kt new file mode 100644 index 0000000..4e186ef --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerApiExtensions.kt @@ -0,0 +1,140 @@ +package app.simplecloud.controller.api.dsl.extensions + +import app.simplecloud.controller.api.ServerApi +import app.simplecloud.controller.api.dsl.markers.ServerDsl +import app.simplecloud.controller.shared.group.Group +import app.simplecloud.controller.shared.server.Server +import build.buf.gen.simplecloud.controller.v1.ServerStartCause +import build.buf.gen.simplecloud.controller.v1.ServerState +import build.buf.gen.simplecloud.controller.v1.ServerStopCause +import build.buf.gen.simplecloud.controller.v1.ServerType + +@ServerDsl +class ServerStartBuilder { + var startCause: ServerStartCause = ServerStartCause.API_START + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + fun getProperties(): Map = properties +} + +@ServerDsl +class ServerStopBuilder { + var stopCause: ServerStopCause = ServerStopCause.API_STOP +} + +@ServerDsl +class ServerStateBuilder { + var state: ServerState = ServerState.UNKNOWN_STATE +} + +@ServerDsl +class ServerPropertyBuilder { + private val properties = mutableMapOf() + + operator fun String.unaryPlus() = this + + operator fun Pair.unaryPlus() { + properties[first] = second + } + + infix fun String.to(value: String) { + properties[this] = value + } + + internal fun build(): Map = properties.toMap() +} + +suspend fun ServerApi.Coroutine.start( + groupName: String, + block: ServerStartBuilder.() -> Unit +): Server { + val builder = ServerStartBuilder().apply(block) + return startServer(groupName, builder.startCause) +} + +suspend fun ServerApi.Coroutine.stop( + id: String, + block: ServerStopBuilder.() -> Unit +): Server { + val builder = ServerStopBuilder().apply(block) + return stopServer(id, builder.stopCause) +} + +suspend fun ServerApi.Coroutine.stopByGroup( + groupName: String, + numericalId: Long, + block: ServerStopBuilder.() -> Unit +): Server { + val builder = ServerStopBuilder().apply(block) + return stopServer(groupName, numericalId, builder.stopCause) +} + +suspend fun ServerApi.Coroutine.updateState( + id: String, + block: ServerStateBuilder.() -> Unit +): Server { + val builder = ServerStateBuilder().apply(block) + return updateServerState(id, builder.state) +} + +suspend fun ServerApi.Coroutine.updateProperty( + id: String, + block: ServerPropertyBuilder.() -> Unit +): Server { + val builder = ServerPropertyBuilder().apply(block) + var updatedServer: Server? = null + builder.build().forEach { (key, value) -> + updatedServer = updateServerProperty(id, key, value) + } + return updatedServer ?: throw IllegalStateException("Failed to update server properties") +} + +suspend fun ServerApi.Coroutine.getAllServers(block: (List) -> Unit = {}) { + block(getAllServers()) +} + +suspend fun ServerApi.Coroutine.getServer( + id: String, + block: (Server) -> Unit = {} +) { + block(getServerById(id)) +} + +suspend fun ServerApi.Coroutine.getServersByGroup( + groupName: String, + block: (List) -> Unit = {} +) { + block(getServersByGroup(groupName)) +} + +suspend fun ServerApi.Coroutine.getServersByGroup( + group: Group, + block: (List) -> Unit = {} +) { + block(getServersByGroup(group)) +} + +suspend fun ServerApi.Coroutine.getServerByNumerical( + groupName: String, + numericalId: Long, + block: (Server) -> Unit = {} +) { + block(getServerByNumerical(groupName, numericalId)) +} + +suspend fun ServerApi.Coroutine.getServersByType( + type: ServerType, + block: (List) -> Unit = {} +) { + block(getServersByType(type)) +} \ No newline at end of file diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerExtensions.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerExtensions.kt new file mode 100644 index 0000000..84c912a --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/extensions/ServerExtensions.kt @@ -0,0 +1,54 @@ +package app.simplecloud.controller.api.dsl.extensions + +import app.simplecloud.controller.api.ServerApi +import app.simplecloud.controller.shared.server.Server + +suspend fun ServerApi.Coroutine.startServer( + groupName: String, + block: ServerStartBuilder.() -> Unit = {} +): Server { + val builder = ServerStartBuilder().apply(block) + val server = startServer(groupName, builder.startCause) + + builder.getProperties().forEach { (key, value) -> + updateServerProperty(server.uniqueId, key, value) + } + + return getServerById(server.uniqueId) +} + +suspend fun ServerApi.Coroutine.stopServer( + id: String, + block: ServerStopBuilder.() -> Unit = {} +): Server { + val builder = ServerStopBuilder().apply(block) + return stopServer(id, builder.stopCause) +} + +suspend fun ServerApi.Coroutine.updateServerState( + id: String, + block: ServerStateBuilder.() -> Unit +): Server { + val builder = ServerStateBuilder().apply(block) + return updateServerState(id, builder.state) +} + +suspend fun ServerApi.Coroutine.updateProperty( + id: String, + key: String, + value: Any +): Server { + return updateServerProperty(id, key, value) +} + +suspend fun ServerApi.Coroutine.updateProperties( + id: String, + block: ServerPropertyBuilder.() -> Unit +): Server { + val builder = ServerPropertyBuilder().apply(block) + var updatedServer: Server? = null + builder.build().forEach { (key, value) -> + updatedServer = updateServerProperty(id, key, value) + } + return updatedServer ?: throw IllegalStateException("Failed to update server properties") +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/markers/DslMarkers.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/markers/DslMarkers.kt new file mode 100644 index 0000000..134332e --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/markers/DslMarkers.kt @@ -0,0 +1,13 @@ +package app.simplecloud.controller.api.dsl.markers + +@DslMarker +annotation class ControllerDsl + +@DslMarker +annotation class ServerDsl + +@DslMarker +annotation class GroupDsl + +@DslMarker +annotation class PropertyDsl diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/GroupScope.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/GroupScope.kt new file mode 100644 index 0000000..db24fa8 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/GroupScope.kt @@ -0,0 +1,97 @@ +package app.simplecloud.controller.api.dsl.scopes + +import app.simplecloud.controller.api.GroupApi +import app.simplecloud.controller.api.dsl.builders.PropertyBuilder +import app.simplecloud.controller.api.dsl.markers.GroupDsl +import app.simplecloud.controller.shared.group.Group +import build.buf.gen.simplecloud.controller.v1.ServerType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +@GroupDsl +class GroupScope(private val api: GroupApi.Coroutine) { + suspend fun create(block: GroupCreateBuilder.() -> Unit): Group { + val builder = GroupCreateBuilder().apply(block) + return api.createGroup(builder.build()) + } + + suspend fun update(block: GroupUpdateBuilder.() -> Unit): Group { + val builder = GroupUpdateBuilder().apply(block) + return api.updateGroup(builder.build()) + } + + suspend fun delete(name: String) = api.deleteGroup(name) + + suspend fun getByName(name: String) = api.getGroupByName(name) + + suspend fun getAll() = api.getAllGroups() + + suspend fun getByType(type: ServerType) = api.getGroupsByType(type) + + suspend fun parallel(block: suspend ParallelGroupScope.() -> Unit) { + coroutineScope { + ParallelGroupScope(this, api).block() + } + } +} + +class ParallelGroupScope( + private val scope: CoroutineScope, + private val api: GroupApi.Coroutine +) { + fun getByName(name: String) = scope.async { api.getGroupByName(name) } + fun getAll() = scope.async { api.getAllGroups() } + fun getByType(type: ServerType) = scope.async { api.getGroupsByType(type) } +} + +@GroupDsl +class GroupCreateBuilder { + var name: String = "" + var type: ServerType = ServerType.UNKNOWN_SERVER + var minMemory: Long = 0 + var maxMemory: Long = 0 + var startPort: Long = 0 + var minOnlineCount: Long = 0 + var maxOnlineCount: Long = 0 + var maxPlayers: Long = 0 + var newServerPlayerRatio: Long = -1 + private val propertyBuilder = PropertyBuilder() + + fun properties(block: PropertyBuilder.() -> Unit) { + propertyBuilder.apply(block) + } + + internal fun build() = Group( + name = name, + type = type, + minMemory = minMemory, + maxMemory = maxMemory, + startPort = startPort, + minOnlineCount = minOnlineCount, + maxOnlineCount = maxOnlineCount, + maxPlayers = maxPlayers, + newServerPlayerRatio = newServerPlayerRatio, + properties = propertyBuilder.build() + ) +} + +@GroupDsl +class GroupUpdateBuilder { + private var existingGroup: Group? = null + private var propertyBuilder = PropertyBuilder() + + fun fromGroup(group: Group) { + existingGroup = group + propertyBuilder.properties(*group.properties.toList().toTypedArray()) + } + + fun properties(block: PropertyBuilder.() -> Unit) { + propertyBuilder.apply(block) + } + + internal fun build(): Group { + requireNotNull(existingGroup) { "Existing group must be set using fromGroup()" } + return existingGroup!!.copy(properties = propertyBuilder.build()) + } +} diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/ServerScope.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/ServerScope.kt new file mode 100644 index 0000000..8a1a598 --- /dev/null +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/dsl/scopes/ServerScope.kt @@ -0,0 +1,26 @@ +package app.simplecloud.controller.api.dsl.scopes + +import app.simplecloud.controller.api.ServerApi +import app.simplecloud.controller.api.dsl.builders.ServerStartBuilder +import app.simplecloud.controller.api.dsl.builders.ServerStopBuilder +import app.simplecloud.controller.api.dsl.markers.ServerDsl +import build.buf.gen.simplecloud.controller.v1.ServerState + +@ServerDsl +class ServerScope(private val api: ServerApi.Coroutine) { + + suspend fun start(groupName: String, block: ServerStartBuilder.() -> Unit) { + val builder = ServerStartBuilder().apply(block) + api.startServer(groupName, builder.startCause) + } + + suspend fun stop(id: String, block: ServerStopBuilder.() -> Unit) { + val builder = ServerStopBuilder().apply(block) + api.stopServer(id, builder.stopCause) + } + + suspend fun updateState(id: String, state: ServerState) { + api.updateServerState(id, state) + } + +} \ No newline at end of file From a58cc5a9d1ec59934ed67eef2a285d90464ecc1e Mon Sep 17 00:00:00 2001 From: PrexorJustin Date: Wed, 18 Dec 2024 22:57:50 +0100 Subject: [PATCH 48/65] feature: logic behind stop group command --- .../simplecloud/controller/api/ServerApi.kt | 65 ++++++++++++++++++- .../impl/coroutines/ServerApiCoroutineImpl.kt | 24 ++++++- .../api/impl/future/ServerApiFutureImpl.kt | 35 +++++++++- .../runtime/oauth/AuthenticationHandler.kt | 3 +- .../runtime/reconciler/GroupReconciler.kt | 10 ++- .../runtime/server/ServerService.kt | 39 +++++++++++ .../controller/shared/group/Group.kt | 3 + .../controller/shared/group/GroupTimeout.kt | 16 +++++ .../controller/shared/server/Server.kt | 2 - gradle/libs.versions.toml | 3 +- 10 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt index 75675bc..383ccbf 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt @@ -1,11 +1,11 @@ package app.simplecloud.controller.api import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.ServerType import app.simplecloud.controller.shared.server.Server import build.buf.gen.simplecloud.controller.v1.ServerStartCause import build.buf.gen.simplecloud.controller.v1.ServerState import build.buf.gen.simplecloud.controller.v1.ServerStopCause +import build.buf.gen.simplecloud.controller.v1.ServerType import java.util.concurrent.CompletableFuture interface ServerApi { @@ -52,14 +52,21 @@ interface ServerApi { * @param groupName the group name of the group the new server should be of. * @return a [CompletableFuture] with a [Server] or null. */ - fun startServer(groupName: String, startCause: ServerStartCause = ServerStartCause.API_START): CompletableFuture + fun startServer( + groupName: String, + startCause: ServerStartCause = ServerStartCause.API_START + ): CompletableFuture /** * @param groupName the group name of the servers group. * @param numericalId the numerical id of the server. * @return a [CompletableFuture] with the stopped [Server]. */ - fun stopServer(groupName: String, numericalId: Long, stopCause: ServerStopCause = ServerStopCause.API_STOP): CompletableFuture + fun stopServer( + groupName: String, + numericalId: Long, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture /** * @param id the id of the server. @@ -67,6 +74,32 @@ interface ServerApi { */ fun stopServer(id: String, stopCause: ServerStopCause = ServerStopCause.API_STOP): CompletableFuture + /** + * Stops all servers within a specified group. + * + * @param groupName The name of the server group to stop. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A [CompletableFuture] containing a list of stopped [Server] instances. + */ + fun stopServers( + groupName: String, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture> + + /** + * Stops all servers within a specified group and sets a timeout to prevent new server starts for the group. + * + * @param groupName The name of the server group to stop. + * @param timeoutSeconds The duration (in seconds) for which new server starts will be prevented. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A [CompletableFuture] containing a list of stopped [Server] instances. + */ + fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture> + /** * @param id the id of the server. * @param state the new state of the server. @@ -145,6 +178,32 @@ interface ServerApi { */ suspend fun stopServer(id: String, stopCause: ServerStopCause = ServerStopCause.API_STOP): Server + /** + * Stops all servers within a specified group. + * + * @param groupName The name of the server group to stop. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A list of stopped [Server] instances. + */ + suspend fun stopServers( + groupName: String, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): List + + /** + * Stops all servers within a specified group and sets a timeout to prevent new server starts for the group. + * + * @param groupName The name of the server group to stop. + * @param timeoutSeconds The duration (in seconds) for which new server starts will be prevented. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A list of stopped [Server] instances. + */ + suspend fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): List + /** * @param id the id of the server. * @param state the new state of the server. diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt index c41b314..ea08a3a 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt @@ -2,9 +2,9 @@ package app.simplecloud.controller.api.impl.coroutines import app.simplecloud.controller.api.ServerApi import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials +import build.buf.gen.simplecloud.controller.v1.* import io.grpc.ManagedChannel class ServerApiCoroutineImpl( @@ -13,7 +13,8 @@ class ServerApiCoroutineImpl( ) : ServerApi.Coroutine { private val serverServiceStub: ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub = - ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub(managedChannel).withCallCredentials(authCallCredentials) + ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub(managedChannel) + .withCallCredentials(authCallCredentials) override suspend fun getAllServers(): List { return serverServiceStub.getAllServers(getAllServersRequest {}).serversList.map { @@ -100,6 +101,25 @@ class ServerApiCoroutineImpl( ) } + override suspend fun stopServers(groupName: String, stopCause: ServerStopCause): List { + return serverServiceStub.stopServersByGroup(stopServersByGroupRequest { + this.groupName = groupName + this.stopCause = stopCause + }).serversList.map { + Server.fromDefinition(it) + } + } + + override suspend fun stopServers(groupName: String, timeoutSeconds: Int, stopCause: ServerStopCause): List { + return serverServiceStub.stopServersByGroupWithTimeout(stopServersByGroupWithTimeoutRequest { + this.groupName = groupName + this.stopCause = stopCause + this.timeoutSeconds = timeoutSeconds + }).serversList.map { + Server.fromDefinition(it) + } + } + override suspend fun updateServerState(id: String, state: ServerState): Server { return Server.fromDefinition( serverServiceStub.updateServerState( diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt index c106d59..ee81b4b 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt @@ -2,10 +2,10 @@ package app.simplecloud.controller.api.impl.future import app.simplecloud.controller.api.ServerApi import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.droplet.api.future.toCompletable +import build.buf.gen.simplecloud.controller.v1.* import io.grpc.ManagedChannel import java.util.concurrent.CompletableFuture @@ -79,7 +79,11 @@ class ServerApiFutureImpl( } } - override fun stopServer(groupName: String, numericalId: Long, stopCause: ServerStopCause): CompletableFuture { + override fun stopServer( + groupName: String, + numericalId: Long, + stopCause: ServerStopCause + ): CompletableFuture { return serverServiceStub.stopServerByNumerical( StopServerByNumericalRequest.newBuilder() .setGroupName(groupName) @@ -102,6 +106,33 @@ class ServerApiFutureImpl( } } + override fun stopServers(groupName: String, stopCause: ServerStopCause): CompletableFuture> { + return serverServiceStub.stopServersByGroup( + StopServersByGroupRequest.newBuilder() + .setGroupName(groupName) + .setStopCause(stopCause) + .build() + ).toCompletable().thenApply { + Server.fromDefinition(it.serversList) + } + } + + override fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause + ): CompletableFuture> { + return serverServiceStub.stopServersByGroupWithTimeout( + StopServersByGroupWithTimeoutRequest.newBuilder() + .setGroupName(groupName) + .setStopCause(stopCause) + .setTimeoutSeconds(timeoutSeconds) + .build() + ).toCompletable().thenApply { + Server.fromDefinition(it.serversList) + } + } + override fun updateServerState(id: String, state: ServerState): CompletableFuture { return serverServiceStub.updateServerState( UpdateServerStateRequest.newBuilder() diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt index dc83558..1a61195 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -130,7 +130,8 @@ class AuthenticationHandler( call.respond(HttpStatusCode.NotFound, "User not found") return } - call.respond(mapOf( + call.respond( + mapOf( "user_id" to user.userId, "username" to user.username, "scope" to user.scopes.joinToString(" "), diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt index 8b562cb..58e97c5 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt @@ -99,8 +99,14 @@ class GroupReconciler( private suspend fun startServers() { val available = serverHostRepository.areServerHostsAvailable() - if(!available) return - if(isNewServerNeeded()) + if (!available) return + group.timeout?.let { + if (it.isCooldownActive()) { + return + } + } + + if (isNewServerNeeded()) startServer() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index bda363f..2b9868f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -3,6 +3,7 @@ package app.simplecloud.controller.runtime.server import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.shared.group.Group +import app.simplecloud.controller.shared.group.GroupTimeout import app.simplecloud.controller.shared.host.ServerHost import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials @@ -198,6 +199,43 @@ class ServerService( } } + override suspend fun stopServersByGroupWithTimeout(request: StopServersByGroupWithTimeoutRequest): StopServersByGroupResponse { + return stopServersByGroup(request.groupName, request.timeoutSeconds, request.stopCause) + } + + override suspend fun stopServersByGroup(request: StopServersByGroupRequest): StopServersByGroupResponse { + return stopServersByGroup(request.groupName, null, request.stopCause) + } + + private suspend fun stopServersByGroup( + groupName: String, + timeout: Int?, + cause: ServerStopCause = ServerStopCause.NATURAL_STOP + ): StopServersByGroupResponse { + val group = groupRepository.find(groupName) + ?: throw StatusException(Status.NOT_FOUND.withDescription("No group was found matching this name. $groupName")) + val groupServers = serverRepository.findServersByGroup(group.name) + if (groupServers.isEmpty()) { + throw StatusException(Status.NOT_FOUND.withDescription("No server was found matching this group name. ${group.name}")) + } + + val serverDefinitionList = mutableListOf() + + try { + timeout?.let { + group.timeout = GroupTimeout(it); + } + + groupServers.forEach { server -> + serverDefinitionList.add(stopServer(server.toDefinition(), cause)) + } + + return stopServersByGroupResponse { servers.addAll(serverDefinitionList) } + } catch (e: Exception) { + throw StatusException(Status.INTERNAL.withDescription("Error whilst stopping server by group").withCause(e)) + } + } + private suspend fun stopServer( server: ServerDefinition, cause: ServerStopCause = ServerStopCause.NATURAL_STOP @@ -244,6 +282,7 @@ class ServerService( ?: throw StatusException(Status.NOT_FOUND.withDescription("Server with id ${request.serverId} does not exist.")) val serverBefore = server.copy() server.state = request.serverState + println("STATE ${server.state}") serverRepository.save(server) pubSubClient.publish( "event", diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt index 5346678..0cff3cc 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt @@ -18,6 +18,9 @@ data class Group( val properties: Map = mutableMapOf() ) { + @Transient + var timeout: GroupTimeout? = null + fun toDefinition(): GroupDefinition { return GroupDefinition.newBuilder() .setName(name) diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt new file mode 100644 index 0000000..5e216b4 --- /dev/null +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt @@ -0,0 +1,16 @@ +package app.simplecloud.controller.shared.group + +data class GroupTimeout(val timeoutDuration: Int, val timeoutBegin: Long = System.currentTimeMillis()) { + + fun isCooldownActive(): Boolean { + val cooldownEndTime = timeoutBegin + timeoutDuration * 1000L + return System.currentTimeMillis() < cooldownEndTime + } + + fun remainingTimeInSeconds(): Long { + val cooldownEndTime = timeoutBegin + timeoutDuration * 1000L + val remainingTime = cooldownEndTime - System.currentTimeMillis() + return if (remainingTime > 0) remainingTime / 1000 else 0 + } + +} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt index 8e7bbf6..cce4dcd 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt @@ -4,9 +4,7 @@ import app.simplecloud.droplet.api.time.ProtobufTimestamp import build.buf.gen.simplecloud.controller.v1.ServerDefinition import build.buf.gen.simplecloud.controller.v1.ServerState import build.buf.gen.simplecloud.controller.v1.ServerType -import java.time.Instant import java.time.LocalDateTime -import java.time.ZoneId data class Server( val uniqueId: String, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0b906c..c076a24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.7e049d6" +droplet-api = "0.0.1-dev.cf07a34" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" @@ -24,7 +24,6 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } - simplecloud-droplet-api = { module = "app.simplecloud.droplet.api:droplet-api", version.ref = "droplet-api" } simplecloud-pubsub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simplecloud-pubsub" } simplecloud-metrics = { module = "app.simplecloud:internal-metrics-api", version.ref = "simplecloud-metrics" } From 0c7f73b540a9d3c84ce720521c54fe04968d8252 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 20 Dec 2024 15:35:34 +0100 Subject: [PATCH 49/65] feat: metric tracking for servers/groups --- .../controller/runtime/ControllerRuntime.kt | 13 +- .../controller/runtime/MetricsEventNames.kt | 8 + .../runtime/YamlDirectoryRepository.kt | 34 ++- .../runtime/group/GroupRepository.kt | 114 ++++++++- .../runtime/server/ServerRepository.kt | 2 +- .../runtime/server/ServerService.kt | 228 +++++++++++++++++- 6 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/MetricsEventNames.kt diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 29e167c..159aa85 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -33,7 +33,12 @@ class ControllerRuntime( private val database = DatabaseFactory.createDatabase(controllerStartCommand.databaseUrl) private val authCallCredentials = AuthCallCredentials(controllerStartCommand.authSecret) - private val groupRepository = GroupRepository(controllerStartCommand.groupPath) + private val pubSubClient = PubSubClient( + controllerStartCommand.grpcHost, + controllerStartCommand.pubSubGrpcPort, + authCallCredentials + ) + private val groupRepository = GroupRepository(controllerStartCommand.groupPath, pubSubClient) private val numericalIdRepository = ServerNumericalIdRepository() private val serverRepository = ServerRepository(database, numericalIdRepository) private val hostRepository = ServerHostRepository() @@ -160,11 +165,7 @@ class ControllerRuntime( hostRepository, groupRepository, authCallCredentials, - PubSubClient( - controllerStartCommand.grpcHost, - controllerStartCommand.pubSubGrpcPort, - authCallCredentials - ), + pubSubClient, serverHostAttacher ) ) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/MetricsEventNames.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/MetricsEventNames.kt new file mode 100644 index 0000000..bae8fbe --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/MetricsEventNames.kt @@ -0,0 +1,8 @@ +package app.simplecloud.controller.runtime + +object MetricsEventNames { + + private const val PREFIX = "metrics-droplet:" + const val RECORD_METRIC = "${PREFIX}record-metric" + +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt index 20759dc..30b1b4e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt @@ -14,6 +14,7 @@ import java.nio.file.* abstract class YamlDirectoryRepository( private val directory: Path, private val clazz: Class, + private val watcherEvents: WatcherEvents = WatcherEvents.empty() ) : LoadableRepository { private val logger = LogManager.getLogger(this::class.java) @@ -113,13 +114,24 @@ abstract class YamlDirectoryRepository( val kind = event.kind() logger.info("Detected change in $resolvedPath (${getChangeStatus(kind)})") when (kind) { - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_MODIFY - -> { - load(resolvedPath.toFile()) + StandardWatchEventKinds.ENTRY_CREATE -> { + val entity = load(resolvedPath.toFile()) + if (entity != null) { + watcherEvents.onCreate(entity) + } + } + StandardWatchEventKinds.ENTRY_MODIFY -> { + val entity = load(resolvedPath.toFile()) + if (entity != null) { + watcherEvents.onModify(entity) + } } StandardWatchEventKinds.ENTRY_DELETE -> { + val entity = entities[resolvedPath.toFile()] + if (entity != null) { + watcherEvents.onDelete(entity) + } deleteFile(resolvedPath.toFile()) } } @@ -138,4 +150,18 @@ abstract class YamlDirectoryRepository( } } + interface WatcherEvents { + fun onCreate(entity: E) + fun onDelete(entity: E) + fun onModify(entity: E) + + companion object { + fun empty(): WatcherEvents = object : WatcherEvents { + override fun onCreate(entity: E) {} + override fun onDelete(entity: E) {} + override fun onModify(entity: E) {} + } + } + } + } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupRepository.kt index 176920a..ce49285 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/group/GroupRepository.kt @@ -1,13 +1,19 @@ package app.simplecloud.controller.runtime.group +import app.simplecloud.controller.runtime.MetricsEventNames import app.simplecloud.controller.runtime.YamlDirectoryRepository import app.simplecloud.controller.shared.group.Group +import app.simplecloud.droplet.api.time.ProtobufTimestamp +import app.simplecloud.pubsub.PubSubClient +import build.buf.gen.simplecloud.metrics.v1.metric +import build.buf.gen.simplecloud.metrics.v1.metricMeta import java.nio.file.Path -import java.util.concurrent.CompletableFuture +import java.time.LocalDateTime class GroupRepository( - path: Path -) : YamlDirectoryRepository(path, Group::class.java) { + path: Path, + private val pubSubClient: PubSubClient +) : YamlDirectoryRepository(path, Group::class.java, WatcherEvents(pubSubClient)) { override fun getFileName(identifier: String): String { return "$identifier.yml" } @@ -23,4 +29,106 @@ class GroupRepository( override suspend fun getAll(): List { return entities.values.toList() } + + private class WatcherEvents( + private val pubsubClient: PubSubClient + ) : YamlDirectoryRepository.WatcherEvents { + + override fun onCreate(entity: Group) { + pubsubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = entity.name + }, + metricMeta { + dataName = "status" + dataValue = "CREATED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "GROUP" + }, + metricMeta { + dataName = "groupName" + dataValue = entity.name + }, + metricMeta { + dataName = "by" + dataValue = "FILE_WATCHER" + } + ) + ) + }) + } + + override fun onDelete(entity: Group) { + pubsubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = entity.name + }, + metricMeta { + dataName = "status" + dataValue = "DELETED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "GROUP" + }, + metricMeta { + dataName = "groupName" + dataValue = entity.name + }, + metricMeta { + dataName = "by" + dataValue = "FILE_WATCHER" + } + ) + ) + }) + } + + override fun onModify(entity: Group) { + pubsubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = entity.name + }, + metricMeta { + dataName = "status" + dataValue = "EDITED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "GROUP" + }, + metricMeta { + dataName = "groupName" + dataValue = entity.name + }, + metricMeta { + dataName = "by" + dataValue = "FILE_WATCHER" + } + ) + ) + }) + } + + } } \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerRepository.kt index 81c5ef2..55fcd05 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerRepository.kt @@ -18,7 +18,7 @@ import java.time.LocalDateTime class ServerRepository( private val database: Database, - private val numericalIdRepository: ServerNumericalIdRepository + private val numericalIdRepository: ServerNumericalIdRepository, ) : LoadableRepository { override suspend fun find(identifier: String): Server? { diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index bda363f..adccea2 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -1,5 +1,6 @@ package app.simplecloud.controller.runtime.server +import app.simplecloud.controller.runtime.MetricsEventNames import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.shared.group.Group @@ -9,6 +10,8 @@ import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.droplet.api.time.ProtobufTimestamp import app.simplecloud.pubsub.PubSubClient import build.buf.gen.simplecloud.controller.v1.* +import build.buf.gen.simplecloud.metrics.v1.metric +import build.buf.gen.simplecloud.metrics.v1.metricMeta import io.grpc.Status import io.grpc.StatusException import org.apache.logging.log4j.LogManager @@ -68,12 +71,51 @@ class ServerService( try { val before = serverRepository.find(server.uniqueId) ?: throw StatusException(Status.NOT_FOUND.withDescription("Server not found")) - pubSubClient.publish( - "event", - ServerUpdateEvent.newBuilder() - .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) - .setServerBefore(before.toDefinition()).setServerAfter(request.server).build() - ) + val wasUpdated = before != server + + if (wasUpdated) { + pubSubClient.publish( + "event", + ServerUpdateEvent.newBuilder() + .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setServerBefore(before.toDefinition()).setServerAfter(request.server).build() + ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.group} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "EDITED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.group + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = "API" + } + ) + ) + }) + } + serverRepository.save(server) return server.toDefinition() } catch (e: Exception) { @@ -101,6 +143,41 @@ class ServerService( .setTerminationMode(ServerTerminationMode.UNKNOWN_MODE) //TODO: Add proto fields to make changing this possible .build() ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.group} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "STOPPED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.group + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = ServerStopCause.NATURAL_STOP.toString() + } + ) + ) + }) + return server.toDefinition() } } @@ -137,6 +214,41 @@ class ServerService( .setStartCause(request.startCause) .build() ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.groupName} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "STARTED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.groupName + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = request.startCause.toString() + } + ) + ) + }) + return server } catch (e: Exception) { throw StatusException(Status.INTERNAL.withDescription("Error whilst starting server").withCause(e)) @@ -217,6 +329,41 @@ class ServerService( .setTerminationMode(ServerTerminationMode.UNKNOWN_MODE) //TODO: Add proto fields to make changing this possible .build() ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.groupName} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "STOPPED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.groupName + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = cause.toString() + } + ) + ) + }) + serverRepository.delete(Server.fromDefinition(stopped)) return stopped } catch (e: Exception) { @@ -236,6 +383,40 @@ class ServerService( ServerUpdateEvent.newBuilder().setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setServerBefore(serverBefore.toDefinition()).setServerAfter(server.toDefinition()).build() ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.group} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "EDITED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.group + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = "API" + } + ) + ) + }) return server.toDefinition() } @@ -250,6 +431,41 @@ class ServerService( ServerUpdateEvent.newBuilder().setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) .setServerBefore(serverBefore.toDefinition()).setServerAfter(server.toDefinition()).build() ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.group} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "EDITED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.group + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = "API" + } + ) + ) + }) + return server.toDefinition() } From 4652000f1e9d0641a781258642200e1e417fc483 Mon Sep 17 00:00:00 2001 From: PrexorJustin Date: Fri, 20 Dec 2024 16:04:10 +0100 Subject: [PATCH 50/65] removed debug message --- .../app/simplecloud/controller/runtime/server/ServerService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 2b9868f..9306a0e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -282,7 +282,6 @@ class ServerService( ?: throw StatusException(Status.NOT_FOUND.withDescription("Server with id ${request.serverId} does not exist.")) val serverBefore = server.copy() server.state = request.serverState - println("STATE ${server.state}") serverRepository.save(server) pubSubClient.publish( "event", From 9243cc82e9835198f521fcc8a67cac4b84312992 Mon Sep 17 00:00:00 2001 From: Philipp Date: Sun, 22 Dec 2024 18:00:03 +0100 Subject: [PATCH 51/65] fix: update server property event change --- .../runtime/server/ServerService.kt | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index adccea2..1d1f779 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -375,48 +375,55 @@ class ServerService( override suspend fun updateServerProperty(request: UpdateServerPropertyRequest): ServerDefinition { val server = serverRepository.find(request.serverId) ?: throw StatusException(Status.NOT_FOUND.withDescription("Server with id ${request.serverId} does not exist.")) - val serverBefore = server.copy() + val serverBefore = Server.fromDefinition(server.toDefinition()) server.properties[request.propertyKey] = request.propertyValue serverRepository.save(server) - pubSubClient.publish( - "event", - ServerUpdateEvent.newBuilder().setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) - .setServerBefore(serverBefore.toDefinition()).setServerAfter(server.toDefinition()).build() - ) - pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { - metricType = "ACTIVITY_LOG" - metricValue = 1L - time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) - meta.addAll( - listOf( - metricMeta { - dataName = "displayName" - dataValue = "${server.group} #${server.numericalId}" - }, - metricMeta { - dataName = "status" - dataValue = "EDITED" - }, - metricMeta { - dataName = "resourceType" - dataValue = "SERVER" - }, - metricMeta { - dataName = "groupName" - dataValue = server.group - }, - metricMeta { - dataName = "numericalId" - dataValue = server.numericalId.toString() - }, - metricMeta { - dataName = "by" - dataValue = "API" - } - ) + if (serverBefore.properties[request.propertyKey] != server.properties[request.propertyKey]) { + pubSubClient.publish( + "event", + ServerUpdateEvent.newBuilder() + .setUpdatedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setServerBefore(serverBefore.toDefinition()) + .setServerAfter(server.toDefinition()) + .build() ) - }) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.group} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "EDITED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.group + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = "API" + } + ) + ) + }) + } + return server.toDefinition() } From e35c8c3d0b9e82b202c111f9b5d3085cb17e7a25 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 25 Dec 2024 20:08:19 +0100 Subject: [PATCH 52/65] fix: enum de/serialization --- .../runtime/YamlDirectoryRepository.kt | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt index 30b1b4e..db2c0e9 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt @@ -2,12 +2,16 @@ package app.simplecloud.controller.runtime import kotlinx.coroutines.* import org.apache.logging.log4j.LogManager +import org.spongepowered.configurate.ConfigurationNode import org.spongepowered.configurate.ConfigurationOptions import org.spongepowered.configurate.kotlin.objectMapperFactory import org.spongepowered.configurate.loader.ParsingException +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer import org.spongepowered.configurate.yaml.NodeStyle import org.spongepowered.configurate.yaml.YamlConfigurationLoader import java.io.File +import java.lang.reflect.Type import java.nio.file.* @@ -75,7 +79,9 @@ abstract class YamlDirectoryRepository( protected fun save(fileName: String, entity: E) { val file = directory.resolve(fileName).toFile() val loader = getOrCreateLoader(file) - val node = loader.createNode(ConfigurationOptions.defaults()) + val node = loader.createNode(ConfigurationOptions.defaults().serializers { + it.register(Enum::class.java, GenericEnumSerializer) + }) node.set(clazz, entity) loader.save(node) entities[file] = entity @@ -89,6 +95,7 @@ abstract class YamlDirectoryRepository( .defaultOptions { options -> options.serializers { builder -> builder.registerAnnotatedObjects(objectMapperFactory()) + builder.register(Enum::class.java, GenericEnumSerializer) } }.build() } @@ -120,11 +127,12 @@ abstract class YamlDirectoryRepository( watcherEvents.onCreate(entity) } } + StandardWatchEventKinds.ENTRY_MODIFY -> { - val entity = load(resolvedPath.toFile()) - if (entity != null) { - watcherEvents.onModify(entity) - } + val entity = load(resolvedPath.toFile()) + if (entity != null) { + watcherEvents.onModify(entity) + } } StandardWatchEventKinds.ENTRY_DELETE -> { @@ -164,4 +172,25 @@ abstract class YamlDirectoryRepository( } } + private object GenericEnumSerializer : TypeSerializer> { + override fun deserialize(type: Type, node: ConfigurationNode): Enum<*> { + val value = node.string ?: throw SerializationException("No value present in node") + + if (type !is Class<*> || !type.isEnum) { + throw SerializationException("Type is not an enum class") + } + + @Suppress("UNCHECKED_CAST") + return try { + java.lang.Enum.valueOf(type as Class>, value) + } catch (e: IllegalArgumentException) { + throw SerializationException("Invalid enum constant") + } + } + + override fun serialize(type: Type, obj: Enum<*>?, node: ConfigurationNode) { + node.set(obj?.name) + } + } + } \ No newline at end of file From 3ac3fe7dacc818ddf948de304400c2c592b453fc Mon Sep 17 00:00:00 2001 From: dayeeet Date: Thu, 26 Dec 2024 01:08:01 +0100 Subject: [PATCH 53/65] refactor: update readme --- readme.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index e7394f5..94efe8a 100644 --- a/readme.md +++ b/readme.md @@ -1,27 +1,101 @@ -# SimpleCloud v3 Controller +# Controller -Process that (automatically) manages minecraft server deployments (across multiple root-servers). -At least one [ServerHost](#serverhosts) is needed to actually start servers. -> Please visit [our documentation](https://docs.simplecloud.app/controller) to learn how exactly it works +![Banner][banner] +
+ +[![Modrinth][badge-modrinth]][modrinth] +[![Dev][badge-dev]][dev] +[![License][badge-license]][license] +
+ +[![Discord][badge-discord]][social-discord] +[![Follow @simplecloudapp][badge-x]][social-x] +[![Follow @simplecloudapp][badge-bluesky]][social-bluesky] +[![Follow @simplecloudapp][badge-youtube]][social-youtube] +
+ +[Report a Bug][issue-bug-report] +ยท +[Request a Feature][issue-feature-request] +
+ +๐ŸŒŸ Give us a star โ€” your support means the world to us! +
+
+ +> All information about this project can be found in our detailed [documentation][docs-thisproject]. + +The controller is a small program that keeps track of server groups and their online servers and manages them. It's the heart of SimpleCloud v3. ## Features - [x] Reconciler (auto-deploying for servers) -- [x] [API](#api-usage) using [gRPC](https://grpc.io/) +- [x] API using [gRPC](https://grpc.io/) - [x] Server cache [SQL](https://en.wikipedia.org/wiki/SQL)-Database (any dialect) +- [ ] Multi controller support +## Dependency + +> For always up-to-date artifacts visit [dev artifacts][dev-artifacts] or [artifacts][artifacts]. + +> Note: If you want to use the dev version, you have to use the [snapshot repository][snapshots]. + +### Gradle Kotlin +```kt +implementation("app.simplecloud.controller:controller-api:VERSION") +``` +### Gradle Groovy +```groovy +implementation 'app.simplecloud.controller:controller-api:VERSION' +``` + +### Maven +```xml + + app.simplecloud.controller + controller-api + VERSION + +``` + +## Contributing +Contributions to SimpleCloud are welcome and highly appreciated. However, before you jump right into it, we would like you to read our [Contribution Guide][docs-contribute]. + +## License +This repository is licensed under [Apache 2.0][license]. + + + + + +[banner]: https://raw.githubusercontent.com/simplecloudapp/branding/refs/heads/main/readme/banner/controller.png +[issue-bug-report]: https://github.com/theSimpleCloud/simplecloud-controller/issues/new?labels=bug&projects=template=01_BUG-REPORT.yml&title=%5BBUG%5D+%3Ctitle%3E +[issue-feature-request]: https://github.com/theSimpleCloud/simplecloud-controller/discussions/new?category=ideas +[docs-thisproject]: https://docs.simplecloud.app/controller +[docs-contribute]: https://docs.simplecloud.app/contribute -## ServerHosts +[modrinth]: https://modrinth.com/organization/simplecloud +[maven-central]: https://central.sonatype.com/artifact/app.simplecloud.controller/controller-api +[dev]: https://repo.simplecloud.app/#/snapshots/app/simplecloud/controller/controller-api -ServerHosts are processes, that directly handle minecraft server deployments. Each root-server should have exactly one -ServerHost online. We provide a [default implementation](), -however, you can write your [own implementation](). You can have as many ServerHost instances as you like. -## API usage +[artifacts]: https://repo.simplecloud.app/#/snapshots/app/simplecloud/controller/controller-api +[dev-artifacts]: https://repo.simplecloud.app/#/snapshots/app/simplecloud/controller/controller-api -> If you are searching for documentation, please visit our [official documentation](https://docs.simplecloud.app/api) +[badge-maven-central]: https://img.shields.io/maven-central/v/app.simplecloud.controller/controller-api?labelColor=18181b&style=flat-square&color=65a30d&label=Release +[badge-dev]: https://repo.simplecloud.app/api/badge/latest/snapshots/app/simplecloud/controller/controller-api?name=Dev&style=flat-square&color=0ea5e9 -The SimpleCloud v3 Controller provides API for both server groups and actual servers. -The group API is used for [CRUD-Operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) of server -groups, whereas the server API is used to manage running servers or starting new ones. + +[license]: https://opensource.org/licenses/Apache-2.0 +[snapshots]: https://repo.simplecloud.app/#/snapshots +[social-x]: https://x.com/simplecloudapp +[social-bluesky]: https://bsky.app/profile/simplecloud.app +[social-youtube]: https://www.youtube.com/@thesimplecloud9075 +[social-discord]: https://discord.simplecloud.app +[badge-modrinth]: https://img.shields.io/badge/modrinth-18181b.svg?style=flat-square&logo=modrinth +[badge-license]: https://img.shields.io/badge/apache%202.0-blue.svg?style=flat-square&label=license&labelColor=18181b&style=flat-square&color=e11d48 +[badge-discord]: https://img.shields.io/badge/Community_Discord-d95652.svg?style=flat-square&logo=discord&color=27272a +[badge-x]: https://img.shields.io/badge/Follow_@simplecloudapp-d95652.svg?style=flat-square&logo=x&color=27272a +[badge-bluesky]: https://img.shields.io/badge/Follow_@simplecloud.app-d95652.svg?style=flat-square&logo=bluesky&color=27272a +[badge-youtube]: https://img.shields.io/badge/youtube-d95652.svg?style=flat-square&logo=youtube&color=27272a \ No newline at end of file From 7b1fce404c9c6d048b3026660d824f258a5a3614 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Fri, 27 Dec 2024 00:41:13 +0100 Subject: [PATCH 54/65] refactor: bump droplet api version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c076a24..c2116c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.cf07a34" +droplet-api = "0.0.1-dev.4d43f53" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" From c3fc82928f2fb2bd3883ba85ee0b0bdf1d1e4726 Mon Sep 17 00:00:00 2001 From: Kaseax Date: Fri, 27 Dec 2024 15:54:54 +0100 Subject: [PATCH 55/65] feat: start multiple servers --- .../runtime/server/ServerService.kt | 108 +++++++++++------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 7984ea0..8b8e81b 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -201,6 +201,29 @@ class ServerService( return getServersByTypeResponse { servers.addAll(typeServers.map { it.toDefinition() }) } } + override suspend fun startMultipleServers(request: ControllerStartMultipleServersRequest): StartMultipleServerResponse { + val host = hostRepository.find(serverRepository) + ?: throw StatusException(Status.NOT_FOUND.withDescription("No server host found, could not start servers")) + val group = groupRepository.find(request.groupName) + ?: throw StatusException(Status.NOT_FOUND.withDescription("No group was found matching this name")) + + val startedServers = mutableListOf() + + try { + for (i in 1..request.amount) { + val server = startServer(host, group) + publishServerStartEvents(server, request.startCause) + startedServers.add(server) + } + } catch (e: Exception) { + throw StatusException(Status.INTERNAL.withDescription("Error whilst starting multiple servers").withCause(e)) + } + + return StartMultipleServerResponse.newBuilder() + .addAllServers(startedServers) + .build() + } + override suspend fun startServer(request: ControllerStartServerRequest): ServerDefinition { val host = hostRepository.find(serverRepository) ?: throw StatusException(Status.NOT_FOUND.withDescription("No server host found, could not start server")) @@ -208,47 +231,8 @@ class ServerService( ?: throw StatusException(Status.NOT_FOUND.withDescription("No group was found matching this name")) try { val server = startServer(host, group) - pubSubClient.publish( - "event", ServerStartEvent.newBuilder() - .setServer(server) - .setStartedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) - .setStartCause(request.startCause) - .build() - ) - pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { - metricType = "ACTIVITY_LOG" - metricValue = 1L - time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) - meta.addAll( - listOf( - metricMeta { - dataName = "displayName" - dataValue = "${server.groupName} #${server.numericalId}" - }, - metricMeta { - dataName = "status" - dataValue = "STARTED" - }, - metricMeta { - dataName = "resourceType" - dataValue = "SERVER" - }, - metricMeta { - dataName = "groupName" - dataValue = server.groupName - }, - metricMeta { - dataName = "numericalId" - dataValue = server.numericalId.toString() - }, - metricMeta { - dataName = "by" - dataValue = request.startCause.toString() - } - ) - ) - }) + publishServerStartEvents(server, request.startCause) return server } catch (e: Exception) { @@ -279,6 +263,50 @@ class ServerService( } } + private suspend fun publishServerStartEvents(server: ServerDefinition, startCause: ServerStartCause) { + pubSubClient.publish( + "event", ServerStartEvent.newBuilder() + .setServer(server) + .setStartedAt(ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now())) + .setStartCause(startCause) + .build() + ) + + pubSubClient.publish(MetricsEventNames.RECORD_METRIC, metric { + metricType = "ACTIVITY_LOG" + metricValue = 1L + time = ProtobufTimestamp.fromLocalDateTime(LocalDateTime.now()) + meta.addAll( + listOf( + metricMeta { + dataName = "displayName" + dataValue = "${server.groupName} #${server.numericalId}" + }, + metricMeta { + dataName = "status" + dataValue = "STARTED" + }, + metricMeta { + dataName = "resourceType" + dataValue = "SERVER" + }, + metricMeta { + dataName = "groupName" + dataValue = server.groupName + }, + metricMeta { + dataName = "numericalId" + dataValue = server.numericalId.toString() + }, + metricMeta { + dataName = "by" + dataValue = startCause.toString() + } + ) + ) + }) + } + private fun buildServer(group: Group, numericalId: Int): Server { return Server.fromDefinition( ServerDefinition.newBuilder() From 6626e106841c0c2a219d548c5e29950799bf419f Mon Sep 17 00:00:00 2001 From: dayeeet Date: Fri, 27 Dec 2024 18:14:17 +0100 Subject: [PATCH 56/65] refactor: bump droplet api version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2116c1..a4625da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.4d43f53" +droplet-api = "0.0.1-dev.2b634c1" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" From e5b5ab84f3a1e152b2906be42a0c6e3df792b0ce Mon Sep 17 00:00:00 2001 From: dayeeet Date: Fri, 27 Dec 2024 18:24:17 +0100 Subject: [PATCH 57/65] refactor: bump droplet api version --- .../controller/runtime/droplet/ControllerDropletService.kt | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt index 4aead6f..118edea 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt @@ -1,5 +1,6 @@ package app.simplecloud.controller.runtime.droplet +import app.simplecloud.controller.runtime.MetricsEventNames import app.simplecloud.droplet.api.droplet.Droplet import build.buf.gen.simplecloud.controller.v1.* import io.grpc.Status diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4625da..e550a9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.2b634c1" +droplet-api = "0.0.1-dev.16b322c" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" From 8f66da2ad36a8f4f6f6d54d79ec1132fe567b500 Mon Sep 17 00:00:00 2001 From: Kaseax Date: Fri, 27 Dec 2024 18:47:41 +0100 Subject: [PATCH 58/65] feat: stop servers started after specific Timestamp --- .../runtime/server/ServerService.kt | 24 ++++++++++++++++--- gradle/libs.versions.toml | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index 8b8e81b..a644436 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -17,6 +17,7 @@ import io.grpc.Status import io.grpc.StatusException import org.apache.logging.log4j.LogManager import java.time.LocalDateTime +import java.time.ZoneOffset import java.util.* class ServerService( @@ -331,6 +332,14 @@ class ServerService( override suspend fun stopServer(request: StopServerRequest): ServerDefinition { val server = serverRepository.find(request.serverId) ?: throw StatusException(Status.NOT_FOUND.withDescription("No server was found matching this id.")) + + request.since?.let { sinceTimestamp -> + val sinceLocalDateTime = LocalDateTime.ofEpochSecond(sinceTimestamp.seconds, sinceTimestamp.nanos, ZoneOffset.UTC) + if (server.createdAt.isBefore(sinceLocalDateTime)) { + return server.toDefinition() + } + } + try { val stopped = stopServer(server.toDefinition(), request.stopCause) return stopped @@ -340,21 +349,30 @@ class ServerService( } override suspend fun stopServersByGroupWithTimeout(request: StopServersByGroupWithTimeoutRequest): StopServersByGroupResponse { - return stopServersByGroup(request.groupName, request.timeoutSeconds, request.stopCause) + val sinceLocalDateTime = request.since?.let { + LocalDateTime.ofEpochSecond(it.seconds, it.nanos, ZoneOffset.UTC) + } + return stopServersByGroup(request.groupName, request.timeoutSeconds, request.stopCause, sinceLocalDateTime) } override suspend fun stopServersByGroup(request: StopServersByGroupRequest): StopServersByGroupResponse { - return stopServersByGroup(request.groupName, null, request.stopCause) + val sinceLocalDateTime = request.since?.let { + LocalDateTime.ofEpochSecond(it.seconds, it.nanos, ZoneOffset.UTC) + } + return stopServersByGroup(request.groupName, null, request.stopCause, sinceLocalDateTime) } private suspend fun stopServersByGroup( groupName: String, timeout: Int?, - cause: ServerStopCause = ServerStopCause.NATURAL_STOP + cause: ServerStopCause = ServerStopCause.NATURAL_STOP, + since: LocalDateTime? = null ): StopServersByGroupResponse { val group = groupRepository.find(groupName) ?: throw StatusException(Status.NOT_FOUND.withDescription("No group was found matching this name. $groupName")) val groupServers = serverRepository.findServersByGroup(group.name) + .filter { since == null || it.createdAt.isAfter(since) } + if (groupServers.isEmpty()) { throw StatusException(Status.NOT_FOUND.withDescription("No server was found matching this group name. ${group.name}")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2116c1..e550a9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.4d43f53" +droplet-api = "0.0.1-dev.16b322c" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" From 71e759b8c5da80a63375bc08f950e45cca8f01af Mon Sep 17 00:00:00 2001 From: Kaseax Date: Fri, 27 Dec 2024 19:50:43 +0100 Subject: [PATCH 59/65] refactor: use ProtobufTimestamp util --- .../simplecloud/controller/runtime/server/ServerService.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index a644436..a69a2cd 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -17,7 +17,6 @@ import io.grpc.Status import io.grpc.StatusException import org.apache.logging.log4j.LogManager import java.time.LocalDateTime -import java.time.ZoneOffset import java.util.* class ServerService( @@ -334,7 +333,7 @@ class ServerService( ?: throw StatusException(Status.NOT_FOUND.withDescription("No server was found matching this id.")) request.since?.let { sinceTimestamp -> - val sinceLocalDateTime = LocalDateTime.ofEpochSecond(sinceTimestamp.seconds, sinceTimestamp.nanos, ZoneOffset.UTC) + val sinceLocalDateTime = ProtobufTimestamp.toLocalDateTime(sinceTimestamp) if (server.createdAt.isBefore(sinceLocalDateTime)) { return server.toDefinition() } @@ -350,14 +349,14 @@ class ServerService( override suspend fun stopServersByGroupWithTimeout(request: StopServersByGroupWithTimeoutRequest): StopServersByGroupResponse { val sinceLocalDateTime = request.since?.let { - LocalDateTime.ofEpochSecond(it.seconds, it.nanos, ZoneOffset.UTC) + ProtobufTimestamp.toLocalDateTime(it) } return stopServersByGroup(request.groupName, request.timeoutSeconds, request.stopCause, sinceLocalDateTime) } override suspend fun stopServersByGroup(request: StopServersByGroupRequest): StopServersByGroupResponse { val sinceLocalDateTime = request.since?.let { - LocalDateTime.ofEpochSecond(it.seconds, it.nanos, ZoneOffset.UTC) + ProtobufTimestamp.toLocalDateTime(it) } return stopServersByGroup(request.groupName, null, request.stopCause, sinceLocalDateTime) } From baf599935d30e7c85438e34c6396e2505e8fd133 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Thu, 2 Jan 2025 18:52:14 +0100 Subject: [PATCH 60/65] feat: make envoy start port configurable --- .../controller/runtime/ControllerRuntime.kt | 2 +- .../droplet/ControllerDropletService.kt | 1 - .../runtime/droplet/DropletRepository.kt | 16 ++-- .../runtime/envoy/ControlPlaneServer.kt | 9 +++ .../controller/runtime/envoy/DropletCache.kt | 8 +- .../simplecloud/controller/runtime/hack/OS.kt | 19 +++++ .../runtime/hack/PortProcessHandle.kt | 81 +++++++++++++++++++ .../launcher/ControllerStartCommand.kt | 5 +- 8 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/OS.kt create mode 100644 controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/PortProcessHandle.kt diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt index 159aa85..e64364d 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/ControllerRuntime.kt @@ -43,7 +43,7 @@ class ControllerRuntime( private val serverRepository = ServerRepository(database, numericalIdRepository) private val hostRepository = ServerHostRepository() private val serverHostAttacher = ServerHostAttacher(hostRepository, serverRepository) - private val dropletRepository = DropletRepository(authCallCredentials, serverHostAttacher, hostRepository) + private val dropletRepository = DropletRepository(authCallCredentials, serverHostAttacher, controllerStartCommand.envoyStartPort, hostRepository) private val pubSubService = PubSubService() private val controlPlaneServer = ControlPlaneServer(controllerStartCommand, dropletRepository) private val authServer = OAuthServer(controllerStartCommand, database) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt index 118edea..4aead6f 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/ControllerDropletService.kt @@ -1,6 +1,5 @@ package app.simplecloud.controller.runtime.droplet -import app.simplecloud.controller.runtime.MetricsEventNames import app.simplecloud.droplet.api.droplet.Droplet import build.buf.gen.simplecloud.controller.v1.* import io.grpc.Status diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt index 9e7c06d..3e4da19 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/droplet/DropletRepository.kt @@ -2,6 +2,7 @@ package app.simplecloud.controller.runtime.droplet import app.simplecloud.controller.runtime.Repository import app.simplecloud.controller.runtime.envoy.DropletCache +import app.simplecloud.controller.runtime.hack.PortProcessHandle import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.runtime.server.ServerHostAttacher import app.simplecloud.controller.shared.host.ServerHost @@ -15,11 +16,12 @@ import kotlinx.coroutines.launch class DropletRepository( private val authCallCredentials: AuthCallCredentials, private val serverHostAttacher: ServerHostAttacher, + private val envoyStartPort: Int, private val serverHostRepository: ServerHostRepository, ) : Repository { private val currentDroplets = mutableListOf() - private val dropletCache = DropletCache(this) + private val dropletCache = DropletCache() override suspend fun getAll(): List { return currentDroplets @@ -33,8 +35,9 @@ class DropletRepository( return currentDroplets.firstOrNull { it.type == type && it.id == identifier } } + @Synchronized override fun save(element: Droplet) { - val updated = managePortRange(element) + val updated = managePortRange(element.copy(envoyPort = envoyStartPort)) val droplet = find(element.type, element.id) if (droplet != null) { currentDroplets[currentDroplets.indexOf(droplet)] = updated @@ -45,10 +48,11 @@ class DropletRepository( postUpdate(updated) } + @Synchronized private fun postUpdate(droplet: Droplet) { + dropletCache.update(currentDroplets) + if (droplet.type != "serverhost") return CoroutineScope(Dispatchers.IO).launch { - dropletCache.update() - if (droplet.type != "serverhost") return@launch serverHostAttacher.attach( ServerHost( droplet.id, droplet.host, droplet.port, ServerHostServiceGrpcKt.ServerHostServiceCoroutineStub( @@ -63,14 +67,14 @@ class DropletRepository( } private fun managePortRange(element: Droplet): Droplet { - if (!currentDroplets.any { it.envoyPort == element.envoyPort }) return element + if (!currentDroplets.any { it.envoyPort == element.envoyPort } && PortProcessHandle.of(element.envoyPort).isEmpty) return element return managePortRange(element.copy(envoyPort = element.envoyPort + 1)) } override suspend fun delete(element: Droplet): Boolean { val found = find(element.type, element.id) ?: return false if (!currentDroplets.remove(found)) return false - dropletCache.update() + dropletCache.update(currentDroplets) if (element.type == "serverhost") { val host = serverHostRepository.findServerHostById(element.id) ?: return true serverHostRepository.delete(host) diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt index 94a9d07..f95c183 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/ControlPlaneServer.kt @@ -43,6 +43,15 @@ class ControlPlaneServer(private val args: ControllerStartCommand, private val d envoyPort = 8080, ) ) + dropletRepository.save( + Droplet( + type = "auth", + id = "internal-auth", + host = args.grpcHost, + port = args.authorizationPort, + envoyPort = 8080 + ) + ) } private fun register(builder: ServerBuilder<*>) { diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt index b95e9e8..a9a9061 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/envoy/DropletCache.kt @@ -31,23 +31,21 @@ import java.util.* /** * This class handles the remapping of the [DropletRepository] to a [SimpleCache] of [Snapshot]s, which are used by the envoy ADS service. */ -class DropletCache(private val dropletRepository: DropletRepository) { +class DropletCache { private val cache = SimpleCache(SimpleCloudNodeGroup()) private val logger = LogManager.getLogger(DropletCache::class.java) //Create a new Snapshot by the droplet repository's data - suspend fun update() { + fun update(droplets: List) { logger.info("Detected new droplets in DropletRepository, adding to ADS...") val clusters = mutableListOf() val listeners = mutableListOf() val clas = mutableListOf() - - dropletRepository.getAll().forEach { + droplets.forEach { clusters.add(createCluster(it)) listeners.add(createListener(it)) clas.add(createCLA(it)) } - cache.setSnapshot( SimpleCloudNodeGroup.GROUP, Snapshot.create( diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/OS.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/OS.kt new file mode 100644 index 0000000..97aaec2 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/OS.kt @@ -0,0 +1,19 @@ +package app.simplecloud.controller.runtime.hack + +enum class OS(val names: List) { + WINDOWS(listOf("windows")), + LINUX(listOf("linux")), + MAC(listOf("mac")); + + companion object { + fun get(): OS? { + val name = System.getProperty("os.name").lowercase() + entries.forEach { + if (it.names.any { osName -> name.contains(osName) }) { + return it + } + } + return null + } + } +} diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/PortProcessHandle.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/PortProcessHandle.kt new file mode 100644 index 0000000..86c1c05 --- /dev/null +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/hack/PortProcessHandle.kt @@ -0,0 +1,81 @@ +package app.simplecloud.controller.runtime.hack + +import app.simplecloud.controller.shared.server.Server +import build.buf.gen.simplecloud.controller.v1.ServerDefinition +import java.time.LocalDateTime +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.regex.Pattern + +object PortProcessHandle { + + private const val WINDOWS_PID_INDEX = 3 + private val windowsPattern = Pattern.compile("\\s*TCP\\s+\\S+:(\\d+)\\s+\\S+:(\\d+)\\s+\\S+\\s+(\\d+)") + + private val preBindPorts = ConcurrentHashMap() + + fun of(port: Int): Optional { + val os = OS.get() ?: return Optional.empty() + val command = when (os) { + OS.LINUX, OS.MAC -> arrayOf("sh", "-c", "lsof -i :$port | awk '{print \$2}'") + OS.WINDOWS -> arrayOf("cmd", "/c", "netstat -ano | findstr $port") + } + + val process = Runtime.getRuntime().exec(command) + + val bufferedReader = process.inputReader() + val processId = bufferedReader.useLines { lines -> + lines.firstNotNullOfOrNull { parseProcessIdOrNull(os, it) } + } + + if (processId == null) { + return Optional.empty() + } + + return ProcessHandle.of(processId) + } + + private fun parseProcessIdOrNull(os: OS, line: String): Long? { + return when (os) { + OS.LINUX, OS.MAC -> line.toLongOrNull() + OS.WINDOWS -> { + val matcher = windowsPattern.matcher(line) + if (!matcher.matches()) { + return null + } + + return matcher.group(WINDOWS_PID_INDEX).toLongOrNull() + } + } + } + + @Synchronized + fun findNextFreePort(startPort: Int, serverDefinition: ServerDefinition): Int { + val server = Server.fromDefinition(serverDefinition) + var port = startPort + val time = LocalDateTime.now() + while (isPortBound(port)) { + port++ + } + addPreBind(port, time, server.properties.getOrDefault("max-startup-seconds", "120").toLong()) + return port + } + + private fun addPreBind(port: Int, time: LocalDateTime, duration: Long) { + preBindPorts[port] = time.plusSeconds(duration) + } + + fun isPortBound(port: Int): Boolean { + return !of(port).isEmpty || LocalDateTime.now().isBefore(preBindPorts.getOrDefault(port, LocalDateTime.MIN)) + } + + fun removePreBind(port: Int, forced: Boolean = false) { + // Check if max-startup-seconds is reached and return if not + if (!forced && LocalDateTime.now().isBefore(preBindPorts.getOrDefault(port, LocalDateTime.MAX))) { + return + } + + preBindPorts.remove(port) + } + +} \ No newline at end of file diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt index b972c8d..cf5c968 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt @@ -46,10 +46,13 @@ class ControllerStartCommand( ).int().default(5818) val envoyDiscoveryPort: Int by option( - help = "Authorization port (default: 5814)", + help = "Envoy Discovery port (default: 5814)", envvar = "ENVOY_DISCOVERY_PORT" ).int().default(5814) + val envoyStartPort: Int by option(help = "Envoy start port (default: 8080)", envvar = "ENVOY_START_PORT").int() + .default(8080) + private val authSecretPath: Path by option( help = "Path to auth secret file (default: .auth.secret)", envvar = "AUTH_SECRET_PATH" From 62095bdf1c4dc1c13d5eff421ec796c5ec712b23 Mon Sep 17 00:00:00 2001 From: PrexorJustin Date: Thu, 9 Jan 2025 18:15:36 +0100 Subject: [PATCH 61/65] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2cd34d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2025] [SimpleCloud] + + 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. From 1d4887d4e8be7fb4f4e153e56f21b1911b3f2292 Mon Sep 17 00:00:00 2001 From: Niklas <57962465+niklasnieberler@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:41:14 +0100 Subject: [PATCH 62/65] feat: add thisServer method in ServerApi --- .../app/simplecloud/controller/api/ServerApi.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt index 383ccbf..5c8e098 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt @@ -17,6 +17,13 @@ interface ServerApi { */ fun getAllServers(): CompletableFuture> + /** + * @return a [CompletableFuture] with the [Server] from the SIMPLECLOUD_UNIQUE_ID environment + */ + fun thisServer(): CompletableFuture { + return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) + } + /** * @param id the id of the server. * @return a [CompletableFuture] with the [Server]. @@ -124,6 +131,13 @@ interface ServerApi { */ suspend fun getAllServers(): List + /** + * @return the [Server] from the SIMPLECLOUD_UNIQUE_ID environment + */ + suspend fun thisServer(): Server { + return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) + } + /** * @param id the id of the server. * @return the [Server]. From 6ebee1bcfc7b41ddb87cbcc3c5e0dae328e0d21b Mon Sep 17 00:00:00 2001 From: Niklas <57962465+niklasnieberler@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:53:11 +0100 Subject: [PATCH 63/65] ref: rename thisServer to getThisServer in ServerApi --- .../main/kotlin/app/simplecloud/controller/api/ServerApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt index 5c8e098..703e357 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt @@ -20,7 +20,7 @@ interface ServerApi { /** * @return a [CompletableFuture] with the [Server] from the SIMPLECLOUD_UNIQUE_ID environment */ - fun thisServer(): CompletableFuture { + fun getThisServer(): CompletableFuture { return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) } @@ -134,7 +134,7 @@ interface ServerApi { /** * @return the [Server] from the SIMPLECLOUD_UNIQUE_ID environment */ - suspend fun thisServer(): Server { + suspend fun getThisServer(): Server { return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) } From 3e8cec222fb0fbb0ef0cc3a4011c0f0f23f7b50c Mon Sep 17 00:00:00 2001 From: Niklas <57962465+niklasnieberler@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:54:35 +0100 Subject: [PATCH 64/65] ref: rename thisServer to getCurrentServer in ServerApi --- .../main/kotlin/app/simplecloud/controller/api/ServerApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt index 703e357..0b31653 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt @@ -20,7 +20,7 @@ interface ServerApi { /** * @return a [CompletableFuture] with the [Server] from the SIMPLECLOUD_UNIQUE_ID environment */ - fun getThisServer(): CompletableFuture { + fun getCurrentServer(): CompletableFuture { return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) } @@ -134,7 +134,7 @@ interface ServerApi { /** * @return the [Server] from the SIMPLECLOUD_UNIQUE_ID environment */ - suspend fun getThisServer(): Server { + suspend fun getCurrentServer(): Server { return getServerById(System.getenv("SIMPLECLOUD_UNIQUE_ID")) } From aa5739d42ceed7b30ba3b722c0c37b1fe50ca310 Mon Sep 17 00:00:00 2001 From: dayeeet Date: Sat, 18 Jan 2025 17:29:34 +0100 Subject: [PATCH 65/65] fix: relocate all libraries --- controller-api/build.gradle.kts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/controller-api/build.gradle.kts b/controller-api/build.gradle.kts index de26167..ecc433b 100644 --- a/controller-api/build.gradle.kts +++ b/controller-api/build.gradle.kts @@ -1,3 +1,17 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + dependencies { api(project(":controller-shared")) +} + +tasks.named("shadowJar", ShadowJar::class) { + mergeServiceFiles() + relocate("com", "app.simplecloud.external.com") + relocate("google", "app.simplecloud.external.google") + relocate("io", "app.simplecloud.external.io") + relocate("org", "app.simplecloud.external.org") + relocate("javax", "app.simplecloud.external.javax") + relocate("android", "app.simplecloud.external.android") + relocate("build.buf.gen.simplecloud", "app.simplecloud.buf") + archiveFileName.set("${project.name}.jar") } \ No newline at end of file