Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(crd-generator): Add support for validation rules #5788

Merged
merged 8 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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()))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed here a way to resolve annotations for inherited classes. Is this the right way to implement it with sundrio?

GetDefinition.of(classRef)

.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
Loading