diff --git a/.gitignore b/.gitignore
index b63da45..973dab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,6 @@ bin/
.vscode/
### Mac OS ###
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+run/
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..51024d5
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..dcc2d5f
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index eab14b2..b2febd7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,27 +4,43 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.kotlin)
alias(libs.plugins.shadow)
+ alias(libs.plugins.sonatypeCentralPortalPublisher)
+ `maven-publish`
}
allprojects {
-
group = "app.simplecloud.controller"
- version = "1.0-SNAPSHOT"
+ version = "0.0.27-EXPERIMENTAL"
repositories {
mavenCentral()
+ maven("https://buf.build/gen/maven")
}
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "com.github.johnrengelman.shadow")
+ apply(plugin = "net.thebugmc.gradle.sonatype-central-portal-publisher")
+ apply(plugin = "maven-publish")
dependencies {
testImplementation(rootProject.libs.kotlinTest)
implementation(rootProject.libs.kotlinJvm)
}
+ publishing {
+ publications {
+ create("mavenJava") {
+ from(components["java"])
+ }
+ }
+ }
+
+ java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
kotlin {
jvmToolchain(17)
}
@@ -41,4 +57,43 @@ subprojects {
tasks.test {
useJUnitPlatform()
}
+
+ centralPortal {
+ name = project.name
+
+ username = project.findProperty("sonatypeUsername") as String
+ password = project.findProperty("sonatypePassword") as String
+
+ pom {
+ name.set("SimpleCloud controller")
+ description.set("The heart of SimpleCloud v3")
+ url.set("https://github.com/theSimpleCloud/simplecloud-controller")
+
+ developers {
+ developer {
+ id.set("fllipeis")
+ email.set("p.eistrach@gmail.com")
+ }
+ developer {
+ id.set("dayyeeet")
+ email.set("david@cappell.net")
+ }
+ }
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+ scm {
+ url.set("https://github.com/theSimpleCloud/simplecloud-controller.git")
+ connection.set("git:git@github.com:theSimpleCloud/simplecloud-controller.git")
+ }
+ }
+ }
+
+ signing {
+ sign(publishing.publications)
+ useGpgCmd()
+ }
}
\ No newline at end of file
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApi.kt
index ba58985..d1a9f91 100644
--- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApi.kt
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApi.kt
@@ -1,15 +1,41 @@
package app.simplecloud.controller.api
-import app.simplecloud.controller.shared.group.Group
-import java.util.concurrent.CompletableFuture
+import app.simplecloud.controller.api.impl.ControllerApiImpl
interface ControllerApi {
/**
- * NOTE: This may be moved to a separate api file.
- * @param name the name of the group.
- * @returns a [CompletableFuture] with the [Group].
+ * @return the Controller [GroupApi]
*/
- fun getGroupByName(name: String): CompletableFuture
+ fun getGroups(): GroupApi
+
+ /**
+ * @return the Controller [ServerApi]
+ */
+ fun getServers(): ServerApi
+
+ companion object {
+
+ /**
+ * Creates a new [ControllerApi] instance
+ * @return the created [ControllerApi]
+ */
+ @JvmStatic
+ fun create(): ControllerApi {
+ val authSecret = System.getenv("CONTROLLER_SECRET")
+ return create(authSecret)
+ }
+
+ /**
+ * Creates a new [ControllerApi] instance
+ * @param authSecret the authentication key used by the Controller
+ * @return the created [ControllerApi]
+ */
+ @JvmStatic
+ fun create(authSecret: String): ControllerApi {
+ return ControllerApiImpl(authSecret)
+ }
+
+ }
}
\ No newline at end of file
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApiSingleton.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApiSingleton.kt
deleted file mode 100644
index 9f141c6..0000000
--- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ControllerApiSingleton.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package app.simplecloud.controller.api
-
-import app.simplecloud.controller.api.impl.ControllerApiImpl
-
-class ControllerApiSingleton {
- companion object {
- @JvmStatic
- lateinit var instance: ControllerApi
- private set
-
- fun init() = init(ControllerApiImpl())
-
- fun init(controllerApi: ControllerApi) {
- instance = controllerApi
- }
- }
-}
\ No newline at end of file
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/GroupApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/GroupApi.kt
new file mode 100644
index 0000000..8289c1f
--- /dev/null
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/GroupApi.kt
@@ -0,0 +1,43 @@
+package app.simplecloud.controller.api
+
+import app.simplecloud.controller.shared.group.Group
+import build.buf.gen.simplecloud.controller.v1.ServerType
+import java.util.concurrent.CompletableFuture
+
+interface GroupApi {
+
+ /**
+ * @param name the name of the group.
+ * @return a [CompletableFuture] with the [Group].
+ */
+ fun getGroupByName(name: String): CompletableFuture
+
+ /**
+ * @param name the name of the group.
+ * @return the deleted [Group].
+ */
+ fun deleteGroup(name: String): CompletableFuture
+
+ /**
+ * @param group the [Group] to create.
+ * @return the created [Group].
+ */
+ fun createGroup(group: Group): CompletableFuture
+
+ /**
+ * @param group the [Group] to update.
+ * @return the updated [Group].
+ */
+ fun updateGroup(group: Group): CompletableFuture
+ /**
+ * @return a [CompletableFuture] with a list of all groups.
+ */
+ fun getAllGroups(): CompletableFuture>
+
+ /**
+ * @param type the [ServerType] of the group
+ * @return a [CompletableFuture] with a list of all groups matching this type.
+ */
+ fun getGroupsByType(type: ServerType): CompletableFuture>
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..af0bb38
--- /dev/null
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt
@@ -0,0 +1,74 @@
+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.ServerState
+import java.util.concurrent.CompletableFuture
+
+interface ServerApi {
+
+ /**
+ * @return a [CompletableFuture] with a [List] of all [Server]s
+ */
+ fun getAllServers(): CompletableFuture>
+
+ /**
+ * @param id the id of the server.
+ * @return a [CompletableFuture] with the [Server].
+ */
+ fun getServerById(id: String): CompletableFuture
+
+ /**
+ * @param groupName the name of the server group.
+ * @return a [CompletableFuture] with a [List] of [Server]s of that group.
+ */
+ fun getServersByGroup(groupName: String): CompletableFuture>
+
+ /**
+ * @param group The server group.
+ * @return a [CompletableFuture] with a [List] of [Server]s of that group.
+ */
+ fun getServersByGroup(group: Group): CompletableFuture>
+
+ /**
+ * @param type The servers type
+ * @return a [CompletableFuture] with a [List] of all [Server]s with this type
+ */
+ fun getServersByType(type: ServerType): CompletableFuture>
+
+ /**
+ * @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): 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): CompletableFuture
+
+ /**
+ * @param id the id of the server.
+ * @return a [CompletableFuture] with the stopped [Server].
+ */
+ fun stopServer(id: String): CompletableFuture
+
+ /**
+ * @param id the id of the server.
+ * @param state the new state of the server.
+ * @return a [CompletableFuture] with the updated [Server].
+ */
+ fun updateServerState(id: String, state: ServerState): CompletableFuture
+
+ /**
+ * @param id the id of the server.
+ * @param key the server property key
+ * @param value the new property value
+ * @return a [CompletableFuture] with the updated [Server].
+ */
+ fun updateServerProperty(id: String, key: String, value: Any): CompletableFuture
+
+}
\ No newline at end of file
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ControllerApiImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ControllerApiImpl.kt
index e7b7ff3..2550e78 100644
--- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ControllerApiImpl.kt
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ControllerApiImpl.kt
@@ -1,30 +1,34 @@
package app.simplecloud.controller.api.impl
import app.simplecloud.controller.api.ControllerApi
-import app.simplecloud.controller.shared.future.toCompletable
-import app.simplecloud.controller.shared.group.Group
-import app.simplecloud.controller.shared.proto.ControllerGroupServiceGrpc
-import app.simplecloud.controller.shared.proto.GetGroupByNameRequest
+import app.simplecloud.controller.api.GroupApi
+import app.simplecloud.controller.api.ServerApi
+import app.simplecloud.controller.shared.auth.AuthCallCredentials
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
-import java.util.concurrent.CompletableFuture
-class ControllerApiImpl : ControllerApi {
+class ControllerApiImpl(
+ authSecret: String
+): ControllerApi {
+
+ private val authCallCredentials = AuthCallCredentials(authSecret)
private val managedChannel = createManagedChannelFromEnv()
+ private val groups: GroupApi = GroupApiImpl(managedChannel, authCallCredentials)
+ private val servers: ServerApi = ServerApiImpl(managedChannel, authCallCredentials)
+
+ /**
+ * @return The controllers [GroupApi]
+ */
+ override fun getGroups(): GroupApi {
+ return groups
+ }
- protected val groupServiceStub: ControllerGroupServiceGrpc.ControllerGroupServiceFutureStub =
- ControllerGroupServiceGrpc.newFutureStub(managedChannel)
-
- override fun getGroupByName(name: String): CompletableFuture {
- return groupServiceStub.getGroupByName(
- GetGroupByNameRequest.newBuilder()
- .setName(name)
- .build()
- ).toCompletable()
- .thenApply {
- Group.fromDefinition(it.group)
- }
+ /**
+ * @return The controllers [ServerApi]
+ */
+ override fun getServers(): ServerApi {
+ return servers
}
private fun createManagedChannelFromEnv(): ManagedChannel {
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/GroupApiImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/GroupApiImpl.kt
new file mode 100644
index 0000000..25b4fc6
--- /dev/null
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/GroupApiImpl.kt
@@ -0,0 +1,73 @@
+package app.simplecloud.controller.api.impl
+
+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 build.buf.gen.simplecloud.controller.v1.ControllerGroupServiceGrpc
+import build.buf.gen.simplecloud.controller.v1.GetGroupByNameRequest
+import build.buf.gen.simplecloud.controller.v1.GetAllGroupsRequest
+import build.buf.gen.simplecloud.controller.v1.GetGroupsByTypeRequest
+import build.buf.gen.simplecloud.controller.v1.ServerType
+import io.grpc.ManagedChannel
+import java.util.concurrent.CompletableFuture
+
+class GroupApiImpl(
+ managedChannel: ManagedChannel,
+ authCallCredentials: AuthCallCredentials
+) : GroupApi {
+
+ private val groupServiceStub: ControllerGroupServiceGrpc.ControllerGroupServiceFutureStub =
+ ControllerGroupServiceGrpc.newFutureStub(managedChannel)
+ .withCallCredentials(authCallCredentials)
+
+ override fun getGroupByName(name: String): CompletableFuture {
+ return groupServiceStub.getGroupByName(
+ GetGroupByNameRequest.newBuilder()
+ .setName(name)
+ .build()
+ ).toCompletable()
+ .thenApply {
+ Group.fromDefinition(it.group)
+ }
+ }
+
+ override fun deleteGroup(name: String): CompletableFuture {
+ return groupServiceStub.deleteGroupByName(
+ GetGroupByNameRequest.newBuilder()
+ .setName(name)
+ .build()
+ ).toCompletable()
+ .thenApply {
+ Group.fromDefinition(it)
+ }
+ }
+
+ override fun createGroup(group: Group): CompletableFuture {
+ return groupServiceStub.createGroup(
+ group.toDefinition()
+ ).toCompletable()
+ .thenApply {
+ Group.fromDefinition(it)
+ }
+ }
+
+ override fun updateGroup(group: Group): CompletableFuture {
+ return groupServiceStub.updateGroup(group.toDefinition()).toCompletable().thenApply {
+ return@thenApply Group.fromDefinition(it)
+ }
+ }
+
+ override fun getAllGroups(): CompletableFuture> {
+ return groupServiceStub.getAllGroups(GetAllGroupsRequest.newBuilder().build()).toCompletable().thenApply {
+ return@thenApply it.groupsList.map { group -> Group.fromDefinition(group) }
+ }
+ }
+
+ override fun getGroupsByType(type: ServerType): CompletableFuture> {
+ return groupServiceStub.getGroupsByType(GetGroupsByTypeRequest.newBuilder().setType(type).build()).toCompletable().thenApply {
+ return@thenApply it.groupsList.map { group -> Group.fromDefinition(group) }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ServerApiImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ServerApiImpl.kt
new file mode 100644
index 0000000..189429c
--- /dev/null
+++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/ServerApiImpl.kt
@@ -0,0 +1,113 @@
+package app.simplecloud.controller.api.impl
+
+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 io.grpc.ManagedChannel
+import java.util.concurrent.CompletableFuture
+
+class ServerApiImpl(
+ managedChannel: ManagedChannel,
+ authCallCredentials: AuthCallCredentials
+) : ServerApi {
+
+ private val serverServiceStub: ControllerServerServiceGrpc.ControllerServerServiceFutureStub =
+ ControllerServerServiceGrpc.newFutureStub(managedChannel).withCallCredentials(authCallCredentials)
+
+ override fun getAllServers(): CompletableFuture> {
+ return serverServiceStub.getAllServers(GetAllServersRequest.newBuilder().build()).toCompletable().thenApply {
+ Server.fromDefinition(it.serversList)
+ }
+ }
+
+ override fun getServerById(id: String): CompletableFuture {
+ return serverServiceStub.getServerById(
+ ServerIdRequest.newBuilder()
+ .setId(id)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it)
+ }
+ }
+
+ override fun getServersByGroup(groupName: String): CompletableFuture> {
+ return serverServiceStub.getServersByGroup(
+ GroupNameRequest.newBuilder()
+ .setName(groupName)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it.serversList)
+ }
+ }
+
+ override fun getServersByGroup(group: Group): CompletableFuture> {
+ return getServersByGroup(group.name)
+ }
+
+ override fun getServersByType(type: ServerType): CompletableFuture> {
+ return serverServiceStub.getServersByType(
+ ServerTypeRequest.newBuilder()
+ .setType(type)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it.serversList)
+ }
+ }
+
+ override fun startServer(groupName: String): CompletableFuture {
+ return serverServiceStub.startServer(
+ GroupNameRequest.newBuilder()
+ .setName(groupName)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it)
+ }
+ }
+
+ override fun stopServer(groupName: String, numericalId: Long): CompletableFuture {
+ return serverServiceStub.stopServerByNumerical(
+ StopServerByNumericalRequest.newBuilder()
+ .setGroup(groupName)
+ .setNumericalId(numericalId)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it)
+ }
+ }
+
+ override fun stopServer(id: String): CompletableFuture {
+ return serverServiceStub.stopServer(
+ ServerIdRequest.newBuilder()
+ .setId(id)
+ .build()
+ ).toCompletable().thenApply {
+ Server.fromDefinition(it)
+ }
+ }
+
+ override fun updateServerState(id: String, state: ServerState): CompletableFuture {
+ return serverServiceStub.updateServerState(
+ ServerUpdateStateRequest.newBuilder()
+ .setState(state)
+ .setId(id)
+ .build()
+ ).toCompletable().thenApply {
+ return@thenApply Server.fromDefinition(it)
+ }
+ }
+
+ override fun updateServerProperty(id: String, key: String, value: Any): CompletableFuture {
+ return serverServiceStub.updateServerProperty(
+ ServerUpdatePropertyRequest.newBuilder()
+ .setKey(key)
+ .setValue(value.toString())
+ .setId(id)
+ .build()
+ ).toCompletable().thenApply {
+ return@thenApply Server.fromDefinition(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/controller-runtime/build.gradle.kts b/controller-runtime/build.gradle.kts
index 32e3518..7b4bdb5 100644
--- a/controller-runtime/build.gradle.kts
+++ b/controller-runtime/build.gradle.kts
@@ -1,12 +1,97 @@
plugins {
application
+ alias(libs.plugins.jooqCodegen)
}
dependencies {
api(project(":controller-shared"))
+ api(rootProject.libs.kotlinCoroutines)
+ api(rootProject.libs.bundles.jooq)
+ api(rootProject.libs.sqliteJdbc)
+ jooqCodegen(rootProject.libs.jooqMetaExtensions)
implementation(rootProject.libs.bundles.log4j)
+ implementation(rootProject.libs.clikt)
+ implementation(rootProject.libs.spotifyCompletableFutures)
}
application {
mainClass.set("app.simplecloud.controller.runtime.launcher.LauncherKt")
+}
+
+sourceSets {
+ main {
+ java {
+ srcDirs(
+ "build/generated/source/db/main/java",
+ )
+ }
+ resources {
+ srcDirs(
+ "src/main/db"
+ )
+ }
+ }
+}
+
+tasks.named("compileKotlin") {
+ dependsOn(tasks.jooqCodegen)
+}
+
+jooq {
+ configuration {
+ generator {
+ target {
+ directory = "build/generated/source/db/main/java"
+ packageName = "app.simplecloud.controller.shared.db"
+ }
+ database {
+ name = "org.jooq.meta.extensions.ddl.DDLDatabase"
+ properties {
+ // Specify the location of your SQL script.
+ // You may use ant-style file matching, e.g. /path/**/to/*.sql
+ //
+ // Where:
+ // - ** matches any directory subtree
+ // - * matches any number of characters in a directory / file name
+ // - ? matches a single character in a directory / file name
+ property {
+ key = "scripts"
+ value = "src/main/db/schema.sql"
+ }
+
+ // The sort order of the scripts within a directory, where:
+ //
+ // - semantic: sorts versions, e.g. v-3.10.0 is after v-3.9.0 (default)
+ // - alphanumeric: sorts strings, e.g. v-3.10.0 is before v-3.9.0
+ // - flyway: sorts files the same way as flyway does
+ // - none: doesn't sort directory contents after fetching them from the directory
+ property {
+ key = "sort"
+ value = "semantic"
+ }
+
+ // The default schema for unqualified objects:
+ //
+ // - public: all unqualified objects are located in the PUBLIC (upper case) schema
+ // - none: all unqualified objects are located in the default schema (default)
+ //
+ // This configuration can be overridden with the schema mapping feature
+ property {
+ key = "unqualifiedSchema"
+ value = "none"
+ }
+
+ // The default name case for unquoted objects:
+ //
+ // - as_is: unquoted object names are kept unquoted
+ // - upper: unquoted object names are turned into upper case (most databases)
+ // - lower: unquoted object names are turned into lower case (e.g. PostgreSQL)
+ property {
+ key = "defaultNameCase"
+ value = "lower"
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/controller-runtime/src/main/db/schema.sql b/controller-runtime/src/main/db/schema.sql
new file mode 100644
index 0000000..d779577
--- /dev/null
+++ b/controller-runtime/src/main/db/schema.sql
@@ -0,0 +1,28 @@
+/**
+ This file represents the sql schema of the v3 backend.
+ 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_server_properties(
+ server_id varchar NOT NULL,
+ key varchar NOT NULL,
+ value varchar,
+ CONSTRAINT compound_key PRIMARY KEY (server_id, key)
+);
\ No newline at end of file
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 b76455e..c5abe3e 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,26 +1,70 @@
package app.simplecloud.controller.runtime
+import app.simplecloud.controller.runtime.database.DatabaseFactory
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.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 io.grpc.ManagedChannel
+import io.grpc.ManagedChannelBuilder
import io.grpc.Server
import io.grpc.ServerBuilder
+import kotlinx.coroutines.*
import org.apache.logging.log4j.LogManager
import kotlin.concurrent.thread
-class ControllerRuntime {
+class ControllerRuntime(
+ private val controllerStartCommand: ControllerStartCommand
+) {
private val logger = LogManager.getLogger(ControllerRuntime::class.java)
+ private val database = DatabaseFactory.createDatabase(controllerStartCommand.databaseUrl)
+ private val authCallCredentials = AuthCallCredentials(controllerStartCommand.authSecret)
- private val groupRepository = GroupRepository()
-
- private val server = createGrpcServerFromEnv()
+ private val groupRepository = GroupRepository(controllerStartCommand.groupPath)
+ private val numericalIdRepository = ServerNumericalIdRepository()
+ private val serverRepository = ServerRepository(database, numericalIdRepository)
+ private val hostRepository = ServerHostRepository()
+ private val reconciler = Reconciler(
+ groupRepository,
+ serverRepository,
+ hostRepository,
+ numericalIdRepository,
+ createManagedChannel(),
+ authCallCredentials
+ )
+ private val server = createGrpcServer()
fun start() {
- logger.info("Starting controller...")
+ setupDatabase()
startGrpcServer()
+ startReconciler()
+ loadGroups()
+ loadServers()
+ }
+
+ private fun setupDatabase() {
+ logger.info("Setting up database...")
+ database.setup()
+ }
+
+ private fun loadServers() {
+ logger.info("Loading servers...")
+ val loadedServers = serverRepository.load()
+ if (loadedServers.isEmpty()) {
+ return
+ }
+
+ logger.info("Loaded servers from cache: ${loadedServers.joinToString { "${it.group}-${it.numericalId}" }}")
}
- fun startGrpcServer() {
+ private fun startGrpcServer() {
logger.info("Starting gRPC server...")
thread {
server.start()
@@ -28,13 +72,52 @@ class ControllerRuntime {
}
}
- private fun createGrpcServerFromEnv(): Server {
- val port = System.getenv("GRPC_PORT")?.toInt() ?: 5816
- return ServerBuilder.forPort(port)
+ private fun startReconciler() {
+ logger.info("Starting Reconciler...")
+ startReconcilerJob()
+ }
+
+ private fun loadGroups() {
+ logger.info("Loading groups...")
+ val loadedGroups = groupRepository.load()
+ if (loadedGroups.isEmpty()) {
+ logger.warn("No groups found.")
+ return
+ }
+
+ logger.info("Loaded groups: ${loadedGroups.joinToString { it.name }}")
+ }
+
+ private fun createGrpcServer(): Server {
+ return ServerBuilder.forPort(controllerStartCommand.grpcPort)
+ .addService(GroupService(groupRepository))
.addService(
- GroupService(groupRepository),
+ ServerService(
+ numericalIdRepository,
+ serverRepository,
+ hostRepository,
+ groupRepository,
+ controllerStartCommand.forwardingSecret,
+ authCallCredentials
+ )
)
+ .intercept(AuthSecretInterceptor(controllerStartCommand.authSecret))
.build()
}
+ private fun createManagedChannel(): ManagedChannel {
+ return ManagedChannelBuilder.forAddress(controllerStartCommand.grpcHost, controllerStartCommand.grpcPort)
+ .usePlaintext()
+ .build()
+ }
+
+ private fun startReconcilerJob(): Job {
+ return CoroutineScope(Dispatchers.Default).launch {
+ while (isActive) {
+ reconciler.reconcile()
+ delay(2000L)
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/LoadableRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/LoadableRepository.kt
new file mode 100644
index 0000000..64de502
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/LoadableRepository.kt
@@ -0,0 +1,5 @@
+package app.simplecloud.controller.runtime
+
+interface LoadableRepository : Repository {
+ fun load(): List
+}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/Repository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/Repository.kt
new file mode 100644
index 0000000..1785fa2
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/Repository.kt
@@ -0,0 +1,11 @@
+package app.simplecloud.controller.runtime
+
+import java.util.concurrent.CompletableFuture
+
+
+interface Repository {
+ fun delete(element: E): CompletableFuture
+ fun save(element: E)
+ fun find(identifier: I): CompletableFuture
+ fun getAll(): CompletableFuture>
+}
\ 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
new file mode 100644
index 0000000..34e627c
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/YamlDirectoryRepository.kt
@@ -0,0 +1,142 @@
+package app.simplecloud.controller.runtime
+
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.LogManager
+import org.spongepowered.configurate.ConfigurationOptions
+import org.spongepowered.configurate.kotlin.objectMapperFactory
+import org.spongepowered.configurate.loader.ParsingException
+import org.spongepowered.configurate.yaml.NodeStyle
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader
+import java.io.File
+import java.nio.file.*
+import java.util.concurrent.CompletableFuture
+
+
+abstract class YamlDirectoryRepository(
+ private val directory: Path,
+ private val clazz: Class,
+) : LoadableRepository {
+
+ private val logger = LogManager.getLogger(this::class.java)
+
+ private val watchService = FileSystems.getDefault().newWatchService()
+ private val loaders = mutableMapOf()
+ protected val entities = mutableMapOf()
+
+ abstract fun getFileName(identifier: I): String
+
+ override fun delete(element: E): CompletableFuture {
+ val file = entities.keys.find { entities[it] == element } ?: return CompletableFuture.completedFuture(false)
+ return CompletableFuture.completedFuture(deleteFile(file))
+ }
+
+ override fun getAll(): CompletableFuture> {
+ return CompletableFuture.completedFuture(entities.values.toList())
+ }
+
+ override fun load(): List {
+ if (!directory.toFile().exists()) {
+ directory.toFile().mkdirs()
+ }
+
+ registerWatcher()
+
+ return Files.list(directory)
+ .toList()
+ .filter { !it.toFile().isDirectory && it.toString().endsWith(".yml") }
+ .mapNotNull { load(it.toFile()) }
+ }
+
+ private fun load(file: File): E? {
+ try {
+ val loader = getOrCreateLoader(file)
+ val node = loader.load(ConfigurationOptions.defaults())
+ val entity = node.get(clazz) ?: return null
+ entities[file] = entity
+ return entity
+ } catch (ex: ParsingException) {
+ val existedBefore = entities.containsKey(file)
+ if (existedBefore) {
+ logger.error("Could not load file ${file.name}. Switching back to an older version.")
+ return null
+ }
+
+ logger.error("Could not load file ${file.name}. Make sure it's correctly formatted!")
+ return null
+ }
+ }
+
+ private fun deleteFile(file: File): Boolean {
+ val deletedSuccessfully = file.delete()
+ val removedSuccessfully = entities.remove(file) != null
+ return deletedSuccessfully && removedSuccessfully
+ }
+
+ protected fun save(fileName: String, entity: E) {
+ val file = directory.resolve(fileName).toFile()
+ val loader = getOrCreateLoader(file)
+ val node = loader.createNode(ConfigurationOptions.defaults())
+ node.set(clazz, entity)
+ loader.save(node)
+ entities[file] = entity
+ }
+
+ private fun getOrCreateLoader(file: File): YamlConfigurationLoader {
+ return loaders.getOrPut(file) {
+ YamlConfigurationLoader.builder()
+ .path(file.toPath())
+ .nodeStyle(NodeStyle.BLOCK)
+ .defaultOptions { options ->
+ options.serializers { builder ->
+ builder.registerAnnotatedObjects(objectMapperFactory())
+ }
+ }.build()
+ }
+ }
+
+ private fun registerWatcher(): Job {
+ directory.register(
+ watchService,
+ StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_DELETE,
+ StandardWatchEventKinds.ENTRY_MODIFY
+ )
+
+ return CoroutineScope(Dispatchers.Default).launch {
+ while (isActive) {
+ val key = watchService.take()
+ for (event in key.pollEvents()) {
+ val path = event.context() as? Path ?: continue
+ val resolvedPath = directory.resolve(path)
+ if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) {
+ continue
+ }
+ 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_DELETE -> {
+ deleteFile(resolvedPath.toFile())
+ }
+ }
+ }
+ key.reset()
+ }
+ }
+ }
+
+ private fun getChangeStatus(kind: WatchEvent.Kind<*>): String {
+ return when (kind) {
+ StandardWatchEventKinds.ENTRY_CREATE -> "Created"
+ StandardWatchEventKinds.ENTRY_DELETE -> "Deleted"
+ StandardWatchEventKinds.ENTRY_MODIFY -> "Modified"
+ else -> "Unknown"
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/Database.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/Database.kt
new file mode 100644
index 0000000..99266b9
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/Database.kt
@@ -0,0 +1,22 @@
+package app.simplecloud.controller.runtime.database
+
+import org.jooq.DSLContext
+
+class Database(
+ val context: DSLContext
+) {
+
+ fun setup() {
+ System.setProperty("org.jooq.no-logo", "true")
+ System.setProperty("org.jooq.no-tips", "true")
+ val setupInputStream = Database::class.java.getResourceAsStream("/schema.sql")
+ ?: throw IllegalArgumentException("Database schema not found.")
+ val setupCommands = setupInputStream.bufferedReader().use { it.readText() }.split(";")
+ setupCommands.forEach {
+ val trimmed = it.trim()
+ if (trimmed.isNotEmpty())
+ context.execute(org.jooq.impl.DSL.sql(trimmed))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/DatabaseFactory.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/DatabaseFactory.kt
new file mode 100644
index 0000000..d7bc914
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/database/DatabaseFactory.kt
@@ -0,0 +1,12 @@
+package app.simplecloud.controller.runtime.database
+
+import org.jooq.impl.DSL
+
+object DatabaseFactory {
+
+ fun createDatabase(databaseUrl: String): Database {
+ val databaseContext = DSL.using(databaseUrl)
+ return Database(databaseContext)
+ }
+
+}
\ 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 c96acf9..73669fe 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,15 +1,26 @@
package app.simplecloud.controller.runtime.group
+import app.simplecloud.controller.runtime.YamlDirectoryRepository
import app.simplecloud.controller.shared.group.Group
-import app.simplecloud.controller.shared.proto.GroupDefinition
+import java.nio.file.Path
+import java.util.concurrent.CompletableFuture
-class GroupRepository {
+class GroupRepository(
+ path: Path
+) : YamlDirectoryRepository(path, Group::class.java) {
+ override fun getFileName(identifier: String): String {
+ return "$identifier.yml"
+ }
- // TODO: Load groups from file
- private val groups = listOf()
+ override fun find(identifier: String): CompletableFuture {
+ return CompletableFuture.completedFuture(entities.values.find { it.name == identifier })
+ }
- fun findGroupByName(name: String): GroupDefinition? {
- return groups.firstOrNull { it.name == name }?.toDefinition()
+ override fun save(element: Group) {
+ save(getFileName(element.name), element)
}
+ override fun getAll(): CompletableFuture> {
+ return CompletableFuture.completedFuture(entities.values.toList())
+ }
}
\ No newline at end of file
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 d8cdef7..5fd259a 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
@@ -1,8 +1,8 @@
package app.simplecloud.controller.runtime.group
-import app.simplecloud.controller.shared.proto.ControllerGroupServiceGrpc
-import app.simplecloud.controller.shared.proto.GetGroupByNameRequest
-import app.simplecloud.controller.shared.proto.GetGroupByNameResponse
+import app.simplecloud.controller.shared.group.Group
+import build.buf.gen.simplecloud.controller.v1.*
+import io.grpc.Status
import io.grpc.stub.StreamObserver
class GroupService(
@@ -13,13 +13,120 @@ class GroupService(
request: GetGroupByNameRequest,
responseObserver: StreamObserver
) {
- val group = groupRepository.findGroupByName(request.name)
- val response = GetGroupByNameResponse.newBuilder()
- .setGroup(group)
- .build()
+ groupRepository.find(request.name).thenApply { group ->
+ if (group == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("This group does not exist")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
- responseObserver.onNext(response)
+ val response = GetGroupByNameResponse.newBuilder()
+ .setGroup(group.toDefinition())
+ .build()
+
+ responseObserver.onNext(response)
+ responseObserver.onCompleted()
+ }
+
+ }
+
+ override fun getAllGroups(request: GetAllGroupsRequest, responseObserver: StreamObserver) {
+ groupRepository.getAll().thenApply { groups ->
+ val response = GetAllGroupsResponse.newBuilder()
+ .addAllGroups(groups.map { it.toDefinition() })
+ .build()
+ responseObserver.onNext(response)
+ responseObserver.onCompleted()
+ }
+
+ }
+
+ override fun getGroupsByType(
+ request: GetGroupsByTypeRequest,
+ responseObserver: StreamObserver
+ ) {
+ val type = request.type
+ groupRepository.getAll().thenApply { groups ->
+ val response = GetGroupsByTypeResponse.newBuilder()
+ .addAllGroups(groups.filter { it.type == type }.map { it.toDefinition() })
+ .build()
+ responseObserver.onNext(response)
+ responseObserver.onCompleted()
+ }
+
+ }
+
+ override fun updateGroup(request: GroupDefinition, responseObserver: StreamObserver) {
+ val group = Group.fromDefinition(request)
+ try {
+ groupRepository.save(group)
+ } catch (e: Exception) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Error whilst updating group")
+ .withCause(e)
+ .asRuntimeException()
+ )
+ return
+ }
+
+ responseObserver.onNext(group.toDefinition())
+ responseObserver.onCompleted()
+ }
+
+ override fun createGroup(request: GroupDefinition, responseObserver: StreamObserver) {
+ val group = Group.fromDefinition(request)
+ try {
+ groupRepository.save(group)
+ } catch (e: Exception) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Error whilst creating group")
+ .withCause(e)
+ .asRuntimeException()
+ )
+ return
+ }
+
+ responseObserver.onNext(group.toDefinition())
responseObserver.onCompleted()
}
+ override fun deleteGroupByName(request: GetGroupByNameRequest, responseObserver: StreamObserver) {
+ groupRepository.find(request.name).thenApply { group ->
+ if (group == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("This group does not exist")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
+ groupRepository.delete(group).thenApply thenDelete@ { successfullyDeleted ->
+ if(!successfullyDeleted) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not delete group")
+ .asRuntimeException()
+ )
+
+ return@thenDelete
+ }
+ responseObserver.onNext(group.toDefinition())
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not delete group")
+ .withCause(it)
+ .asRuntimeException()
+ )
+ }
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostException.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostException.kt
new file mode 100644
index 0000000..e4651e1
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostException.kt
@@ -0,0 +1,3 @@
+package app.simplecloud.controller.runtime.host
+
+class ServerHostException(message: String) : Exception(message)
\ No newline at end of file
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
new file mode 100644
index 0000000..14ddb8d
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostRepository.kt
@@ -0,0 +1,60 @@
+package app.simplecloud.controller.runtime.host
+
+import app.simplecloud.controller.runtime.Repository
+import app.simplecloud.controller.runtime.server.ServerRepository
+import app.simplecloud.controller.shared.host.ServerHost
+import com.spotify.futures.CompletableFutures
+import io.grpc.ConnectivityState
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ConcurrentHashMap
+
+class ServerHostRepository : Repository {
+
+ private val hosts: ConcurrentHashMap = ConcurrentHashMap()
+
+ fun findServerHostById(id: String): ServerHost? {
+ return hosts.getOrDefault(hosts.keys.firstOrNull { it == id }, null)
+ }
+
+ override fun save(element: ServerHost) {
+ hosts[element.id] = element
+ }
+
+ override fun find(identifier: ServerRepository): CompletableFuture {
+ return mapHostsToServerHostWithServerCount(identifier).thenApply {
+ val serverHostWithServerCount = it.minByOrNull { it.serverCount }
+ serverHostWithServerCount?.serverHost
+ }
+ }
+
+ fun areServerHostsAvailable(): CompletableFuture {
+ return CompletableFuture.supplyAsync {
+ hosts.any {
+ val channel = it.value.createChannel()
+ val state = channel.getState(true)
+ channel.shutdown()
+ state == ConnectivityState.IDLE || state == ConnectivityState.READY
+ }
+ }
+ }
+
+ override fun delete(element: ServerHost): CompletableFuture {
+ return CompletableFuture.completedFuture(hosts.remove(element.id, element))
+ }
+
+ override fun getAll(): CompletableFuture> {
+ return CompletableFuture.completedFuture(hosts.values.toList())
+ }
+
+ private fun mapHostsToServerHostWithServerCount(identifier: ServerRepository): CompletableFuture> {
+ return CompletableFutures.allAsList(
+ hosts.values.map { serverHost ->
+ identifier.findServersByHostId(serverHost.id).thenApply {
+ ServerHostWithServerCount(serverHost, it.size)
+ }
+ }
+ )
+ }
+
+}
+
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostWithServerCount.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostWithServerCount.kt
new file mode 100644
index 0000000..30006b9
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/host/ServerHostWithServerCount.kt
@@ -0,0 +1,8 @@
+package app.simplecloud.controller.runtime.host
+
+import app.simplecloud.controller.shared.host.ServerHost
+
+data class ServerHostWithServerCount(
+ val serverHost: ServerHost,
+ val serverCount: Int
+)
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
new file mode 100644
index 0000000..85f5558
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/ControllerStartCommand.kt
@@ -0,0 +1,50 @@
+package app.simplecloud.controller.runtime.launcher
+
+import app.simplecloud.controller.runtime.ControllerRuntime
+import app.simplecloud.controller.shared.secret.AuthFileSecretFactory
+import com.github.ajalt.clikt.core.CliktCommand
+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.int
+import com.github.ajalt.clikt.parameters.types.path
+import java.nio.file.Path
+
+class ControllerStartCommand : CliktCommand() {
+
+ private val defaultDatabaseUrl = "jdbc:sqlite:database.db"
+
+ val groupPath: Path by option(help = "Path to the group files (default: groups)", envvar = "GROUPS_PATH")
+ .path()
+ .default(Path.of("groups"))
+ val databaseUrl: String by option(help = "Database URL (default: ${defaultDatabaseUrl})", envvar = "DATABASE_URL")
+ .default(defaultDatabaseUrl)
+
+ val grpcHost: String by option(help = "Grpc host (default: localhost)", envvar = "GRPC_HOST").default("localhost")
+ val grpcPort: Int by option(help = "Grpc port (default: 5816)", envvar = "GRPC_PORT").int().default(5816)
+
+ private val authSecretPath: Path by option(
+ help = "Path to auth secret file (default: .auth.secret)",
+ envvar = "AUTH_SECRET_PATH"
+ )
+ .path()
+ .default(Path.of(".secrets", "auth.secret"))
+
+ 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)
+ controllerRuntime.start()
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..10c2f99
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/Launcher.kt
@@ -0,0 +1,16 @@
+package app.simplecloud.controller.runtime.launcher
+
+import org.apache.logging.log4j.LogManager
+
+
+fun main(args: Array) {
+ configureLog4j()
+ ControllerStartCommand().main(args)
+}
+
+fun configureLog4j() {
+ val globalExceptionHandlerLogger = LogManager.getLogger("GlobalExceptionHandler")
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
+ globalExceptionHandlerLogger.error("Uncaught exception in thread ${thread.name}", throwable)
+ }
+}
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/LauncherKt.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/LauncherKt.kt
deleted file mode 100644
index 60744fe..0000000
--- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/launcher/LauncherKt.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package app.simplecloud.controller.runtime.launcher
-
-import app.simplecloud.controller.runtime.ControllerRuntime
-
-fun main() {
- val controllerRuntime = ControllerRuntime()
- controllerRuntime.start()
-}
\ No newline at end of file
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
new file mode 100644
index 0000000..26de957
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt
@@ -0,0 +1,137 @@
+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 build.buf.gen.simplecloud.controller.v1.ControllerServerServiceGrpc.ControllerServerServiceFutureStub
+import build.buf.gen.simplecloud.controller.v1.GroupNameRequest
+import build.buf.gen.simplecloud.controller.v1.ServerIdRequest
+import build.buf.gen.simplecloud.controller.v1.ServerState
+import org.apache.logging.log4j.LogManager
+import java.time.LocalDateTime
+import kotlin.math.min
+
+class GroupReconciler(
+ private val serverRepository: ServerRepository,
+ private val serverHostRepository: ServerHostRepository,
+ private val numericalIdRepository: ServerNumericalIdRepository,
+ private val serverStub: ControllerServerServiceFutureStub,
+ private val group: Group,
+) {
+
+ private val logger = LogManager.getLogger(GroupReconciler::class.java)
+ private val servers = this.serverRepository.findServersByGroup(this.group.name).get()
+
+ private val availableServerCount = calculateAvailableServerCount()
+
+ fun reconcile() {
+ cleanupServers()
+ cleanupNumericalIds()
+ startServers()
+ }
+
+ private fun calculateAvailableServerCount(): Int {
+ return servers.count { server ->
+ hasAvailableState(server) && isOnlineCountBelowPlayerRatio(server)
+ }
+ }
+
+ private fun isOnlineCountBelowPlayerRatio(server: Server): Boolean {
+ if (this.group.newServerPlayerRatio <= 0) {
+ return true
+ }
+ return (server.playerCount / server.maxPlayers) * 100 < this.group.newServerPlayerRatio
+ }
+
+ private fun hasAvailableState(server: Server): Boolean {
+ return server.state == ServerState.AVAILABLE
+ || server.state == ServerState.STARTING
+ || server.state == ServerState.PREPARING
+ }
+
+ private fun cleanupServers() {
+ val fullyStartedServers = this.servers.filter { it.state == ServerState.AVAILABLE }
+ val hasMoreServersThenNeeded = fullyStartedServers.size > this.group.minOnlineCount
+ if (!hasMoreServersThenNeeded)
+ return
+
+ val serverCountToStop = fullyStartedServers.size - this.group.minOnlineCount
+ fullyStartedServers
+ .filter { !wasUpdatedRecently(it) }
+ .shuffled()
+ .take(serverCountToStop.toInt())
+ .forEach { stopServer(it) }
+ }
+
+ private fun stopServer(server: Server) {
+ logger.info("Stopping server ${server.uniqueId} of group ${server.group}")
+ serverStub.stopServer(
+ ServerIdRequest.newBuilder()
+ .setId(server.uniqueId)
+ .build()
+ ).toCompletable()
+ .thenApply {
+ logger.info("Stopped server ${server.uniqueId} of group ${server.group}")
+ }.exceptionally {
+ logger.error("Could not stop server ${server.uniqueId} of group ${server.group}: ${it.message}")
+ }
+ }
+
+ private fun cleanupNumericalIds() {
+ val usedNumericalIds = this.servers.map { it.numericalId }
+ val numericalIds = this.numericalIdRepository.findNumericalIds(this.group.name)
+
+ val unusedNumericalIds = numericalIds.filter { !usedNumericalIds.contains(it) }
+
+ unusedNumericalIds.forEach { this.numericalIdRepository.removeNumericalId(this.group.name, it) }
+
+ if (unusedNumericalIds.isNotEmpty()) {
+ logger.info("Removed unused numerical ids $unusedNumericalIds of group ${this.group.name}")
+ }
+ }
+
+ private fun wasUpdatedRecently(server: Server): Boolean {
+ return server.updatedAt.isAfter(LocalDateTime.now().minusMinutes(INACTIVE_SERVER_TIME))
+ }
+
+ private fun startServers() {
+ serverHostRepository.areServerHostsAvailable().thenApply {
+ if (!it) return@thenApply
+ if (isNewServerNeeded())
+ startServer()
+ }
+ }
+
+ private fun startServer() {
+ logger.info("Starting new instance of group ${this.group.name}")
+ serverStub.startServer(GroupNameRequest.newBuilder().setName(this.group.name).build()).toCompletable()
+ .thenApply {
+ logger.info("Started new instance ${it.groupName}-${it.numericalId}/${it.uniqueId} of group ${this.group.name} on ${it.ip}:${it.port}")
+ }.exceptionally {
+ it.printStackTrace()
+ logger.error("Could not start a new instance of group ${this.group.name}: ${it.message}")
+ }
+ }
+
+ private fun isNewServerNeeded(): Boolean {
+ return calculateServerCountToStart() > 0
+ }
+
+ private fun calculateServerCountToStart(): Int {
+ val currentServerCount = this.servers.size
+ val neededServerCount = this.group.minOnlineCount - this.availableServerCount
+ val maxNewServers = this.group.maxOnlineCount - currentServerCount
+
+ if (neededServerCount > 0)
+ return min(neededServerCount, maxNewServers).toInt()
+ return 0
+ }
+
+ companion object {
+ private const val INACTIVE_SERVER_TIME = 5L
+ }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..93525d2
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/Reconciler.kt
@@ -0,0 +1,37 @@
+package app.simplecloud.controller.runtime.reconciler
+
+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 build.buf.gen.simplecloud.controller.v1.ControllerServerServiceGrpc
+import io.grpc.ManagedChannel
+
+class Reconciler(
+ private val groupRepository: GroupRepository,
+ private val serverRepository: ServerRepository,
+ private val serverHostRepository: ServerHostRepository,
+ private val numericalIdRepository: ServerNumericalIdRepository,
+ managedChannel: ManagedChannel,
+ authCallCredentials: AuthCallCredentials,
+) {
+
+ private val serverStub = ControllerServerServiceGrpc.newFutureStub(managedChannel)
+ .withCallCredentials(authCallCredentials)
+
+ fun reconcile() {
+ this.groupRepository.getAll().thenApply {
+ it.forEach { group ->
+ GroupReconciler(
+ serverRepository,
+ serverHostRepository,
+ numericalIdRepository,
+ serverStub,
+ group
+ ).reconcile()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerNumericalIdRepository.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerNumericalIdRepository.kt
new file mode 100644
index 0000000..d8962cd
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerNumericalIdRepository.kt
@@ -0,0 +1,33 @@
+package app.simplecloud.controller.runtime.server
+
+import java.util.concurrent.ConcurrentHashMap
+
+class ServerNumericalIdRepository {
+
+ private val numericalIds = ConcurrentHashMap>()
+
+ @Synchronized
+ fun findNextNumericalId(group: String): Int {
+ val numericalIds = findNumericalIds(group)
+ var nextId = 1
+ while (numericalIds.contains(nextId)) {
+ nextId++
+ }
+ saveNumericalId(group, nextId)
+ return nextId
+ }
+
+ fun saveNumericalId(group: String, id: Int) {
+ numericalIds.compute(group) { _, v -> v?.plus(id) ?: setOf(id) }
+ }
+
+ @Synchronized
+ fun removeNumericalId(group: String, id: Int): Boolean {
+ return numericalIds.computeIfPresent(group) { _, v -> v.minus(id) } != null
+ }
+
+ fun findNumericalIds(group: String): Set {
+ return numericalIds[group] ?: emptySet()
+ }
+
+}
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
new file mode 100644
index 0000000..7b3dea6
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerRepository.kt
@@ -0,0 +1,232 @@
+package app.simplecloud.controller.runtime.server
+
+import app.simplecloud.controller.runtime.LoadableRepository
+import app.simplecloud.controller.runtime.database.Database
+import app.simplecloud.controller.shared.db.Tables.CLOUD_SERVERS
+import app.simplecloud.controller.shared.db.Tables.CLOUD_SERVER_PROPERTIES
+import app.simplecloud.controller.shared.db.tables.records.CloudServersRecord
+import app.simplecloud.controller.shared.server.Server
+import build.buf.gen.simplecloud.controller.v1.ServerState
+import build.buf.gen.simplecloud.controller.v1.ServerType
+import org.jooq.Result
+import java.time.LocalDateTime
+import java.util.concurrent.CompletableFuture
+
+class ServerRepository(
+ private val database: Database,
+ private val numericalIdRepository: ServerNumericalIdRepository
+) : LoadableRepository {
+
+
+ override fun find(identifier: String): CompletableFuture {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .where(CLOUD_SERVERS.UNIQUE_ID.eq(identifier))
+ .fetchInto(
+ CLOUD_SERVERS
+ )
+ return@supplyAsync toList(query).firstOrNull()
+ }
+ }
+
+
+ fun findServerByNumerical(group: String, id: Int): CompletableFuture {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select().from(CLOUD_SERVERS)
+ .where(
+ CLOUD_SERVERS.GROUP_NAME.eq(group)
+ .and(CLOUD_SERVERS.NUMERICAL_ID.eq(id))
+ )
+ .fetchInto(CLOUD_SERVERS)
+ return@supplyAsync toList(query).firstOrNull()
+ }
+ }
+
+ override fun getAll(): CompletableFuture> {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .fetchInto(CLOUD_SERVERS)
+ return@supplyAsync toList(query)
+ }
+ }
+
+ fun findServersByHostId(id: String): CompletableFuture> {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .where(CLOUD_SERVERS.HOST_ID.eq(id))
+ .fetchInto(
+ CLOUD_SERVERS
+ )
+ return@supplyAsync toList(query)
+ }
+ }
+
+ fun findServersByGroup(group: String): CompletableFuture> {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .where(CLOUD_SERVERS.GROUP_NAME.eq(group))
+ .fetchInto(
+ CLOUD_SERVERS
+ )
+ return@supplyAsync toList(query)
+ }
+
+ }
+
+ fun findServersByType(type: ServerType): CompletableFuture> {
+ return CompletableFuture.supplyAsync {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .where(CLOUD_SERVERS.TYPE.eq(type.toString()))
+ .fetchInto(CLOUD_SERVERS)
+ return@supplyAsync toList(query)
+ }
+ }
+
+ private fun toList(query: Result): List {
+ val result = mutableListOf()
+ query.map {
+ val propertiesQuery =
+ database.context.select()
+ .from(CLOUD_SERVER_PROPERTIES)
+ .where(CLOUD_SERVER_PROPERTIES.SERVER_ID.eq(it.uniqueId))
+ .fetchInto(CLOUD_SERVER_PROPERTIES)
+ result.add(
+ Server(
+ it.uniqueId,
+ ServerType.valueOf(it.type),
+ it.groupName,
+ it.hostId,
+ it.numericalId,
+ it.ip,
+ it.port.toLong(),
+ it.minimumMemory.toLong(),
+ it.maximumMemory.toLong(),
+ it.maxPlayers.toLong(),
+ it.playerCount.toLong(),
+ propertiesQuery.map { item ->
+ item.key to item.value
+ }.toMap().toMutableMap(),
+ ServerState.valueOf(it.state),
+ it.createdAt,
+ it.updatedAt
+ )
+ )
+ }
+ return result
+ }
+
+ override fun delete(element: Server): CompletableFuture {
+ val canDelete =
+ database.context.deleteFrom(CLOUD_SERVER_PROPERTIES)
+ .where(CLOUD_SERVER_PROPERTIES.SERVER_ID.eq(element.uniqueId))
+ .executeAsync().toCompletableFuture().thenApply {
+ return@thenApply true
+ }.exceptionally {
+ it.printStackTrace()
+ return@exceptionally false
+ }.get()
+ if (!canDelete) return CompletableFuture.completedFuture(false)
+ numericalIdRepository.removeNumericalId(element.group, element.numericalId)
+ return database.context.deleteFrom(CLOUD_SERVERS)
+ .where(CLOUD_SERVERS.UNIQUE_ID.eq(element.uniqueId))
+ .executeAsync()
+ .toCompletableFuture().thenApply {
+ return@thenApply it > 0
+ }.exceptionally {
+ it.printStackTrace()
+ return@exceptionally false
+ }
+ }
+
+ @Synchronized
+ override fun save(element: Server) {
+ numericalIdRepository.saveNumericalId(element.group, element.numericalId)
+
+ val currentTimestamp = LocalDateTime.now()
+ database.context.insertInto(
+ CLOUD_SERVERS,
+
+ CLOUD_SERVERS.UNIQUE_ID,
+ CLOUD_SERVERS.TYPE,
+ CLOUD_SERVERS.GROUP_NAME,
+ CLOUD_SERVERS.HOST_ID,
+ CLOUD_SERVERS.NUMERICAL_ID,
+ CLOUD_SERVERS.IP,
+ CLOUD_SERVERS.PORT,
+ CLOUD_SERVERS.MINIMUM_MEMORY,
+ CLOUD_SERVERS.MAXIMUM_MEMORY,
+ CLOUD_SERVERS.MAX_PLAYERS,
+ CLOUD_SERVERS.PLAYER_COUNT,
+ CLOUD_SERVERS.STATE,
+ CLOUD_SERVERS.CREATED_AT,
+ CLOUD_SERVERS.UPDATED_AT
+ )
+ .values(
+ element.uniqueId,
+ element.type.toString(),
+ element.group,
+ element.host,
+ element.numericalId,
+ element.ip,
+ element.port.toInt(),
+ element.minMemory.toInt(),
+ element.maxMemory.toInt(),
+ element.maxPlayers.toInt(),
+ element.playerCount.toInt(),
+ element.state.toString(),
+ currentTimestamp,
+ currentTimestamp
+ )
+ .onDuplicateKeyUpdate()
+ .set(CLOUD_SERVERS.UNIQUE_ID, element.uniqueId)
+ .set(CLOUD_SERVERS.TYPE, element.type.toString())
+ .set(CLOUD_SERVERS.GROUP_NAME, element.group)
+ .set(CLOUD_SERVERS.HOST_ID, element.host)
+ .set(CLOUD_SERVERS.NUMERICAL_ID, element.numericalId)
+ .set(CLOUD_SERVERS.IP, element.ip)
+ .set(CLOUD_SERVERS.PORT, element.port.toInt())
+ .set(CLOUD_SERVERS.MINIMUM_MEMORY, element.minMemory.toInt())
+ .set(CLOUD_SERVERS.MAXIMUM_MEMORY, element.maxMemory.toInt())
+ .set(CLOUD_SERVERS.MAX_PLAYERS, element.maxPlayers.toInt())
+ .set(CLOUD_SERVERS.PLAYER_COUNT, element.playerCount.toInt())
+ .set(CLOUD_SERVERS.STATE, element.state.toString())
+ .set(CLOUD_SERVERS.UPDATED_AT, currentTimestamp)
+ .executeAsync()
+ element.properties.forEach {
+ database.context.insertInto(
+ CLOUD_SERVER_PROPERTIES,
+
+ CLOUD_SERVER_PROPERTIES.SERVER_ID,
+ CLOUD_SERVER_PROPERTIES.KEY,
+ CLOUD_SERVER_PROPERTIES.VALUE
+ )
+ .values(
+ element.uniqueId,
+ it.key,
+ it.value
+ )
+ .onDuplicateKeyUpdate()
+ .set(CLOUD_SERVER_PROPERTIES.SERVER_ID, element.uniqueId)
+ .set(CLOUD_SERVER_PROPERTIES.KEY, it.key)
+ .set(CLOUD_SERVER_PROPERTIES.VALUE, it.value)
+ .executeAsync()
+ }
+ }
+
+ override fun load(): List {
+ val query = database.context.select()
+ .from(CLOUD_SERVERS)
+ .fetchInto(CLOUD_SERVERS)
+ val list = toList(query)
+ list.forEach {
+ numericalIdRepository.saveNumericalId(it.group, it.numericalId)
+ }
+ return list
+ }
+
+}
\ 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
new file mode 100644
index 0000000..c0b6dfd
--- /dev/null
+++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt
@@ -0,0 +1,349 @@
+package app.simplecloud.controller.runtime.server
+
+import app.simplecloud.controller.runtime.group.GroupRepository
+import app.simplecloud.controller.runtime.host.ServerHostException
+import app.simplecloud.controller.runtime.host.ServerHostRepository
+import app.simplecloud.controller.shared.auth.AuthCallCredentials
+import app.simplecloud.controller.shared.future.toCompletable
+import app.simplecloud.controller.shared.group.Group
+import app.simplecloud.controller.shared.host.ServerHost
+import app.simplecloud.controller.shared.server.Server
+import build.buf.gen.simplecloud.controller.v1.*
+import io.grpc.Context
+import io.grpc.Status
+import io.grpc.stub.StreamObserver
+import org.apache.logging.log4j.LogManager
+import java.time.LocalDateTime
+import java.util.*
+import java.util.concurrent.CompletableFuture
+
+class ServerService(
+ private val numericalIdRepository: ServerNumericalIdRepository,
+ private val serverRepository: ServerRepository,
+ private val hostRepository: ServerHostRepository,
+ private val groupRepository: GroupRepository,
+ private val forwardingSecret: String,
+ private val authCallCredentials: AuthCallCredentials
+) : ControllerServerServiceGrpc.ControllerServerServiceImplBase() {
+
+ private val logger = LogManager.getLogger(ServerService::class.java)
+
+ override fun attachServerHost(request: ServerHostDefinition, responseObserver: StreamObserver) {
+ val serverHost = ServerHost.fromDefinition(request)
+ try {
+ hostRepository.delete(serverHost)
+ hostRepository.save(serverHost)
+ }catch (e: Exception) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not save serverhost")
+ .withCause(e)
+ .asRuntimeException()
+ )
+ return
+ }
+ logger.info("Successfully registered ServerHost ${serverHost.id}.")
+ responseObserver.onNext(serverHost.toDefinition())
+ responseObserver.onCompleted()
+ Context.current().fork().run {
+ val channel = serverHost.createChannel()
+ val stub = ServerHostServiceGrpc.newFutureStub(channel)
+ .withCallCredentials(authCallCredentials)
+ serverRepository.findServersByHostId(serverHost.id).thenApply {
+ it.forEach { server ->
+ logger.info("Reattaching Server ${server.uniqueId} of group ${server.group}...")
+ stub.reattachServer(server.toDefinition()).toCompletable().thenApply {
+ logger.info("Success!")
+ }.exceptionally {
+ logger.error("Server was found to be offline, unregistering...")
+ serverRepository.delete(server)
+ }.get()
+ }
+ channel.shutdown()
+ }
+ }
+ }
+
+ override fun getAllServers(
+ request: GetAllServersRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.getAll().thenApply { servers ->
+ responseObserver.onNext(
+ GetAllServersResponse.newBuilder()
+ .addAllServers(servers.map { it.toDefinition() })
+ .build()
+ )
+ responseObserver.onCompleted()
+ }
+ }
+
+ override fun getServerByNumerical(
+ request: GetServerByNumericalRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.findServerByNumerical(request.group, request.numericalId.toInt()).thenApply { server ->
+ if (server == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("No server was found matching this group and numerical id")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ }
+ }
+
+ override fun stopServerByNumerical(
+ request: StopServerByNumericalRequest,
+ responseObserver: StreamObserver
+ ) {
+
+ serverRepository.findServerByNumerical(request.group, request.numericalId.toInt()).thenApply { server ->
+ if (server == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("No server was found matching this group and numerical id")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
+ stopServer(server.toDefinition()).thenApply {
+ responseObserver.onNext(it)
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(it)
+ }
+ }
+
+ }
+
+ override fun updateServer(request: ServerUpdateRequest, responseObserver: StreamObserver) {
+ val deleted = request.deleted
+ val server = Server.fromDefinition(request.server)
+ if (!deleted) {
+ try {
+ serverRepository.save(server)
+ }catch (e: Exception) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not update server")
+ .withCause(e)
+ .asRuntimeException()
+ )
+ return
+ }
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ } else {
+ logger.info("Deleting server ${server.uniqueId} of group ${request.server.groupName}...")
+ serverRepository.delete(server).thenApply thenDelete@ {
+ if(!it) {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not delete server")
+ .asRuntimeException()
+ )
+ return@thenDelete
+ }
+ logger.info("Deleted server ${server.uniqueId} of group ${request.server.groupName}.")
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(
+ Status.INTERNAL
+ .withDescription("Could not delete server")
+ .withCause(it)
+ .asRuntimeException()
+ )
+ }
+ }
+ }
+
+ override fun getServerById(request: ServerIdRequest, responseObserver: StreamObserver) {
+ serverRepository.find(request.id).thenApply { server ->
+ if (server == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("No server was found matching this unique id")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ }
+
+ }
+
+ override fun getServersByGroup(
+ request: GroupNameRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.findServersByGroup(request.name).thenApply { servers ->
+ val response = GetServersByGroupResponse.newBuilder()
+ .addAllServers(servers.map { it.toDefinition() })
+ .build()
+ responseObserver.onNext(response)
+ responseObserver.onCompleted()
+ }
+ }
+
+ override fun getServersByType(
+ request: ServerTypeRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.findServersByType(request.type).thenApply { servers ->
+ val response = GetServersByGroupResponse.newBuilder()
+ .addAllServers(servers.map { it.toDefinition() })
+ .build()
+ responseObserver.onNext(response)
+ responseObserver.onCompleted()
+ }
+ }
+
+ override fun startServer(request: GroupNameRequest, responseObserver: StreamObserver) {
+ hostRepository.find(serverRepository).thenApply { host ->
+ if (host == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("No server host found, could not start server")
+ .asRuntimeException()
+ )
+ return@thenApply
+ }
+ groupRepository.find(request.name).thenApply { group ->
+ if (group == null) {
+ responseObserver.onError(
+ Status.NOT_FOUND
+ .withDescription("No group was found matching this name")
+ .asRuntimeException()
+ )
+ } else {
+ startServer(host, group)
+ }
+ }
+ }
+ }
+
+ private fun startServer(host: ServerHost, group: Group): CompletableFuture {
+ val numericalId = numericalIdRepository.findNextNumericalId(group.name)
+ val server = buildServer(group, numericalId, forwardingSecret)
+ serverRepository.save(server)
+ val channel = host.createChannel()
+ val stub = ServerHostServiceGrpc.newFutureStub(channel)
+ .withCallCredentials(authCallCredentials)
+ serverRepository.save(server)
+ return stub.startServer(
+ StartServerRequest.newBuilder()
+ .setGroup(group.toDefinition())
+ .setServer(server.toDefinition())
+ .build()
+ ).toCompletable().thenApply {
+ serverRepository.save(Server.fromDefinition(it))
+ channel.shutdown()
+ return@thenApply it
+ }.exceptionally {
+ serverRepository.delete(server)
+ numericalIdRepository.removeNumericalId(group.name, server.numericalId)
+ channel.shutdown()
+ throw it
+ }
+ }
+
+ private fun buildServer(group: Group, numericalId: Int, forwardingSecret: String): Server {
+ return Server.fromDefinition(
+ ServerDefinition.newBuilder()
+ .setNumericalId(numericalId)
+ .setType(group.type)
+ .setGroupName(group.name)
+ .setMinimumMemory(group.minMemory)
+ .setMaximumMemory(group.maxMemory)
+ .setState(ServerState.PREPARING)
+ .setMaxPlayers(group.maxPlayers)
+ .setCreatedAt(LocalDateTime.now().toString())
+ .setUpdatedAt(LocalDateTime.now().toString())
+ .setPlayerCount(0)
+ .setUniqueId(UUID.randomUUID().toString().replace("-", "")).putAllProperties(
+ mapOf(
+ *group.properties.entries.map { it.key to it.value }.toTypedArray(),
+ "forwarding-secret" to forwardingSecret,
+ )
+ ).build()
+ )
+ }
+
+ override fun stopServer(request: ServerIdRequest, responseObserver: StreamObserver) {
+ serverRepository.find(request.id).thenApply { server ->
+ if (server == null) {
+ throw Status.NOT_FOUND
+ .withDescription("No server was found matching this id.")
+ .asRuntimeException()
+ }
+ stopServer(server.toDefinition()).thenApply {
+ responseObserver.onNext(it)
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(it)
+ }.get()
+ }.exceptionally {
+ responseObserver.onError(it)
+ }
+ }
+
+ private fun stopServer(server: ServerDefinition): CompletableFuture {
+ val host = hostRepository.findServerHostById(server.hostId)
+ ?: throw Status.NOT_FOUND
+ .withDescription("No server host was found matching this server.")
+ .asRuntimeException()
+ val channel = host.createChannel()
+ val stub = ServerHostServiceGrpc.newFutureStub(channel)
+ .withCallCredentials(authCallCredentials)
+ return stub.stopServer(server).toCompletable().thenApply {
+ serverRepository.delete(Server.fromDefinition(server))
+ channel.shutdown()
+ return@thenApply it
+ }
+ }
+
+ override fun updateServerProperty(
+ request: ServerUpdatePropertyRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.find(request.id).thenApply { server ->
+ if (server == null) {
+ throw Status.NOT_FOUND
+ .withDescription("Server with id ${request.id} does not exist.")
+ .asRuntimeException()
+ }
+ server.properties[request.key] = request.value
+ serverRepository.save(server)
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(it)
+ }
+ }
+
+ override fun updateServerState(
+ request: ServerUpdateStateRequest,
+ responseObserver: StreamObserver
+ ) {
+ serverRepository.find(request.id).thenApply { server ->
+ if (server == null) {
+ throw Status.NOT_FOUND
+ .withDescription("Server with id ${request.id} does not exist.")
+ .asRuntimeException()
+ }
+ server.state = request.state
+ serverRepository.save(server)
+ responseObserver.onNext(server.toDefinition())
+ responseObserver.onCompleted()
+ }.exceptionally {
+ responseObserver.onError(it)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/controller-runtime/src/main/resources/log4j2.xml b/controller-runtime/src/main/resources/log4j2.xml
index e32762f..462153c 100644
--- a/controller-runtime/src/main/resources/log4j2.xml
+++ b/controller-runtime/src/main/resources/log4j2.xml
@@ -4,10 +4,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/controller-shared/build.gradle.kts b/controller-shared/build.gradle.kts
index cf71efc..56a56b8 100644
--- a/controller-shared/build.gradle.kts
+++ b/controller-shared/build.gradle.kts
@@ -1,51 +1,4 @@
-import com.google.protobuf.gradle.*
-
-plugins {
- alias(libs.plugins.protobuf)
-}
-
dependencies {
api(rootProject.libs.bundles.proto)
+ api(rootProject.libs.bundles.configurate)
}
-
-sourceSets {
- main {
- kotlin {
- srcDirs(
- "build/generated/source/proto/main/grpckt",
- "build/generated/source/proto/main/kotlin",
- )
- }
- java {
- srcDirs(
- "build/generated/source/proto/main/grpc",
- "build/generated/source/proto/main/java",
- )
- }
- }
-}
-
-protobuf {
- protoc {
- artifact = "com.google.protobuf:protoc:3.23.1"
- }
- plugins {
- id("grpc") {
- artifact = "io.grpc:protoc-gen-grpc-java:1.55.1"
- }
- id("grpckt") {
- artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.0:jdk8@jar"
- }
- }
- generateProtoTasks {
- all().forEach {
- it.plugins {
- id("grpc")
- id("grpckt")
- }
- it.builtins {
- id("kotlin")
- }
- }
- }
-}
\ 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
new file mode 100644
index 0000000..bf9c277
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/MetadataKeys.kt
@@ -0,0 +1,9 @@
+package app.simplecloud.controller.shared
+
+import io.grpc.Metadata
+
+object MetadataKeys {
+
+ val AUTH_SECRET_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER)
+
+}
\ 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
new file mode 100644
index 0000000..8ffa976
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthCallCredentials.kt
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..290c710
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/auth/AuthSecretInterceptor.kt
@@ -0,0 +1,24 @@
+package app.simplecloud.controller.shared.auth
+
+import app.simplecloud.controller.shared.MetadataKeys
+import io.grpc.*
+
+class AuthSecretInterceptor(
+ private val secretKey: String
+) : ServerInterceptor {
+
+ override fun interceptCall(
+ call: ServerCall,
+ headers: Metadata,
+ next: ServerCallHandler
+ ): ServerCall.Listener {
+ val secretKey = headers.get(MetadataKeys.AUTH_SECRET_KEY)
+ if (this.secretKey != secretKey) {
+ call.close(Status.UNAUTHENTICATED, headers)
+ return object : ServerCall.Listener() {}
+ }
+
+ return Contexts.interceptCall(Context.current(), call, headers, next)
+ }
+
+}
\ 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
index f91c395..013f43d 100644
--- 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
@@ -30,6 +30,7 @@ class ListenableFutureAdapter(
}
}, ForkJoinPool.commonPool())
}
+
companion object {
fun toCompletable(listenableFuture: ListenableFuture): CompletableFuture {
val listenableFutureAdapter: ListenableFutureAdapter = ListenableFutureAdapter(listenableFuture)
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 2897e7f..014f507 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
@@ -1,14 +1,35 @@
package app.simplecloud.controller.shared.group
-import app.simplecloud.controller.shared.proto.GroupDefinition
+import build.buf.gen.simplecloud.controller.v1.GroupDefinition
+import build.buf.gen.simplecloud.controller.v1.ServerType
+import org.spongepowered.configurate.objectmapping.ConfigSerializable
+@ConfigSerializable
data class Group(
- val name: String,
+ val name: String = "",
+ val type: ServerType = ServerType.OTHER,
+ val minMemory: Long = 0,
+ val maxMemory: Long = 0,
+ val startPort: Long = 0,
+ val minOnlineCount: Long = 0,
+ val maxOnlineCount: Long = 0,
+ val maxPlayers: Long = 0,
+ val newServerPlayerRatio: Long = -1,
+ val properties: Map = mapOf()
) {
fun toDefinition(): GroupDefinition {
return GroupDefinition.newBuilder()
.setName(name)
+ .setType(type)
+ .setMinimumMemory(minMemory)
+ .setMaximumMemory(maxMemory)
+ .setStartPort(startPort)
+ .setMinimumOnlineCount(minOnlineCount)
+ .setMaximumOnlineCount(maxOnlineCount)
+ .setMaxPlayers(maxPlayers)
+ .setNewServerPlayerRatio(newServerPlayerRatio)
+ .putAllProperties(properties)
.build()
}
@@ -16,7 +37,16 @@ data class Group(
@JvmStatic
fun fromDefinition(groupDefinition: GroupDefinition): Group {
return Group(
- groupDefinition.name
+ groupDefinition.name,
+ groupDefinition.type,
+ groupDefinition.minimumMemory,
+ groupDefinition.maximumMemory,
+ groupDefinition.startPort,
+ groupDefinition.minimumOnlineCount,
+ groupDefinition.maximumOnlineCount,
+ groupDefinition.maxPlayers,
+ groupDefinition.newServerPlayerRatio,
+ groupDefinition.propertiesMap
)
}
}
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
new file mode 100644
index 0000000..c712a22
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/host/ServerHost.kt
@@ -0,0 +1,39 @@
+package app.simplecloud.controller.shared.host
+
+import build.buf.gen.simplecloud.controller.v1.ServerHostDefinition
+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
+) {
+
+ fun toDefinition(): ServerHostDefinition {
+ return ServerHostDefinition.newBuilder()
+ .setHost(host)
+ .setPort(port)
+ .setUniqueId(id)
+ .build()
+ }
+
+ companion object {
+ @JvmStatic
+ fun fromDefinition(serverHostDefinition: ServerHostDefinition): ServerHost {
+ return ServerHost(
+ serverHostDefinition.uniqueId,
+ serverHostDefinition.host,
+ serverHostDefinition.port
+ )
+ }
+ }
+
+
+ fun createChannel(): ManagedChannel {
+ return ManagedChannelBuilder.forAddress(host, port).usePlaintext().build()
+ }
+
+}
\ 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
new file mode 100644
index 0000000..bd0d540
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/AuthFileSecretFactory.kt
@@ -0,0 +1,28 @@
+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
new file mode 100644
index 0000000..8c18bd3
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/secret/SecretGenerator.kt
@@ -0,0 +1,15 @@
+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/server/Server.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt
new file mode 100644
index 0000000..94c5398
--- /dev/null
+++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt
@@ -0,0 +1,72 @@
+package app.simplecloud.controller.shared.server
+
+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.LocalDateTime
+
+data class Server(
+ val uniqueId: String,
+ val type: ServerType,
+ val group: String,
+ val host: String?,
+ val numericalId: Int,
+ val ip: String,
+ val port: Long,
+ val minMemory: Long,
+ val maxMemory: Long,
+ val maxPlayers: Long,
+ var playerCount: Long,
+ val properties: MutableMap,
+ var state: ServerState,
+ val createdAt: LocalDateTime,
+ val updatedAt: LocalDateTime
+) {
+ fun toDefinition(): ServerDefinition {
+ return ServerDefinition.newBuilder()
+ .setUniqueId(uniqueId)
+ .setType(type)
+ .setGroupName(group)
+ .setHostId(host)
+ .setIp(ip)
+ .setPort(port)
+ .setState(state)
+ .setMinimumMemory(minMemory)
+ .setMaximumMemory(maxMemory)
+ .setPlayerCount(playerCount)
+ .setMaxPlayers(maxPlayers)
+ .putAllProperties(properties)
+ .setNumericalId(numericalId)
+ .setCreatedAt(createdAt.toString())
+ .setUpdatedAt(updatedAt.toString())
+ .build()
+ }
+
+ companion object {
+ @JvmStatic
+ fun fromDefinition(serverDefinition: ServerDefinition): Server {
+ return Server(
+ serverDefinition.uniqueId,
+ serverDefinition.type,
+ serverDefinition.groupName,
+ serverDefinition.hostId,
+ serverDefinition.numericalId,
+ serverDefinition.ip,
+ serverDefinition.port,
+ serverDefinition.minimumMemory,
+ serverDefinition.maximumMemory,
+ serverDefinition.maxPlayers,
+ serverDefinition.playerCount,
+ serverDefinition.propertiesMap,
+ serverDefinition.state,
+ LocalDateTime.parse(serverDefinition.createdAt),
+ LocalDateTime.parse(serverDefinition.updatedAt)
+ )
+ }
+
+ @JvmStatic
+ fun fromDefinition(definitions: List): List {
+ return definitions.map { definition -> fromDefinition(definition) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/controller-shared/src/main/proto/controller_group_api.proto b/controller-shared/src/main/proto/controller_group_api.proto
deleted file mode 100644
index 6e40d92..0000000
--- a/controller-shared/src/main/proto/controller_group_api.proto
+++ /dev/null
@@ -1,20 +0,0 @@
-syntax = "proto3";
-
-package app.simplecloud.controller.shared.proto;
-
-option java_package = "app.simplecloud.controller.shared.proto";
-option java_multiple_files = true;
-
-import "controller_types.proto";
-
-message GetGroupByNameRequest {
- string name = 1;
-}
-
-message GetGroupByNameResponse {
- GroupDefinition group = 2;
-}
-
-service ControllerGroupService {
- rpc getGroupByName(GetGroupByNameRequest) returns (GetGroupByNameResponse);
-}
\ No newline at end of file
diff --git a/controller-shared/src/main/proto/controller_types.proto b/controller-shared/src/main/proto/controller_types.proto
deleted file mode 100644
index 4aa24b8..0000000
--- a/controller-shared/src/main/proto/controller_types.proto
+++ /dev/null
@@ -1,44 +0,0 @@
-syntax = "proto3";
-
-package app.simplecloud.controller.shared.proto;
-
-option java_package = "app.simplecloud.controller.shared.proto";
-option java_multiple_files = true;
-
-message GroupDefinition {
- string name = 1;
- string template_name = 2;
- uint64 minimum_memory = 3;
- uint64 maximum_memory = 4;
- uint64 start_port = 5;
- uint64 minimum_online_count = 6;
- uint64 maximum_online_count = 7;
- map properties = 8;
-}
-
-message ServerDefinition {
- string unique_id = 1;
- string groupName = 2;
- uint32 numerical_id = 3;
- string template_name = 4;
- uint64 port = 5;
- uint64 minimum_memory = 6;
- uint64 maximum_memory = 7;
- uint64 player_count = 8;
- map properties = 9;
- ServerState state = 10;
- ServerAllocationState allocation_state = 11;
-}
-
-enum ServerState {
- UNKNOWN = 0;
- STARTING = 1;
- RUNNING = 2;
- STOPPING = 3;
- STOPPED = 4;
-}
-
-enum ServerAllocationState {
- ALLOCATED = 0;
- UNALLOCATED = 1;
-}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3efaae8..c6e6e36 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,21 +1,49 @@
[versions]
kotlin = "1.8.0"
+kotlinCoroutines = "1.4.2"
shadow = "8.1.1"
log4j = "2.20.0"
-protobuf = "3.23.1"
-protobufPlugin = "0.8.19"
-grpcVersion = "1.55.1"
+protobuf = "3.25.2"
+grpc = "1.61.0"
+grpcKotlin = "1.4.1"
+simpleCloudProtoSpecs = "1.4.1.1.20240504121939.09af2f3cc691"
+jooq = "3.19.3"
+configurate = "4.1.2"
+sqliteJdbc = "3.44.1.0"
+clikt = "4.3.0"
+sonatypeCentralPortalPublisher = "1.2.3"
+spotifyCompletableFutures = "0.3.6"
[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" }
+
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" }
-protobuf = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
-grpcStub = { module = "io.grpc:grpc-stub", version.ref = "grpcVersion" }
-grpcProtobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpcVersion" }
-grpcNettyShaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpcVersion" }
+
+protobufKotlin = { 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" }
+
+simpleCloudProtoSpecs = { module = "build.buf.gen:simplecloud_proto-specs_grpc_kotlin", version.ref = "simpleCloudProtoSpecs" }
+
+qooq = { module = "org.jooq:jooq", version.ref = "jooq" }
+qooqMeta = { module = "org.jooq:jooq-meta", version.ref = "jooq" }
+jooqMetaExtensions = { module = "org.jooq:jooq-meta-extensions", version.ref = "jooq" }
+
+configurateYaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" }
+configurateExtraKotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" }
+
+sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" }
+
+clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
+
+spotifyCompletableFutures = { module = "com.spotify:completable-futures", version.ref = "spotifyCompletableFutures" }
[bundles]
log4j = [
@@ -24,13 +52,24 @@ log4j = [
"log4jSlf4j"
]
proto = [
- "protobuf",
+ "protobufKotlin",
"grpcStub",
+ "grpcKotlinStub",
"grpcProtobuf",
- "grpcNettyShaded"
+ "grpcNettyShaded",
+ "simpleCloudProtoSpecs"
+]
+jooq = [
+ "qooq",
+ "qooqMeta"
+]
+configurate = [
+ "configurateYaml",
+ "configurateExtraKotlin"
]
[plugins]
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
-protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
\ No newline at end of file
+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
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0832ac8..2f2ebf5 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu Jan 18 09:50:39 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..e7394f5
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,27 @@
+# SimpleCloud v3 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
+
+## Features
+
+- [x] Reconciler (auto-deploying for servers)
+- [x] [API](#api-usage) using [gRPC](https://grpc.io/)
+- [x] Server cache [SQL](https://en.wikipedia.org/wiki/SQL)-Database (any dialect)
+
+## ServerHosts
+
+ServerHosts are processes, that directly handle minecraft server deployments. Each root-server should have exactly one
+ServerHost online. We provide a [default implementation](),
+however, you can write your [own implementation](). You can have as many ServerHost instances as you like.
+
+## API usage
+
+> If you are searching for documentation, please visit our [official documentation](https://docs.simplecloud.app/api)
+
+The SimpleCloud v3 Controller provides API for both server groups and actual servers.
+The group API is used for [CRUD-Operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) of server
+groups, whereas the server API is used to manage running servers or starting new ones.
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3b5cc01..e5c8b31 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,7 +14,7 @@ plugins {
rootProject.name = "simplecloud-controller"
include(
- "controller-shared",
- "controller-api",
- "controller-runtime"
+ "controller-shared",
+ "controller-api",
+ "controller-runtime"
)
\ No newline at end of file
diff --git a/structure.png b/structure.png
new file mode 100644
index 0000000..a530aba
Binary files /dev/null and b/structure.png differ