diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt index 75675bc..383ccbf 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/ServerApi.kt @@ -1,11 +1,11 @@ package app.simplecloud.controller.api import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.ServerType import app.simplecloud.controller.shared.server.Server import build.buf.gen.simplecloud.controller.v1.ServerStartCause import build.buf.gen.simplecloud.controller.v1.ServerState import build.buf.gen.simplecloud.controller.v1.ServerStopCause +import build.buf.gen.simplecloud.controller.v1.ServerType import java.util.concurrent.CompletableFuture interface ServerApi { @@ -52,14 +52,21 @@ interface ServerApi { * @param groupName the group name of the group the new server should be of. * @return a [CompletableFuture] with a [Server] or null. */ - fun startServer(groupName: String, startCause: ServerStartCause = ServerStartCause.API_START): CompletableFuture + fun startServer( + groupName: String, + startCause: ServerStartCause = ServerStartCause.API_START + ): CompletableFuture /** * @param groupName the group name of the servers group. * @param numericalId the numerical id of the server. * @return a [CompletableFuture] with the stopped [Server]. */ - fun stopServer(groupName: String, numericalId: Long, stopCause: ServerStopCause = ServerStopCause.API_STOP): CompletableFuture + fun stopServer( + groupName: String, + numericalId: Long, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture /** * @param id the id of the server. @@ -67,6 +74,32 @@ interface ServerApi { */ fun stopServer(id: String, stopCause: ServerStopCause = ServerStopCause.API_STOP): CompletableFuture + /** + * Stops all servers within a specified group. + * + * @param groupName The name of the server group to stop. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A [CompletableFuture] containing a list of stopped [Server] instances. + */ + fun stopServers( + groupName: String, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture> + + /** + * Stops all servers within a specified group and sets a timeout to prevent new server starts for the group. + * + * @param groupName The name of the server group to stop. + * @param timeoutSeconds The duration (in seconds) for which new server starts will be prevented. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A [CompletableFuture] containing a list of stopped [Server] instances. + */ + fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): CompletableFuture> + /** * @param id the id of the server. * @param state the new state of the server. @@ -145,6 +178,32 @@ interface ServerApi { */ suspend fun stopServer(id: String, stopCause: ServerStopCause = ServerStopCause.API_STOP): Server + /** + * Stops all servers within a specified group. + * + * @param groupName The name of the server group to stop. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A list of stopped [Server] instances. + */ + suspend fun stopServers( + groupName: String, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): List + + /** + * Stops all servers within a specified group and sets a timeout to prevent new server starts for the group. + * + * @param groupName The name of the server group to stop. + * @param timeoutSeconds The duration (in seconds) for which new server starts will be prevented. + * @param stopCause The reason for stopping the servers. Defaults to [ServerStopCause.API_STOP]. + * @return A list of stopped [Server] instances. + */ + suspend fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause = ServerStopCause.API_STOP + ): List + /** * @param id the id of the server. * @param state the new state of the server. diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt index c41b314..ea08a3a 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/coroutines/ServerApiCoroutineImpl.kt @@ -2,9 +2,9 @@ package app.simplecloud.controller.api.impl.coroutines import app.simplecloud.controller.api.ServerApi import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials +import build.buf.gen.simplecloud.controller.v1.* import io.grpc.ManagedChannel class ServerApiCoroutineImpl( @@ -13,7 +13,8 @@ class ServerApiCoroutineImpl( ) : ServerApi.Coroutine { private val serverServiceStub: ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub = - ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub(managedChannel).withCallCredentials(authCallCredentials) + ControllerServerServiceGrpcKt.ControllerServerServiceCoroutineStub(managedChannel) + .withCallCredentials(authCallCredentials) override suspend fun getAllServers(): List { return serverServiceStub.getAllServers(getAllServersRequest {}).serversList.map { @@ -100,6 +101,25 @@ class ServerApiCoroutineImpl( ) } + override suspend fun stopServers(groupName: String, stopCause: ServerStopCause): List { + return serverServiceStub.stopServersByGroup(stopServersByGroupRequest { + this.groupName = groupName + this.stopCause = stopCause + }).serversList.map { + Server.fromDefinition(it) + } + } + + override suspend fun stopServers(groupName: String, timeoutSeconds: Int, stopCause: ServerStopCause): List { + return serverServiceStub.stopServersByGroupWithTimeout(stopServersByGroupWithTimeoutRequest { + this.groupName = groupName + this.stopCause = stopCause + this.timeoutSeconds = timeoutSeconds + }).serversList.map { + Server.fromDefinition(it) + } + } + override suspend fun updateServerState(id: String, state: ServerState): Server { return Server.fromDefinition( serverServiceStub.updateServerState( diff --git a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt index c106d59..ee81b4b 100644 --- a/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt +++ b/controller-api/src/main/kotlin/app/simplecloud/controller/api/impl/future/ServerApiFutureImpl.kt @@ -2,10 +2,10 @@ package app.simplecloud.controller.api.impl.future import app.simplecloud.controller.api.ServerApi import app.simplecloud.controller.shared.group.Group -import build.buf.gen.simplecloud.controller.v1.* import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials import app.simplecloud.droplet.api.future.toCompletable +import build.buf.gen.simplecloud.controller.v1.* import io.grpc.ManagedChannel import java.util.concurrent.CompletableFuture @@ -79,7 +79,11 @@ class ServerApiFutureImpl( } } - override fun stopServer(groupName: String, numericalId: Long, stopCause: ServerStopCause): CompletableFuture { + override fun stopServer( + groupName: String, + numericalId: Long, + stopCause: ServerStopCause + ): CompletableFuture { return serverServiceStub.stopServerByNumerical( StopServerByNumericalRequest.newBuilder() .setGroupName(groupName) @@ -102,6 +106,33 @@ class ServerApiFutureImpl( } } + override fun stopServers(groupName: String, stopCause: ServerStopCause): CompletableFuture> { + return serverServiceStub.stopServersByGroup( + StopServersByGroupRequest.newBuilder() + .setGroupName(groupName) + .setStopCause(stopCause) + .build() + ).toCompletable().thenApply { + Server.fromDefinition(it.serversList) + } + } + + override fun stopServers( + groupName: String, + timeoutSeconds: Int, + stopCause: ServerStopCause + ): CompletableFuture> { + return serverServiceStub.stopServersByGroupWithTimeout( + StopServersByGroupWithTimeoutRequest.newBuilder() + .setGroupName(groupName) + .setStopCause(stopCause) + .setTimeoutSeconds(timeoutSeconds) + .build() + ).toCompletable().thenApply { + Server.fromDefinition(it.serversList) + } + } + override fun updateServerState(id: String, state: ServerState): CompletableFuture { return serverServiceStub.updateServerState( UpdateServerStateRequest.newBuilder() diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt index dc83558..1a61195 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/oauth/AuthenticationHandler.kt @@ -130,7 +130,8 @@ class AuthenticationHandler( call.respond(HttpStatusCode.NotFound, "User not found") return } - call.respond(mapOf( + call.respond( + mapOf( "user_id" to user.userId, "username" to user.username, "scope" to user.scopes.joinToString(" "), diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt index 8b562cb..58e97c5 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/reconciler/GroupReconciler.kt @@ -99,8 +99,14 @@ class GroupReconciler( private suspend fun startServers() { val available = serverHostRepository.areServerHostsAvailable() - if(!available) return - if(isNewServerNeeded()) + if (!available) return + group.timeout?.let { + if (it.isCooldownActive()) { + return + } + } + + if (isNewServerNeeded()) startServer() } diff --git a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt index bda363f..9306a0e 100644 --- a/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt +++ b/controller-runtime/src/main/kotlin/app/simplecloud/controller/runtime/server/ServerService.kt @@ -3,6 +3,7 @@ package app.simplecloud.controller.runtime.server import app.simplecloud.controller.runtime.group.GroupRepository import app.simplecloud.controller.runtime.host.ServerHostRepository import app.simplecloud.controller.shared.group.Group +import app.simplecloud.controller.shared.group.GroupTimeout import app.simplecloud.controller.shared.host.ServerHost import app.simplecloud.controller.shared.server.Server import app.simplecloud.droplet.api.auth.AuthCallCredentials @@ -198,6 +199,43 @@ class ServerService( } } + override suspend fun stopServersByGroupWithTimeout(request: StopServersByGroupWithTimeoutRequest): StopServersByGroupResponse { + return stopServersByGroup(request.groupName, request.timeoutSeconds, request.stopCause) + } + + override suspend fun stopServersByGroup(request: StopServersByGroupRequest): StopServersByGroupResponse { + return stopServersByGroup(request.groupName, null, request.stopCause) + } + + private suspend fun stopServersByGroup( + groupName: String, + timeout: Int?, + cause: ServerStopCause = ServerStopCause.NATURAL_STOP + ): StopServersByGroupResponse { + val group = groupRepository.find(groupName) + ?: throw StatusException(Status.NOT_FOUND.withDescription("No group was found matching this name. $groupName")) + val groupServers = serverRepository.findServersByGroup(group.name) + if (groupServers.isEmpty()) { + throw StatusException(Status.NOT_FOUND.withDescription("No server was found matching this group name. ${group.name}")) + } + + val serverDefinitionList = mutableListOf() + + try { + timeout?.let { + group.timeout = GroupTimeout(it); + } + + groupServers.forEach { server -> + serverDefinitionList.add(stopServer(server.toDefinition(), cause)) + } + + return stopServersByGroupResponse { servers.addAll(serverDefinitionList) } + } catch (e: Exception) { + throw StatusException(Status.INTERNAL.withDescription("Error whilst stopping server by group").withCause(e)) + } + } + private suspend fun stopServer( server: ServerDefinition, cause: ServerStopCause = ServerStopCause.NATURAL_STOP diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt index 5346678..0cff3cc 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/Group.kt @@ -18,6 +18,9 @@ data class Group( val properties: Map = mutableMapOf() ) { + @Transient + var timeout: GroupTimeout? = null + fun toDefinition(): GroupDefinition { return GroupDefinition.newBuilder() .setName(name) diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt new file mode 100644 index 0000000..5e216b4 --- /dev/null +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/group/GroupTimeout.kt @@ -0,0 +1,16 @@ +package app.simplecloud.controller.shared.group + +data class GroupTimeout(val timeoutDuration: Int, val timeoutBegin: Long = System.currentTimeMillis()) { + + fun isCooldownActive(): Boolean { + val cooldownEndTime = timeoutBegin + timeoutDuration * 1000L + return System.currentTimeMillis() < cooldownEndTime + } + + fun remainingTimeInSeconds(): Long { + val cooldownEndTime = timeoutBegin + timeoutDuration * 1000L + val remainingTime = cooldownEndTime - System.currentTimeMillis() + return if (remainingTime > 0) remainingTime / 1000 else 0 + } + +} \ No newline at end of file diff --git a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt index 8e7bbf6..cce4dcd 100644 --- a/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt +++ b/controller-shared/src/main/kotlin/app/simplecloud/controller/shared/server/Server.kt @@ -4,9 +4,7 @@ import app.simplecloud.droplet.api.time.ProtobufTimestamp import build.buf.gen.simplecloud.controller.v1.ServerDefinition import build.buf.gen.simplecloud.controller.v1.ServerState import build.buf.gen.simplecloud.controller.v1.ServerType -import java.time.Instant import java.time.LocalDateTime -import java.time.ZoneId data class Server( val uniqueId: String, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0b906c..c076a24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.0.20" kotlin-coroutines = "1.9.0" shadow = "8.3.3" log4j = "2.20.0" -droplet-api = "0.0.1-dev.7e049d6" +droplet-api = "0.0.1-dev.cf07a34" simplecloud-pubsub = "1.0.5" simplecloud-metrics = "1.0.0" jooq = "3.19.3" @@ -24,7 +24,6 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } - simplecloud-droplet-api = { module = "app.simplecloud.droplet.api:droplet-api", version.ref = "droplet-api" } simplecloud-pubsub = { module = "app.simplecloud:simplecloud-pubsub", version.ref = "simplecloud-pubsub" } simplecloud-metrics = { module = "app.simplecloud:internal-metrics-api", version.ref = "simplecloud-metrics" }