Skip to content

Commit

Permalink
[apply] drop cpu:null values for first installation since it consider…
Browse files Browse the repository at this point in the history
…s 0 as default value and fails the deployment
  • Loading branch information
rmannibucau committed May 7, 2024
1 parent 26d758f commit f602010
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,6 +80,9 @@ public class KubeClient implements ConfigHolder {
@Inject
private Yaml2JsonConverter yaml2json;

@Inject
private ContainerSanitizer containerSanitizer;

@Inject
private ApiPreloader apiPreloader;

Expand Down Expand Up @@ -487,9 +491,16 @@ private CompletionStage<HttpResponse<String>> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 extends JsonStructure> T replaceIfPresent(final T source,
final String parentPtr, final String name,
final Function<JsonValue, JsonValue> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit f602010

Please sign in to comment.