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]
+