diff --git a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcher.kt b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a897482a63b748930bf68710cbe34a5647767b2 --- /dev/null +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcher.kt @@ -0,0 +1,61 @@ +package rocks.theodolite.kubernetes.patcher + +import com.fasterxml.jackson.annotation.JsonTypeName +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.databind.node.JsonNodeType +import io.fabric8.kubernetes.api.model.GenericKubernetesResource +import io.fabric8.kubernetes.api.model.HasMetadata +import org.json.JSONString + +/** + * Patches an arbitrary field in a [GenericKubernetesResource]. + * + * @param path Path as List of Strings and Ints to the field to be patched. + */ +class GenericResourcePatcher(val path: List<Any>, val type: Type = Type.STRING) : AbstractStringPatcher() { + + override fun patchSingleResource(resource: HasMetadata, value: String): HasMetadata { + + if (resource is GenericKubernetesResource) { + val castedValue = when (type) { + Type.STRING -> value + Type.BOOLEAN -> value.toBoolean() + Type.NUMBER -> value.toDouble() + Type.INTEGER -> value.toInt() + } + var current: Any? = resource.additionalProperties + for (segment in path.dropLast(1)) { + current = if (segment is Int && current is MutableList<*> && current.size > segment) { + current.toTypedArray()[segment] + } else if (segment is String && current is Map<*, *>) { + current[segment] + } else { + throw IllegalArgumentException("Provided path is invalid") + } + } + val segment = path.lastOrNull() + if (segment == null) { + throw IllegalArgumentException("Path must not be empty") + } else if (segment is Int && current is MutableList<*> && current.size > segment) { + (current as MutableList<Any?>)[segment] = castedValue + } else if (segment is String && current is Map<*, *>) { + (current as MutableMap<String, Any>)[segment] = castedValue + } else { + throw IllegalArgumentException("Cannot set value for path") + } + } + return resource + } + + enum class Type(val value: String) { + STRING("string"), + BOOLEAN("boolean"), + NUMBER("number"), + INTEGER("integer"); + + companion object { + fun from(type: String): Type = + values().find { it.value == type } ?: throw IllegalArgumentException("Requested Type does not exist") + } + } +} 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 f469eaee39e5e2eb9e5aa0ae2bd8346eabf3006d..238e7caa0ad70ca321f134dd36ffd7d3c16871b4 100644 --- a/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt +++ b/theodolite/src/main/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactory.kt @@ -94,6 +94,12 @@ class PatcherFactory { "VolumesConfigMapPatcher" -> VolumesConfigMapPatcher( volumeName = patcher.properties["volumeName"] ?: throwInvalid(patcher) ) + "GenericResourcePatcher" -> GenericResourcePatcher( + path = (patcher.properties["path"] ?: throwInvalid(patcher)) + .split("/") + .map { it.toIntOrNull() ?: it }, + type = patcher.properties["type"]?.let { GenericResourcePatcher.Type.from(it) } ?: GenericResourcePatcher.Type.STRING + ) else -> throw InvalidPatcherConfigurationException("Patcher type ${patcher.type} not found.") } } diff --git a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcherTest.kt b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcherTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..236a22bfa8cfb50973536cd4ac0f9fd7db9375b1 --- /dev/null +++ b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/GenericResourcePatcherTest.kt @@ -0,0 +1,109 @@ +package rocks.theodolite.kubernetes.patcher + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext +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.* +import org.junit.jupiter.api.Test +import registerResource + +@QuarkusTest +@WithKubernetesTestServer +internal class GenericResourcePatcherTest { + + @KubernetesTestServer + private lateinit var server: KubernetesServer + + @Test + fun testPatchString() { + val sourceResource = listOf(createServiceMonitor()) + val patcher = GenericResourcePatcher(listOf("spec", "endpoints", 0, "interval")) + val patchedResource = patcher.patch(sourceResource, "20s") + patchedResource.forEach { + assertEquals( + "20s", + ((((it as GenericKubernetesResource) + .additionalProperties["spec"] as Map<String, Any>) + ["endpoints"] as List<Any>) + [0] as Map<String, Any>) + ["interval"]) + } + } + + @Test + fun testPatchBoolean() { + val sourceResource = listOf(createServiceMonitor()) + val patcher = GenericResourcePatcher(listOf("spec", "endpoints", 0, "honorTimestamps"), GenericResourcePatcher.Type.BOOLEAN) + val patchedResource = patcher.patch(sourceResource, "true") + patchedResource.forEach { + assertEquals( + true, + ((((it as GenericKubernetesResource) + .additionalProperties["spec"] as Map<String, Any>) + ["endpoints"] as List<Any>) + [0] as Map<String, Any>) + ["honorTimestamps"]) + } + } + + @Test + fun testPatchInteger() { + val sourceResource = listOf(createServiceMonitor()) + val patcher = GenericResourcePatcher(listOf("spec", "labelLimit"), GenericResourcePatcher.Type.INTEGER) + val patchedResource = patcher.patch(sourceResource, "11") + patchedResource.forEach { + assertEquals( + 11, + ((it as GenericKubernetesResource) + .additionalProperties["spec"] as Map<String, Any>) + ["labelLimit"]) + } + } + + @Test + fun testPatchNumber() { + val sourceResource = listOf(createServiceMonitor()) + val patcher = GenericResourcePatcher(listOf("spec", "myMadeUpProp"), GenericResourcePatcher.Type.NUMBER) + val patchedResource = patcher.patch(sourceResource, "11.2") + patchedResource.forEach { + assertEquals( + 11.2, + ((it as GenericKubernetesResource) + .additionalProperties["spec"] as Map<String, Any>) + ["labelLimit"]) + } + } + + @Test + fun testPatchNumberWithoutDecimals() { + val sourceResource = listOf(createServiceMonitor()) + val patcher = GenericResourcePatcher(listOf("spec", "myMadeUpProp"), GenericResourcePatcher.Type.NUMBER) + val patchedResource = patcher.patch(sourceResource, "11") + patchedResource.forEach { + assertEquals( + 11.0, + ((it as GenericKubernetesResource) + .additionalProperties["spec"] as Map<String, Any>) + ["labelLimit"]) + } + } + + fun createServiceMonitor(): HasMetadata { + val serviceMonitorContext = ResourceDefinitionContext.Builder() + .withGroup("monitoring.coreos.com") + .withKind("ServiceMonitor") + .withPlural("servicemonitors") + .withNamespaced(true) + .withVersion("v1") + .build() + server.registerResource(serviceMonitorContext) + + val serviceMonitorStream = javaClass.getResourceAsStream("/k8s-resource-files/test-service-monitor.yaml") + return server.client.load(serviceMonitorStream).get()[0] + } +} diff --git a/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactoryTest.kt b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..19d7c6cb3997d93d62908ebf4646f52d12b3d6d5 --- /dev/null +++ b/theodolite/src/test/kotlin/rocks/theodolite/kubernetes/patcher/PatcherFactoryTest.kt @@ -0,0 +1,81 @@ +package rocks.theodolite.kubernetes.patcher + +import io.quarkus.test.junit.QuarkusTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@QuarkusTest +internal class PatcherFactoryTest { + + @Test + fun testGenericResourcePatcherWithoutType() { + val patcherDefinition = PatcherDefinition() + patcherDefinition.type = "GenericResourcePatcher" + patcherDefinition.properties = mapOf( + "path" to "some/path/123/toSomeField" + ) + val patcher = PatcherFactory.createPatcher(patcherDefinition) + assertTrue(patcher is GenericResourcePatcher) + val castedPatcher = patcher as GenericResourcePatcher + assertEquals(listOf("some", "path", 123, "toSomeField"), castedPatcher.path) + assertEquals(GenericResourcePatcher.Type.STRING, castedPatcher.type) + } + + @Test + fun testGenericResourcePatcherWithStringType() { + val patcherDefinition = PatcherDefinition() + patcherDefinition.type = "GenericResourcePatcher" + patcherDefinition.properties = mapOf( + "path" to "spec", + "type" to "string" + ) + val patcher = PatcherFactory.createPatcher(patcherDefinition) + assertTrue(patcher is GenericResourcePatcher) + val castedPatcher = patcher as GenericResourcePatcher + assertEquals(GenericResourcePatcher.Type.STRING, castedPatcher.type) + } + + @Test + fun testGenericResourcePatcherWithBooleanType() { + val patcherDefinition = PatcherDefinition() + patcherDefinition.type = "GenericResourcePatcher" + patcherDefinition.properties = mapOf( + "path" to "spec", + "type" to "boolean" + ) + val patcher = PatcherFactory.createPatcher(patcherDefinition) + assertTrue(patcher is GenericResourcePatcher) + val castedPatcher = patcher as GenericResourcePatcher + assertEquals(GenericResourcePatcher.Type.BOOLEAN, castedPatcher.type) + } + + @Test + fun testGenericResourcePatcherWithIntegerType() { + val patcherDefinition = PatcherDefinition() + patcherDefinition.type = "GenericResourcePatcher" + patcherDefinition.properties = mapOf( + "path" to "spec", + "type" to "integer" + ) + val patcher = PatcherFactory.createPatcher(patcherDefinition) + assertTrue(patcher is GenericResourcePatcher) + val castedPatcher = patcher as GenericResourcePatcher + assertEquals(GenericResourcePatcher.Type.INTEGER, castedPatcher.type) + } + + @Test + fun testGenericResourcePatcherWithNumberType() { + val patcherDefinition = PatcherDefinition() + patcherDefinition.type = "GenericResourcePatcher" + patcherDefinition.properties = mapOf( + "path" to "spec", + "type" to "number" + ) + val patcher = PatcherFactory.createPatcher(patcherDefinition) + assertTrue(patcher is GenericResourcePatcher) + val castedPatcher = patcher as GenericResourcePatcher + assertEquals(GenericResourcePatcher.Type.NUMBER, castedPatcher.type) + } + +} \ No newline at end of file diff --git a/theodolite/src/test/resources/k8s-resource-files/test-service-monitor.yaml b/theodolite/src/test/resources/k8s-resource-files/test-service-monitor.yaml index e8a0e52e15245e790adf2cbf84edb517754267be..7afe8fe71e38ddd5e53116759a250d97b20af9ac 100644 --- a/theodolite/src/test/resources/k8s-resource-files/test-service-monitor.yaml +++ b/theodolite/src/test/resources/k8s-resource-files/test-service-monitor.yaml @@ -4,4 +4,11 @@ metadata: labels: app: titan-ccp-aggregation appScope: titan-ccp - name: test-service-monitor \ No newline at end of file + name: test-service-monitor +spec: + selector: + matchLabels: + app: flink + endpoints: + - port: metrics + interval: 10s \ No newline at end of file