Skip to content

Commit

Permalink
feat(crd-generator): add support for validation rules (#5788)
Browse files Browse the repository at this point in the history
  • Loading branch information
baloo42 authored Mar 25, 2024
1 parent d24e424 commit 3ae4f9d
Show file tree
Hide file tree
Showing 14 changed files with 926 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Fix #5357: adding additional Quantity methods
* Fix #5635: refined LeaderElector lifecycle and logging
* Fix #5787: (crd-generator) add support for deprecated versions for generated CRDs
* Fix #5788: (crd-generator) add support for Kubernetes validation rules

#### Dependency Upgrade

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.generator.annotation.ValidationRule;
import io.fabric8.kubernetes.api.model.Duration;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Quantity;
Expand All @@ -31,6 +32,7 @@
import io.sundr.model.Property;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;
import io.sundr.model.functions.GetDefinition;
import io.sundr.utils.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -43,11 +45,14 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.sundr.model.utils.Types.BOOLEAN_REF;
import static io.sundr.model.utils.Types.DOUBLE_REF;
Expand Down Expand Up @@ -111,6 +116,8 @@ public abstract class AbstractJsonSchema<T, B> {
public static final String ANNOTATION_PERSERVE_UNKNOWN_FIELDS = "io.fabric8.crd.generator.annotation.PreserveUnknownFields";
public static final String ANNOTATION_SCHEMA_SWAP = "io.fabric8.crd.generator.annotation.SchemaSwap";
public static final String ANNOTATION_SCHEMA_SWAPS = "io.fabric8.crd.generator.annotation.SchemaSwaps";
public static final String ANNOTATION_VALIDATION_RULE = "io.fabric8.generator.annotation.ValidationRule";
public static final String ANNOTATION_VALIDATION_RULES = "io.fabric8.generator.annotation.ValidationRules";

public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";
Expand Down Expand Up @@ -150,8 +157,8 @@ protected static class SchemaPropsOptions {
final String pattern;
final boolean nullable;
final boolean required;

final boolean preserveUnknownFields;
final List<KubernetesValidationRule> validationRules;

SchemaPropsOptions() {
defaultValue = null;
Expand All @@ -161,9 +168,11 @@ protected static class SchemaPropsOptions {
nullable = false;
required = false;
preserveUnknownFields = false;
validationRules = null;
}

public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern,
List<KubernetesValidationRule> validationRules,
boolean nullable, boolean required, boolean preserveUnknownFields) {
this.defaultValue = defaultValue;
this.min = min;
Expand All @@ -172,6 +181,7 @@ public SchemaPropsOptions(String defaultValue, Double min, Double max, String pa
this.nullable = nullable;
this.required = required;
this.preserveUnknownFields = preserveUnknownFields;
this.validationRules = validationRules;
}

public Optional<String> getDefault() {
Expand Down Expand Up @@ -201,6 +211,11 @@ public boolean getRequired() {
public boolean isPreserveUnknownFields() {
return preserveUnknownFields;
}

public List<KubernetesValidationRule> getValidationRules() {
return Optional.ofNullable(validationRules)
.orElseGet(Collections::emptyList);
}
}

/**
Expand Down Expand Up @@ -267,6 +282,18 @@ private void extractSchemaSwap(ClassRef definitionType, Object annotation, Inter
}
}

private static Stream<KubernetesValidationRule> extractKubernetesValidationRules(AnnotationRef annotationRef) {
switch (annotationRef.getClassRef().getFullyQualifiedName()) {
case ANNOTATION_VALIDATION_RULE:
return Stream.of(KubernetesValidationRule.from(annotationRef));
case ANNOTATION_VALIDATION_RULES:
return Arrays.stream(((ValidationRule[]) annotationRef.getParameters().get(VALUE)))
.map(KubernetesValidationRule::from);
default:
return Stream.empty();
}
}

private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSchemaSwaps schemaSwaps, String... ignore) {
Set<String> ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore))
: Collections
Expand Down Expand Up @@ -332,15 +359,23 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSche
facade.min,
facade.max,
facade.pattern,
facade.validationRules,
facade.nullable,
facade.required,
facade.preserveUnknownFields);

addProperty(possiblyRenamedProperty, builder, possiblyUpdatedSchema, options);
}

List<KubernetesValidationRule> validationRules = Stream
.concat(definition.getAnnotations().stream(), definition.getExtendsList().stream()
.flatMap(classRef -> GetDefinition.of(classRef).getAnnotations().stream()))
.flatMap(AbstractJsonSchema::extractKubernetesValidationRules)
.filter(Objects::nonNull)
.collect(Collectors.toList());

swaps.throwIfUnmatchedSwaps();
return build(builder, required, preserveUnknownFields);
return build(builder, required, validationRules, preserveUnknownFields);
}

private Map<String, Method> indexPotentialAccessors(TypeDef definition) {
Expand All @@ -362,6 +397,7 @@ private static class PropertyOrAccessor {
private Double min;
private Double max;
private String pattern;
private List<KubernetesValidationRule> validationRules;
private boolean nullable;
private boolean required;
private boolean ignored;
Expand Down Expand Up @@ -428,6 +464,10 @@ public void process() {
case ANNOTATION_SCHEMA_FROM:
schemaFrom = extractClassRef(a.getParameters().get("type"));
break;
case ANNOTATION_VALIDATION_RULE:
case ANNOTATION_VALIDATION_RULES:
validationRules = extractKubernetesValidationRules(a).collect(Collectors.toList());
break;
}
});
}
Expand Down Expand Up @@ -456,6 +496,10 @@ public Optional<String> getPattern() {
return Optional.ofNullable(pattern);
}

public Optional<List<KubernetesValidationRule>> getValidationRules() {
return Optional.ofNullable(validationRules);
}

public boolean isRequired() {
return required;
}
Expand Down Expand Up @@ -510,6 +554,7 @@ private static class PropertyFacade {
private String nameContributedBy;
private String descriptionContributedBy;
private TypeRef schemaFrom;
private List<KubernetesValidationRule> validationRules;

public PropertyFacade(Property property, Map<String, Method> potentialAccessors, ClassRef schemaSwap) {
original = property;
Expand All @@ -533,6 +578,7 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
min = null;
max = null;
pattern = null;
validationRules = new LinkedList<>();
}

public Property process() {
Expand Down Expand Up @@ -562,6 +608,7 @@ public Property process() {
min = p.getMin().orElse(min);
max = p.getMax().orElse(max);
pattern = p.getPattern().orElse(pattern);
p.getValidationRules().ifPresent(rules -> validationRules.addAll(rules));

if (p.isNullable()) {
nullable = true;
Expand All @@ -588,6 +635,72 @@ public Property process() {
}
}

/**
* Version independent DTO for a ValidationRule
*/
protected static class KubernetesValidationRule {
private String fieldPath;
private String message;
private String messageExpression;
private Boolean optionalOldSelf;
private String reason;
private String rule;

public String getFieldPath() {
return fieldPath;
}

public String getMessage() {
return message;
}

public String getMessageExpression() {
return messageExpression;
}

public Boolean getOptionalOldSelf() {
return optionalOldSelf;
}

public String getReason() {
return reason;
}

public String getRule() {
return rule;
}

static KubernetesValidationRule from(AnnotationRef annotationRef) {
KubernetesValidationRule result = new KubernetesValidationRule();
result.rule = (String) annotationRef.getParameters().get(VALUE);
result.reason = mapNotEmpty((String) annotationRef.getParameters().get("reason"));
result.message = mapNotEmpty((String) annotationRef.getParameters().get("message"));
result.messageExpression = mapNotEmpty((String) annotationRef.getParameters().get("messageExpression"));
result.fieldPath = mapNotEmpty((String) annotationRef.getParameters().get("fieldPath"));
result.optionalOldSelf = Boolean.TRUE.equals(annotationRef.getParameters().get("optionalOldSelf")) ? Boolean.TRUE : null;
return result;
}

static KubernetesValidationRule from(ValidationRule validationRule) {
KubernetesValidationRule result = new KubernetesValidationRule();
result.rule = validationRule.value();
result.reason = mapNotEmpty(validationRule.reason());
result.message = mapNotEmpty(validationRule.message());
result.messageExpression = mapNotEmpty(validationRule.messageExpression());
result.fieldPath = mapNotEmpty(validationRule.fieldPath());
result.optionalOldSelf = validationRule.optionalOldSelf() ? true : null;
return result;
}

private static String mapNotEmpty(String s) {
if (s == null)
return null;
if (s.isEmpty())
return null;
return s;
}
}

private boolean isPotentialAccessor(Method method) {
final String name = method.getName();
return name.startsWith("is") || name.startsWith("get") || name.startsWith("set");
Expand Down Expand Up @@ -654,9 +767,14 @@ private String extractUpdatedNameFromJacksonPropertyIfPresent(Property property)
*
* @param builder the builder used to build the final schema
* @param required the list of names of required fields
* @param validationRules the list of validation rules
* @param preserveUnknownFields whether preserveUnknownFields is enabled
* @return the built JSON schema
*/
public abstract T build(B builder, List<String> required, boolean preserveUnknownFields);
public abstract T build(B builder,
List<String> required,
List<KubernetesValidationRule> validationRules,
boolean preserveUnknownFields);

/**
* Builds the specific JSON schema representing the structural schema for the specified property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
import io.fabric8.crd.generator.AbstractJsonSchema;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsBuilder;
import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule;
import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRuleBuilder;
import io.sundr.model.Property;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER;

Expand Down Expand Up @@ -77,6 +81,17 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
options.getMax().ifPresent(schema::setMaximum);
options.getPattern().ifPresent(schema::setPattern);

List<ValidationRule> validationRulesFromProperty = options.getValidationRules().stream()
.map(this::mapValidationRule)
.collect(Collectors.toList());

List<ValidationRule> resultingValidationRules = new ArrayList<>(schema.getXKubernetesValidations());
resultingValidationRules.addAll(validationRulesFromProperty);

if (!resultingValidationRules.isEmpty()) {
schema.setXKubernetesValidations(resultingValidationRules);
}

if (options.isNullable()) {
schema.setNullable(true);
}
Expand All @@ -90,11 +105,13 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
}

@Override
public JSONSchemaProps build(JSONSchemaPropsBuilder builder, List<String> required, boolean preserveUnknownFields) {
public JSONSchemaProps build(JSONSchemaPropsBuilder builder, List<String> required,
List<KubernetesValidationRule> validationRules, boolean preserveUnknownFields) {
builder = builder.withRequired(required);
if (preserveUnknownFields) {
builder.withXKubernetesPreserveUnknownFields(preserveUnknownFields);
}
builder.addAllToXKubernetesValidations(mapValidationRules(validationRules));
return builder.build();
}

Expand Down Expand Up @@ -139,4 +156,21 @@ protected JSONSchemaProps addDescription(JSONSchemaProps schema, String descript
.withDescription(description)
.build();
}

private List<ValidationRule> mapValidationRules(List<KubernetesValidationRule> validationRules) {
return validationRules.stream()
.map(this::mapValidationRule)
.collect(Collectors.toList());
}

private ValidationRule mapValidationRule(KubernetesValidationRule validationRule) {
return new ValidationRuleBuilder()
.withRule(validationRule.getRule())
.withMessage(validationRule.getMessage())
.withMessageExpression(validationRule.getMessageExpression())
.withReason(validationRule.getReason())
.withFieldPath(validationRule.getFieldPath())
.withOptionalOldSelf(validationRule.getOptionalOldSelf())
.build();
}
}
Loading

0 comments on commit 3ae4f9d

Please sign in to comment.