Skip to content

Commit

Permalink
[linting] adding checks for latest usage, warning for not using anti …
Browse files Browse the repository at this point in the history
…affinity, checks for liveness and readyness presence
  • Loading branch information
rmannibucau committed Aug 18, 2023
1 parent 44a2623 commit ce61f41
Show file tree
Hide file tree
Showing 13 changed files with 700 additions and 51 deletions.
2 changes: 1 addition & 1 deletion bundlebee-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.5.0</version>
<executions>
<execution>
<goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public class LintCommand implements CompletingExecutable {
@ConfigProperty(name = "bundlebee.lint.ignoredRules", defaultValue = "-")
private List<String> ignoredRules;

@Inject
@Description("Should remediation be shown (it is verbose so skipped by default).")
@ConfigProperty(name = "bundlebee.lint.showRemediation", defaultValue = "false")
private boolean showRemediation;

@Inject
private AlveolusHandler visitor;

Expand All @@ -97,13 +102,22 @@ public class LintCommand implements CompletingExecutable {
@Any
private Instance<LintingCheck> checks;

private List<String> ruleNames;

@Override
public Stream<String> complete(final Map<String, String> options, final String optionName) {
switch (optionName) {
case "failLevel":
return Stream.of(LintError.LintLevel.values()).map(LintError.LintLevel::name);
case "alveolus":
return visitor.findCompletionAlveoli(options);
case "ignoredRules":
if (ruleNames == null) {
ruleNames = this.checks.stream()
.map(LintingCheck::name)
.collect(toList());
}
return ruleNames.stream();
default:
return Stream.empty();
}
Expand Down Expand Up @@ -139,8 +153,8 @@ private void doLint(final AlveolusHandler.AlveolusContext ctx,
final var ld = new LintingCheck.LintableDescriptor(descriptor, desc);
result.errors.addAll(checks.stream()
.filter(c -> c.accept(ld))
.flatMap(c -> c.validate(ld))
.map(it -> new DecoratedLintError(it, ctx.getAlveolus().getName(), descriptor))
.flatMap(c -> c.validate(ld)
.map(it -> new DecoratedLintError(it, ctx.getAlveolus().getName(), descriptor, c.remediation())))
.collect(toList()));
}

Expand All @@ -165,7 +179,13 @@ private void postProcess(final LintErrors result) {
})
.forEach(i -> {
final var level = i.getError().getLevel();
log.log(level == LintError.LintLevel.ERROR ? Level.SEVERE : Level.parse(level.name()), i.format());
final var logLevel = level == LintError.LintLevel.ERROR ? Level.SEVERE : Level.parse(level.name());
final var message = i.format();
if (!showRemediation) {
log.log(logLevel, message);
} else {
log.log(logLevel, message + (i.getRemediation() != null && !i.getRemediation().isBlank() ? "\n -> " + i.getRemediation() : ""));
}
});
}

Expand Down Expand Up @@ -215,6 +235,7 @@ private static class DecoratedLintError {
private final LintError error;
private final String aveolus;
private final String descriptor;
private final String remediation;

public String format() {
return "[" + getAveolus() + "][" + getDescriptor() + "] " + getError().getMessage();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2021-2023 - 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.command.impl;

import io.yupiik.bundlebee.core.command.Executable;
import io.yupiik.bundlebee.core.command.impl.lint.LintingCheck;
import lombok.extern.java.Log;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.util.concurrent.CompletionStage;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.joining;

@Log
@Dependent
public class ListLintCommand implements Executable {
@Inject
@Any
private Instance<LintingCheck> checks;

@Override
public String name() {
return "list-lint-rules";
}

@Override
public String description() {
return "List available linting rules (ease exclusions for ex).";
}

@Override
public CompletionStage<?> execute() {
return completedFuture(checks.stream()
.map(c -> " *" + c.name())
.collect(joining("\n")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,13 @@

import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonValue;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

public abstract class CheckResources extends CheckValue {
public abstract class CheckResources extends ContainerValueValidator {
private final String resource;
private final String type;

public CheckResources(final String type, final String resource) {
super(
Set.of("Deployment", "CronJob", "Pod", "Job"),
Map.of( // points to the list of containers, then nested pointer will be /$i/resources/${type}s/$resource
"Deployment", "/spec/template/spec/containers",
"CronJob", "/spec/jobTemplate/template/spec/containers",
"Job", "/spec/template/spec/containers",
"Pod", "/spec/containers"));
this.type = type;
this.resource = resource;
}
Expand All @@ -56,13 +46,7 @@ public String remediation() {
}

@Override
protected Stream<LintError> doValidate(final LintableDescriptor descriptor, final JsonValue containers) {
return containers.asJsonArray().stream()
.map(JsonValue::asJsonObject)
.flatMap(it -> validate(it, descriptor));
}

private Stream<LintError> validate(final JsonObject container, final LintableDescriptor descriptor) {
protected Stream<LintError> validate(final JsonObject container, final LintableDescriptor descriptor) {
final var resources = container.getJsonObject("resources");
if (resources == null) {
return Stream.of(new LintError(LintError.LintLevel.ERROR, "No resources element in container"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,92 @@
package io.yupiik.bundlebee.core.command.impl.lint.builtin;

import io.yupiik.bundlebee.core.command.impl.lint.LintError;
import lombok.RequiredArgsConstructor;

import javax.json.Json;
import javax.json.JsonPointer;
import javax.json.JsonValue;
import javax.json.spi.JsonProvider;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static io.yupiik.bundlebee.core.command.impl.lint.LintError.LintLevel.ERROR;
import static java.util.stream.Collectors.toMap;

public abstract class CheckValue extends CheckByKind {
private final Map<String, JsonPointer> pointers;
private static final JsonProvider PROVIDER = JsonProvider.provider();

protected CheckValue(final Set<String> supportedKinds, final Map<String, String> jsonPointers) {
private final Map<String, Ptr> pointers;
private final Map<String, Condition> conditions;

protected CheckValue(final Set<String> supportedKinds, final Map<String, String> jsonPointers, final Map<String, Condition> conditions) {
super(supportedKinds);
this.pointers = jsonPointers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> Json.createPointer(e.getValue())));
this.pointers = jsonPointers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> new Ptr(e.getValue(), PROVIDER.createPointer(e.getValue()))));
this.conditions = conditions;
if (this.conditions != null) {
this.conditions.values().stream()
.filter(it -> it.pointer != null)
.forEach(v -> v.jsonPointer = PROVIDER.createPointer(v.pointer));
}
}

@Override
public Stream<LintError> validate(final LintableDescriptor descriptor) {
final var ptr = pointers.get(descriptor.getDescriptor().getString("kind", ""));
final var kind = descriptor.getDescriptor().getString("kind", "");
final var ptr = pointers.get(kind);
try {
final var value = ptr.pointer.getValue(descriptor.getDescriptor());
if (conditions == null) {
return doValidate(descriptor, value);
}

if (shouldSkip(kind, descriptor)) {
return Stream.empty();
}

return doValidate(descriptor, value);
} catch (final RuntimeException re) {
if (shouldSkip(kind, descriptor)) {
return Stream.empty();
}

if (ignoreError(descriptor)) {
return Stream.empty();
}

return Stream.of(new LintError(ERROR, "No '" + ptr.ptr + "' in '" + descriptor.getName() + "'"));
}
}

private boolean shouldSkip(final String kind, final LintableDescriptor descriptor) {
final var condition = conditions.get(kind);
try {
return doValidate(descriptor, ptr.getValue(descriptor.getDescriptor()));
return condition != null && !condition.tester.test(
condition.jsonPointer == null ? null : condition.jsonPointer.getValue(descriptor.getDescriptor()));
} catch (final RuntimeException re) {
return Stream.of(new LintError(ERROR, "No '" + ptr.toString() + "' in '" + descriptor.getName() + "'"));
return !condition.tester.test(JsonValue.NULL);
}
}

protected boolean ignoreError(final LintableDescriptor descriptor) {
return false;
}

protected abstract Stream<LintError> doValidate(LintableDescriptor descriptor, JsonValue value);

@RequiredArgsConstructor
private static class Ptr {
private final String ptr;
private final JsonPointer pointer;
}

@RequiredArgsConstructor
protected static class Condition {
private final String pointer;
private final Predicate<JsonValue> tester;

// internal
private JsonPointer jsonPointer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2021-2023 - 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.command.impl.lint.builtin;

import io.yupiik.bundlebee.core.command.impl.lint.LintError;

import javax.json.JsonObject;
import javax.json.JsonValue;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;

public abstract class ContainerValueValidator extends CheckValue {
private static final Set<String> RUNNABLE_TYPES = Set.of("Deployment", "CronJob", "Pod", "Job");
private static final Map<String, String> RUNNABLE_CONTAINERS_POINTERS = Map.of(
"Deployment", "/spec/template/spec/containers",
"CronJob", "/spec/jobTemplate/template/spec/containers",
"Job", "/spec/template/spec/containers",
"Pod", "/spec/containers");

protected ContainerValueValidator() {
super(RUNNABLE_TYPES, RUNNABLE_CONTAINERS_POINTERS, null);
}

protected ContainerValueValidator(final Set<String> types) {
super(types, types.stream().collect(toMap(identity(), RUNNABLE_CONTAINERS_POINTERS::get)), null);
}

@Override
protected Stream<LintError> doValidate(final LintableDescriptor descriptor, final JsonValue containers) {
return containers.asJsonArray().stream()
.map(JsonValue::asJsonObject)
.flatMap(it -> validate(it, descriptor));
}

protected abstract Stream<LintError> validate(final JsonObject container, final LintableDescriptor descriptor);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2021-2023 - 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.command.impl.lint.builtin;

import io.yupiik.bundlebee.core.command.impl.lint.LintError;

import javax.enterprise.context.Dependent;
import javax.json.JsonObject;
import java.util.Set;
import java.util.stream.Stream;

@Dependent
public class LivenessProbeImage extends ContainerValueValidator {
public LivenessProbeImage() {
super(Set.of("Deployment"));
}

@Override
public String name() {
return "no-liveness-probe";
}

@Override
public String description() {
return "Ensures a liveness probe is defined.";
}

@Override
public String remediation() {
return "Any container (from containers array) should have a liveness probe.";
}

@Override
protected Stream<LintError> validate(final JsonObject container, final LintableDescriptor descriptor) {
final var probe = container.getJsonObject("livenessProbe");
if (probe == null) {
return Stream.of(new LintError(LintError.LintLevel.ERROR, "No liveness probe"));
}
return Stream.empty();
}
}
Loading

0 comments on commit ce61f41

Please # to comment.