diff --git a/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/kube/KubeClient.java b/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/kube/KubeClient.java index 3452c8f7..0f6b6c8e 100644 --- a/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/kube/KubeClient.java +++ b/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/kube/KubeClient.java @@ -19,6 +19,7 @@ import io.yupiik.bundlebee.core.http.JsonHttpResponse; import io.yupiik.bundlebee.core.lang.ConfigHolder; import io.yupiik.bundlebee.core.qualifier.BundleBee; +import io.yupiik.bundlebee.core.service.ContainerSanitizer; import io.yupiik.bundlebee.core.yaml.Yaml2JsonConverter; import lombok.Data; import lombok.extern.java.Log; @@ -79,6 +80,9 @@ public class KubeClient implements ConfigHolder { @Inject private Yaml2JsonConverter yaml2json; + @Inject + private ContainerSanitizer containerSanitizer; + @Inject private ApiPreloader apiPreloader; @@ -487,9 +491,16 @@ private CompletionStage> doApply(final JsonObject rawDesc, return completedStage(findResponse); } + final JsonObject preparedAndFilteredDescriptor; + if (findResponse.statusCode() == 404 && containerSanitizer.canSanitizeCpuResource(kindLowerCased)) { + preparedAndFilteredDescriptor = containerSanitizer.dropCpuResources(kindLowerCased, preparedDesc); + } else { + preparedAndFilteredDescriptor = preparedDesc; + } + log.finest(() -> name + " (" + kindLowerCased + ") does not exist, creating it"); return api.execute(HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(preparedDesc.toString())) + .POST(HttpRequest.BodyPublishers.ofString(preparedAndFilteredDescriptor.toString())) .header("Content-Type", "application/json") .header("Accept", "application/json"), baseUri + fieldManager) diff --git a/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/service/ContainerSanitizer.java b/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/service/ContainerSanitizer.java new file mode 100644 index 00000000..5650420c --- /dev/null +++ b/bundlebee-core/src/main/java/io/yupiik/bundlebee/core/service/ContainerSanitizer.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2021 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.bundlebee.core.service; + +import io.yupiik.bundlebee.core.qualifier.BundleBee; +import lombok.extern.java.Log; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.JsonArrayBuilder; +import javax.json.JsonBuilderFactory; +import javax.json.JsonException; +import javax.json.JsonObject; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import javax.json.spi.JsonProvider; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import static java.util.logging.Level.FINEST; +import static java.util.stream.Collectors.toMap; + +@Log +@ApplicationScoped +public class ContainerSanitizer { + @Inject + @BundleBee + private JsonProvider jsonProvider; + + @Inject + @BundleBee + private JsonBuilderFactory jsonBuilderFactory; + + public boolean canSanitizeCpuResource(final String kindLowerCased) { + return "cronjobs".equals(kindLowerCased) || "deployments".equals(kindLowerCased) || + "daemonsets".equals(kindLowerCased) || "pods".equals(kindLowerCased) || "jobs".equals(kindLowerCased); + } + + // for first installation if cpu value is null then it is considered as being 0 - merge patch are ok after + public JsonObject dropCpuResources(final String kind, final JsonObject preparedDesc) { + final String containersParentPointer; + switch (kind) { + case "deployments": + case "daemonsets": + case "jobs": + containersParentPointer = "/spec/template/spec"; + break; + case "cronjobs": + containersParentPointer = "/spec/jobTemplate/spec/template/spec"; + break; + case "pods": + containersParentPointer = "/spec"; + break; + default: + containersParentPointer = null; + } + + return replaceIfPresent( + replaceIfPresent(preparedDesc, containersParentPointer, "initContainers", this::dropNullCpu), + containersParentPointer, "containers", this::dropNullCpu); + } + + private JsonValue dropNullCpu(final JsonObject container) { + final var resources = container.get("resources"); + if (resources == null) { + return container; + } + + final var resourcesObj = resources.asJsonObject(); + if (!resourcesObj.containsKey("requests") && !resourcesObj.containsKey("limits")) { + return container; + } + + final var builder = jsonBuilderFactory.createObjectBuilder(resourcesObj.entrySet().stream() + .filter(it -> !"requests".equals(it.getKey()) && !"limits".equals(it.getKey())) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))); + Stream.of("requests", "limits") + .filter(resourcesObj::containsKey) + .forEach(k -> { + final var subObj = resourcesObj.get(k).asJsonObject(); + if (!JsonValue.NULL.equals(subObj.get("cpu"))) { + builder.add(k, subObj); + } else { + final var value = jsonBuilderFactory.createObjectBuilder(subObj + .entrySet().stream() + .filter(it -> !"cpu".equals(it.getKey())) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))) + .build(); + if (!value.isEmpty()) { + builder.add(k, value); + } + } + }); + return jsonBuilderFactory.createObjectBuilder(container.entrySet().stream() + .filter(it -> !"resources".equals(it.getKey())) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))) + .add("resources", builder) + .build(); + } + + private JsonValue dropNullCpu(final JsonValue jsonValue) { + try { + return jsonValue.asJsonArray().stream() + .map(JsonValue::asJsonObject) + .map(this::dropNullCpu) + .collect(Collector.of(jsonBuilderFactory::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll)) + .build(); + } catch (final RuntimeException re) { + log.log(FINEST, re, () -> "Can't check null cpu resources: " + re.getMessage()); + return jsonValue; + } + } + + private T replaceIfPresent(final T source, + final String parentPtr, final String name, + final Function fn) { + final var rawPtr = parentPtr + '/' + name; + final var ptr = jsonProvider.createPointer(rawPtr); + try { + final var value = ptr.getValue(source); + final var changed = fn.apply(value); + if (value == changed) { + return source; + } + return ptr.replace(source, changed); + } catch (final JsonException je) { + log.log(FINEST, je, je::getMessage); + return source; + } + } +} diff --git a/bundlebee-core/src/test/java/io/yupiik/bundlebee/core/service/ContainerSanitizerTest.java b/bundlebee-core/src/test/java/io/yupiik/bundlebee/core/service/ContainerSanitizerTest.java new file mode 100644 index 00000000..13b3daa7 --- /dev/null +++ b/bundlebee-core/src/test/java/io/yupiik/bundlebee/core/service/ContainerSanitizerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.bundlebee.core.service; + +import io.yupiik.bundlebee.core.qualifier.BundleBee; +import org.apache.openwebbeans.junit5.Cdi; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import javax.inject.Inject; +import javax.json.JsonBuilderFactory; + +import static javax.json.JsonValue.EMPTY_JSON_OBJECT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@Cdi +@TestInstance(PER_CLASS) +class ContainerSanitizerTest { + @Inject + private ContainerSanitizer sanitizer; + + @Inject + @BundleBee + private JsonBuilderFactory json; + + @Test + void noResources() { + assertEquals( + "{\"spec\":{\"containers\":[{}]}}", + sanitizer.dropCpuResources("pods", json.createObjectBuilder() + .add("spec", json.createObjectBuilder() + .add("containers", json.createArrayBuilder() + .add(json.createObjectBuilder()))) + .build()) + .toString()); + } + + @Test + void emptyResources() { + assertEquals( + "{\"spec\":{\"containers\":[{\"resources\":{}}]}}", + sanitizer.dropCpuResources("pods", json.createObjectBuilder() + .add("spec", json.createObjectBuilder() + .add("containers", json.createArrayBuilder() + .add(json.createObjectBuilder() + .add("resources", EMPTY_JSON_OBJECT)))) + .build()) + .toString()); + } + + @Test + void cpuOk() { + assertEquals( + "{\"spec\":{\"containers\":[{\"resources\":{\"requests\":{\"cpu\":1}}}]}}", + sanitizer.dropCpuResources("pods", json.createObjectBuilder() + .add("spec", json.createObjectBuilder() + .add("containers", json.createArrayBuilder() + .add(json.createObjectBuilder() + .add("resources", json.createObjectBuilder() + .add("requests", json.createObjectBuilder() + .add("cpu", 1)))))) + .build()) + .toString()); + } + + @Test + void cpuNull() { + assertEquals( + "{\"spec\":{\"containers\":[{\"resources\":{}}]}}", + sanitizer.dropCpuResources("pods", json.createObjectBuilder() + .add("spec", json.createObjectBuilder() + .add("containers", json.createArrayBuilder() + .add(json.createObjectBuilder() + .add("resources", json.createObjectBuilder() + .add("requests", json.createObjectBuilder() + .addNull("cpu")))))) + .build()) + .toString()); + } + + @Test + void cpuNullWithMemory() { + assertEquals( + "{\"spec\":{\"containers\":[{\"resources\":{\"requests\":{\"memory\":\"512Mi\"}}}]}}", + sanitizer.dropCpuResources("pods", json.createObjectBuilder() + .add("spec", json.createObjectBuilder() + .add("containers", json.createArrayBuilder() + .add(json.createObjectBuilder() + .add("resources", json.createObjectBuilder() + .add("requests", json.createObjectBuilder() + .addNull("cpu") + .add("memory", "512Mi")))))) + .build()) + .toString()); + } +}