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
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 Down Expand Up @@ -48,6 +49,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static io.sundr.model.utils.Types.BOOLEAN_REF;
import static io.sundr.model.utils.Types.DOUBLE_REF;
Expand Down Expand Up @@ -111,6 +113,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 +154,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 +165,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 +178,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 +208,11 @@ public boolean getRequired() {
public boolean isPreserveUnknownFields() {
return preserveUnknownFields;
}

public Optional<List<KubernetesValidationRule>> getValidationRules() {
return Optional.ofNullable(validationRules)
.flatMap(rules -> rules.isEmpty() ? Optional.empty() : Optional.of(rules));
}
}

/**
Expand Down Expand Up @@ -332,6 +344,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSche
facade.min,
facade.max,
facade.pattern,
facade.validationRules,
facade.nullable,
facade.required,
facade.preserveUnknownFields);
Expand Down Expand Up @@ -362,6 +375,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 +442,14 @@ public void process() {
case ANNOTATION_SCHEMA_FROM:
schemaFrom = extractClassRef(a.getParameters().get("type"));
break;
case ANNOTATION_VALIDATION_RULE:
validationRules = Collections.singletonList(KubernetesValidationRule.from(a));
break;
case ANNOTATION_VALIDATION_RULES:
validationRules = Arrays.stream(((ValidationRule[]) a.getParameters().get(VALUE)))
.map(KubernetesValidationRule::from)
.collect(Collectors.toList());
break;
}
});
}
Expand Down Expand Up @@ -456,6 +478,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 +536,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 +560,7 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
min = null;
max = null;
pattern = null;
validationRules = null;
}

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

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

protected static class KubernetesValidationRule {
protected String fieldPath;
protected String message;
protected String messageExpression;
protected Boolean optionalOldSelf;
protected String reason;
protected 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) annotationRef.getParameters().get("optionalOldSelf")) ? 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
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.List;
import java.util.stream.Collectors;

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

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

options.getValidationRules()
.map(this::mapValidationRules)
.ifPresent(schema::setXKubernetesValidations);

if (options.isNullable()) {
schema.setNullable(true);
}
Expand Down Expand Up @@ -139,4 +146,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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import io.fabric8.crd.generator.AbstractJsonSchema;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaProps;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaPropsBuilder;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRule;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRuleBuilder;
import io.sundr.model.Property;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;

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

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

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

options.getValidationRules()
.map(this::mapValidationRules)
.ifPresent(schema::setXKubernetesValidations);

if (options.isNullable()) {
schema.setNullable(true);
}
Expand Down Expand Up @@ -142,4 +149,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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.fabric8.generator.annotation.Nullable;
import io.fabric8.generator.annotation.Pattern;
import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Data;

@Data
Expand Down Expand Up @@ -54,6 +55,14 @@ public class AnnotatedSpec {

private boolean ignoredBar;

@ValidationRule(value = "self.startwith('prefix-')", message = "kubernetesValidationRule must start with prefix 'prefix-'")
private String kubernetesValidationRule;

@ValidationRule("first.rule")
@ValidationRule("second.rule")
@ValidationRule(value = "third.rule", reason = "FieldValueForbidden")
private String kubernetesValidationRules;

@JsonProperty("from-getter")
@JsonPropertyDescription("from-getter-description")
@Required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.kubernetes.api.model.AnyType;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule;
import io.sundr.model.TypeDef;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -102,7 +103,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
assertNotNull(schema);
Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
final JSONSchemaProps specSchema = properties.get("spec");
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 13);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 15);

// check descriptions are present
assertTrue(spec.containsKey("from-field"));
Expand Down Expand Up @@ -177,6 +178,34 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
// check ignored fields
assertFalse(spec.containsKey("ignoredFoo"));
assertFalse(spec.containsKey("ignoredBar"));

final JSONSchemaProps k8sValidationProps = spec.get("kubernetesValidationRule");
final List<ValidationRule> k8sValidationRulesSingle = k8sValidationProps.getXKubernetesValidations();
assertNotNull(k8sValidationRulesSingle);
assertEquals(1, k8sValidationRulesSingle.size());
assertEquals("self.startwith('prefix-')", k8sValidationRulesSingle.get(0).getRule());
assertEquals("kubernetesValidationRule must start with prefix 'prefix-'", k8sValidationRulesSingle.get(0).getMessage());
assertNull(k8sValidationRulesSingle.get(0).getMessageExpression());
assertNull(k8sValidationRulesSingle.get(0).getReason());
assertNull(k8sValidationRulesSingle.get(0).getFieldPath());
assertNull(k8sValidationRulesSingle.get(0).getOptionalOldSelf());

final JSONSchemaProps kubernetesValidationsRepeated = spec.get("kubernetesValidationRules");
final List<ValidationRule> kubernetesValidationsRepeatedRules = kubernetesValidationsRepeated.getXKubernetesValidations();
assertNotNull(kubernetesValidationsRepeatedRules);
assertEquals(3, kubernetesValidationsRepeatedRules.size());
assertEquals("first.rule", kubernetesValidationsRepeatedRules.get(0).getRule());
assertNull(kubernetesValidationsRepeatedRules.get(0).getFieldPath());
assertNull(kubernetesValidationsRepeatedRules.get(0).getReason());
assertNull(kubernetesValidationsRepeatedRules.get(0).getMessage());
assertNull(kubernetesValidationsRepeatedRules.get(0).getMessageExpression());
assertNull(kubernetesValidationsRepeatedRules.get(0).getOptionalOldSelf());
assertEquals("second.rule", kubernetesValidationsRepeatedRules.get(1).getRule());
assertNull(kubernetesValidationsRepeatedRules.get(1).getFieldPath());
assertNull(kubernetesValidationsRepeatedRules.get(1).getReason());
assertNull(kubernetesValidationsRepeatedRules.get(1).getMessage());
assertNull(kubernetesValidationsRepeatedRules.get(1).getMessageExpression());
assertNull(kubernetesValidationsRepeatedRules.get(1).getOptionalOldSelf());
}

@Test
Expand Down
Loading
Loading