diff --git a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/AbstractResourcePatcher.kt b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/AbstractResourcePatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..daa873f17e4dba97b896fd455121a0e1c01e7b81 --- /dev/null +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/AbstractResourcePatcher.kt @@ -0,0 +1,63 @@ +package rocks.theodolite.kubernetes.patcher + +import io.fabric8.kubernetes.api.model.Container +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.Quantity +import io.fabric8.kubernetes.api.model.apps.Deployment +import io.fabric8.kubernetes.api.model.apps.StatefulSet +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +/** + * Abstract [Patcher] to set resource limits or requests to Deployments and StatefulSets. + * + * @param container Container to be patched. + * @param requiredResource The resource to be requested or limited (e.g., **cpu** or **memory**) + * @param format Format add to the provided value (e.g., `GBi` or `m`, see [Quantity]). + * @param factor A factor to multiply the provided value with. + */ +abstract class AbstractResourcePatcher( + private val container: String, + protected val requiredResource: String, + private val format: String? = null, + private val factor: Int? = null +) : AbstractPatcher() { + + override fun patchSingleResource(resource: HasMetadata, value: String): HasMetadata { + when (resource) { + is Deployment -> { + resource.spec.template.spec.containers.filter { it.name == container }.forEach { + setLimits(it, value) + } + } + is StatefulSet -> { + resource.spec.template.spec.containers.filter { it.name == container }.forEach { + setLimits(it, value) + } + } + else -> { + throw InvalidPatcherConfigurationException("ResourceLimitPatcher is not applicable for $resource.") + } + } + return resource + } + + private fun setLimits(container: Container, value: String) { + val quantity = if (this.format != null || this.factor != null) { + val amountAsInt = value.toIntOrNull()?.times(this.factor ?: 1) + if (amountAsInt == null) { + logger.warn { "Patcher value cannot be parsed as Int. Ignoring quantity format and factor." } + Quantity(value) + } else { + Quantity(amountAsInt.toString(), format ?: "") + } + } else { + Quantity(value) + } + + setLimits(container, quantity) + } + + abstract fun setLimits(container: Container, quantity: Quantity) +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt index a4dcf68d2b4ec12facb26755e9f63e298725e195..10e9319660e6e6c49affc4b992f2b292f689f8da 100644 --- a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt @@ -50,11 +50,15 @@ class PatcherFactory { ) "ResourceLimitPatcher" -> ResourceLimitPatcher( container = patcherDefinition.properties["container"]!!, - limitedResource = patcherDefinition.properties["limitedResource"]!! + limitedResource = patcherDefinition.properties["limitedResource"]!!, + format = patcherDefinition.properties["format"], + factor = patcherDefinition.properties["factor"]?.toInt() ) "ResourceRequestPatcher" -> ResourceRequestPatcher( container = patcherDefinition.properties["container"]!!, - requestedResource = patcherDefinition.properties["requestedResource"]!! + requestedResource = patcherDefinition.properties["requestedResource"]!!, + format = patcherDefinition.properties["format"], + factor = patcherDefinition.properties["factor"]?.toInt() ) "SchedulerNamePatcher" -> SchedulerNamePatcher() "LabelPatcher" -> LabelPatcher( diff --git a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcher.kt b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcher.kt index c8064c605fbd8c65d97d9fbd8ee24fd49ad893da..c1ae22f00a8fde16aedef6b70ea098cbdd895dd4 100644 --- a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcher.kt +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcher.kt @@ -1,57 +1,43 @@ package rocks.theodolite.kubernetes.patcher -import io.fabric8.kubernetes.api.model.* -import io.fabric8.kubernetes.api.model.apps.Deployment -import io.fabric8.kubernetes.api.model.apps.StatefulSet +import io.fabric8.kubernetes.api.model.Container +import io.fabric8.kubernetes.api.model.Quantity +import io.fabric8.kubernetes.api.model.ResourceRequirements /** * The Resource limit [Patcher] set resource limits for deployments and statefulSets. + * The Resource limit [Patcher] sets resource limits for Deployments and StatefulSets. * - * @param k8sResource Kubernetes resource to be patched. * @param container Container to be patched. - * @param limitedResource The resource to be limited (e.g. **cpu or memory**) + * @param limitedResource The resource to be limited (e.g., **cpu** or **memory**) + * @param format Format add to the provided value (e.g., `GBi` or `m`, see [Quantity]). + * @param factor A factor to multiply the provided value with. */ class ResourceLimitPatcher( - private val container: String, - private val limitedResource: String -) : AbstractPatcher() { - - override fun patchSingleResource(resource: HasMetadata, value: String): HasMetadata { - when (resource) { - is Deployment -> { - resource.spec.template.spec.containers.filter { it.name == container }.forEach { - setLimits(it, value) - } + container: String, + limitedResource: String, + format: String? = null, + factor: Int? = null +) : AbstractResourcePatcher( + container = container, + requiredResource = limitedResource, + format = format, + factor = factor +) { + override fun setLimits(container: Container, quantity: Quantity) { + when { + container.resources == null -> { + val resource = ResourceRequirements() + resource.limits = mapOf(requiredResource to quantity) + container.resources = resource } - is StatefulSet -> { - resource.spec.template.spec.containers.filter { it.name == container }.forEach { - setLimits(it, value) - } + container.resources.limits.isEmpty() -> { + container.resources.limits = mapOf(requiredResource to quantity) } else -> { - throw InvalidPatcherConfigurationException("ResourceLimitPatcher is not applicable for $resource.") + container.resources.limits[requiredResource] = quantity } } - return resource } - - private fun setLimits(container: Container, value: String) { - when { - container.resources == null -> { - val resource = ResourceRequirements() - resource.limits = mapOf(limitedResource to Quantity(value)) - container.resources = resource - } - container.resources.limits.isEmpty() -> { - container.resources.limits = mapOf(limitedResource to Quantity(value)) - } - else -> { - val values = mutableMapOf<String, Quantity>() - container.resources.limits.forEach { entry -> values[entry.key] = entry.value } - values[limitedResource] = Quantity(value) - container.resources.limits = values - } - } - } } diff --git a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcher.kt b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcher.kt index 7d9cd8b748d9e41d5506508259452032b1228015..a6b037ddeb3a1fb3d7aa2c1f2579cada81922d9a 100644 --- a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcher.kt +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcher.kt @@ -1,55 +1,44 @@ package rocks.theodolite.kubernetes.patcher -import io.fabric8.kubernetes.api.model.* -import io.fabric8.kubernetes.api.model.apps.Deployment -import io.fabric8.kubernetes.api.model.apps.StatefulSet +import io.fabric8.kubernetes.api.model.Container +import io.fabric8.kubernetes.api.model.Quantity +import io.fabric8.kubernetes.api.model.ResourceRequirements + /** - * The Resource request [Patcher] set resource limits for deployments and statefulSets. + * The Resource request [Patcher] sets resource requests for Deployments and StatefulSets. * * @param container Container to be patched. - * @param requestedResource The resource to be requested (e.g. **cpu or memory**) + * @param requestedResource The resource to be requested (e.g., **cpu** or **memory**) + * @param format Format add to the provided value (e.g., `GBi` or `m`, see [Quantity]). + * @param factor A factor to multiply the provided value with. */ class ResourceRequestPatcher( - private val container: String, - private val requestedResource: String -) : AbstractPatcher() { - + container: String, + requestedResource: String, + format: String? = null, + factor: Int? = null +) : AbstractResourcePatcher( + container = container, + requiredResource = requestedResource, + format = format, + factor = factor +) { - override fun patchSingleResource(resource: HasMetadata, value: String): HasMetadata { - when (resource) { - is Deployment -> { - resource.spec.template.spec.containers.filter { it.name == container }.forEach { - setRequests(it, value) - } - } - is StatefulSet -> { - resource.spec.template.spec.containers.filter { it.name == container }.forEach { - setRequests(it, value) - } - } - else -> { - throw InvalidPatcherConfigurationException("ResourceRequestPatcher is not applicable for $resource.") - } - } - return resource - } - private fun setRequests(container: Container, value: String) { + override fun setLimits(container: Container, quantity: Quantity) { when { container.resources == null -> { val resource = ResourceRequirements() - resource.requests = mapOf(requestedResource to Quantity(value)) + resource.requests = mapOf(requiredResource to quantity) container.resources = resource } container.resources.requests.isEmpty() -> { - container.resources.requests = mapOf(requestedResource to Quantity(value)) + container.resources.requests = mapOf(requiredResource to quantity) } else -> { - val values = mutableMapOf<String, Quantity>() - container.resources.requests.forEach { entry -> values[entry.key] = entry.value } - values[requestedResource] = Quantity(value) - container.resources.requests = values + container.resources.requests[requiredResource] = quantity } } } + } diff --git a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcherTest.kt b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcherTest.kt index b0af74d1e207ee10fac548f27267356711943dd0..86de40e207950657a1e6ebf1114dff51f1c2222e 100644 --- a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcherTest.kt +++ b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceLimitPatcherTest.kt @@ -1,27 +1,15 @@ package rocks.theodolite.kubernetes.patcher -import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.apps.Deployment import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest import io.quarkus.test.kubernetes.client.KubernetesTestServer import io.quarkus.test.kubernetes.client.WithKubernetesTestServer -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test - -/** - * Resource patcher test - * - * This class tested 4 scenarios for the ResourceLimitPatcher and the ResourceRequestPatcher. - * The different test cases specifies four possible situations: - * Case 1: In the given YAML declaration memory and cpu are defined - * Case 2: In the given YAML declaration only cpu is defined - * Case 3: In the given YAML declaration only memory is defined - * Case 4: In the given YAML declaration neither `Resource Request` nor `Request Limit` is defined - */ @QuarkusTest @WithKubernetesTestServer -@Disabled class ResourceLimitPatcherTest { @KubernetesTestServer @@ -30,14 +18,14 @@ class ResourceLimitPatcherTest { fun applyTest(fileName: String) { val cpuValue = "50m" val memValue = "3Gi" - val k8sResource = server.client.apps().deployments().load(javaClass.getResourceAsStream(fileName)).get() + val k8sResource = getDeployment(fileName) val defCPU = PatcherDefinition() defCPU.resource = "/cpu-memory-deployment.yaml" defCPU.type = "ResourceLimitPatcher" defCPU.properties = mapOf( "limitedResource" to "cpu", - "container" to "application" + "container" to "uc-application" ) val defMEM = PatcherDefinition() @@ -48,13 +36,13 @@ class ResourceLimitPatcherTest { "container" to "uc-application" ) - PatchHandler.patchResource(mutableMapOf(Pair("cpu-memory-deployment.yaml", listOf(k8sResource as HasMetadata))), defCPU, cpuValue) - PatchHandler.patchResource(mutableMapOf(Pair("cpu-memory-deployment.yaml", listOf(k8sResource as HasMetadata))), defMEM, memValue) - - k8sResource.spec.template.spec.containers.filter { it.name == defCPU.properties["container"]!! } + val firstPatched = PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", listOf(k8sResource))), defCPU, cpuValue) + val finalPatched = PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", firstPatched)), defMEM, memValue) + assertEquals(1, finalPatched.size) + (finalPatched[0] as Deployment).spec.template.spec.containers.filter { it.name == defCPU.properties["container"]!! } .forEach { - assertTrue(it.resources.limits["cpu"].toString() == cpuValue) - assertTrue(it.resources.limits["memory"].toString() == memValue) + assertEquals(cpuValue, it.resources.limits["cpu"].toString()) + assertEquals(memValue, it.resources.limits["memory"].toString()) } } @@ -81,4 +69,80 @@ class ResourceLimitPatcherTest { // Case 4: In the given YAML declaration neither `Resource Request` nor `Request Limit` is defined applyTest("/no-resources-deployment.yaml") } + + @Test + fun testWithNoFactorSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceLimitPatcher( + "uc-application", + "memory" + ).patch(listOf(initialDeployment), "1Gi") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("1Gi", it.resources.limits["memory"].toString()) + } + } + + @Test + fun testWithFormatSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceLimitPatcher( + "uc-application", + "memory", + format = "GBi" + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("2GBi", it.resources.limits["memory"].toString()) + } + } + + @Test + fun testWithFactorSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceLimitPatcher( + "uc-application", + "memory", + factor = 4000 + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("8000", it.resources.limits["memory"].toString()) + } + } + + @Test + fun testWithFactorAndFormatSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceLimitPatcher( + "uc-application", + "memory", + format = "GBi", + factor = 4, + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("8GBi", it.resources.limits["memory"].toString()) + } + } + + private fun getDeployment(fileName: String): Deployment { + return server.client.apps().deployments().load(javaClass.getResourceAsStream(fileName)).get() + } } diff --git a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcherTest.kt b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcherTest.kt index a076e541e742e97ffa95dccff925892dd63ff17a..415d92f59f8ca0fd5a331ae0fa5bc71b5ef4f506 100644 --- a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcherTest.kt +++ b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/ResourceRequestPatcherTest.kt @@ -1,22 +1,12 @@ package rocks.theodolite.kubernetes.patcher +import io.fabric8.kubernetes.api.model.apps.Deployment import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest import io.quarkus.test.kubernetes.client.KubernetesTestServer import io.quarkus.test.kubernetes.client.WithKubernetesTestServer -import io.smallrye.common.constraint.Assert.assertTrue +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test - -/** - * Resource patcher test - * - * This class tested 4 scenarios for the ResourceLimitPatcher and the ResourceRequestPatcher. - * The different test cases specifies four possible situations: - * Case 1: In the given YAML declaration memory and cpu are defined - * Case 2: In the given YAML declaration only cpu is defined - * Case 3: In the given YAML declaration only memory is defined - * Case 4: In the given YAML declaration neither `Resource Request` nor `Request Limit` is defined - */ @QuarkusTest @WithKubernetesTestServer class ResourceRequestPatcherTest { @@ -27,14 +17,14 @@ class ResourceRequestPatcherTest { fun applyTest(fileName: String) { val cpuValue = "50m" val memValue = "3Gi" - val k8sResource = server.client.apps().deployments().load(javaClass.getResourceAsStream(fileName)).get() + val k8sResource = getDeployment(fileName) val defCPU = PatcherDefinition() defCPU.resource = "/cpu-memory-deployment.yaml" defCPU.type = "ResourceRequestPatcher" defCPU.properties = mapOf( "requestedResource" to "cpu", - "container" to "application" + "container" to "uc-application" ) val defMEM = PatcherDefinition() @@ -42,16 +32,16 @@ class ResourceRequestPatcherTest { defMEM.type = "ResourceRequestPatcher" defMEM.properties = mapOf( "requestedResource" to "memory", - "container" to "application" + "container" to "uc-application" ) - PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", listOf(k8sResource))), defCPU, cpuValue) - PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", listOf(k8sResource))), defMEM, memValue) - - k8sResource.spec.template.spec.containers.filter { it.name == defCPU.properties["container"]!! } + val firstPatched = PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", listOf(k8sResource))), defCPU, cpuValue) + val finalPatched = PatchHandler.patchResource(mutableMapOf(Pair("/cpu-memory-deployment.yaml", firstPatched)), defMEM, memValue) + assertEquals(1, finalPatched.size) + (finalPatched[0] as Deployment).spec.template.spec.containers.filter { it.name == defCPU.properties["container"]!! } .forEach { - assertTrue(it.resources.requests["cpu"].toString() == cpuValue) - assertTrue(it.resources.requests["memory"].toString() == memValue) + assertEquals(cpuValue, it.resources.requests["cpu"].toString()) + assertEquals(memValue, it.resources.requests["memory"].toString()) } } @@ -78,4 +68,81 @@ class ResourceRequestPatcherTest { // Case 4: In the given YAML declaration neither `Resource Request` nor `Request Limit` is defined applyTest("/no-resources-deployment.yaml") } + + @Test + fun testWithNoFactorSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceRequestPatcher( + "uc-application", + "memory" + ).patch(listOf(initialDeployment), "1Gi") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("1Gi", it.resources.requests["memory"].toString()) + } + } + + @Test + fun testWithFormatSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceRequestPatcher( + "uc-application", + "memory", + format = "GBi" + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("2GBi", it.resources.requests["memory"].toString()) + } + } + + @Test + fun testWithFactorSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceRequestPatcher( + "uc-application", + "memory", + factor = 4000 + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("8000", it.resources.requests["memory"].toString()) + } + } + + @Test + fun testWithFactorAndFormatSet() { + val initialDeployment = getDeployment("/cpu-memory-deployment.yaml") + val patchedDeployments = ResourceRequestPatcher( + "uc-application", + "memory", + format = "GBi", + factor = 4, + ).patch(listOf(initialDeployment), "2") + assertEquals(1, patchedDeployments.size) + val patchedDeployment = patchedDeployments[0] as Deployment + + val containers = patchedDeployment.spec.template.spec.containers.filter { it.name == "uc-application" } + assertEquals(1, containers.size) + containers.forEach { + assertEquals("8GBi", it.resources.requests["memory"].toString()) + } + } + + private fun getDeployment(fileName: String): Deployment { + return server.client.apps().deployments().load(javaClass.getResourceAsStream(fileName)).get() + } + } \ No newline at end of file