From e2dbaf7f114181e4069d2a1c4f5386f983acfab3 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Thu, 28 Sep 2023 08:32:58 +0100 Subject: [PATCH] Add default support for the crd-generator (#5431) --- .../crd/generator/AbstractJsonSchema.java | 22 ++++++++++++- .../fabric8/crd/generator/CRDGenerator.java | 2 +- .../fabric8/crd/generator/v1/JsonSchema.java | 10 ++++++ .../crd/generator/v1beta1/JsonSchema.java | 10 ++++++ .../crd/example/annotated/AnnotatedSpec.java | 9 +++++- .../crd/generator/v1/JsonSchemaTest.java | 24 ++++++++++++-- doc/CRD-generator.md | 24 ++++++++++++++ .../fabric8/generator/annotation/Default.java | 31 +++++++++++++++++++ 8 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 generator-annotations/src/main/java/io/fabric8/generator/annotation/Default.java diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java index 21b550555a7..e2574b918b0 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java @@ -84,6 +84,7 @@ public abstract class AbstractJsonSchema { public static final String ANNOTATION_JSON_IGNORE = "com.fasterxml.jackson.annotation.JsonIgnore"; public static final String ANNOTATION_JSON_ANY_GETTER = "com.fasterxml.jackson.annotation.JsonAnyGetter"; public static final String ANNOTATION_JSON_ANY_SETTER = "com.fasterxml.jackson.annotation.JsonAnySetter"; + public static final String ANNOTATION_DEFAULT = "io.fabric8.generator.annotation.Default"; public static final String ANNOTATION_MIN = "io.fabric8.generator.annotation.Min"; public static final String ANNOTATION_MAX = "io.fabric8.generator.annotation.Max"; public static final String ANNOTATION_PATTERN = "io.fabric8.generator.annotation.Pattern"; @@ -123,6 +124,7 @@ public static String getSchemaTypeFor(TypeRef typeRef) { } protected static class SchemaPropsOptions { + final String defaultValue; final Double min; final Double max; final String pattern; @@ -132,6 +134,7 @@ protected static class SchemaPropsOptions { final boolean preserveUnknownFields; SchemaPropsOptions() { + defaultValue = null; min = null; max = null; pattern = null; @@ -140,8 +143,9 @@ protected static class SchemaPropsOptions { preserveUnknownFields = false; } - public SchemaPropsOptions(Double min, Double max, String pattern, + public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern, boolean nullable, boolean required, boolean preserveUnknownFields) { + this.defaultValue = defaultValue; this.min = min; this.max = max; this.pattern = pattern; @@ -150,6 +154,10 @@ public SchemaPropsOptions(Double min, Double max, String pattern, this.preserveUnknownFields = preserveUnknownFields; } + public Optional getDefault() { + return Optional.ofNullable(defaultValue); + } + public Optional getMin() { return Optional.ofNullable(min); } @@ -331,6 +339,7 @@ private T internalFromImpl(TypeDef definition, Set visited, List { switch (a.getClassRef().getFullyQualifiedName()) { + case ANNOTATION_DEFAULT: + defaultValue = (String) a.getParameters().get(VALUE); + break; case ANNOTATION_NULLABLE: nullable = true; break; @@ -444,6 +457,10 @@ public boolean isNullable() { return nullable; } + public Optional getDefault() { + return Optional.ofNullable(defaultValue); + } + public Optional getMax() { return Optional.ofNullable(max); } @@ -500,6 +517,7 @@ private static class PropertyFacade { private final Set matchedSchemaSwaps; private String renamedTo; private String description; + private String defaultValue; private Double min; private Double max; private String pattern; @@ -531,6 +549,7 @@ public PropertyFacade(Property property, Map potentialAccessors, if (method != null) { propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name)); } + defaultValue = null; min = null; max = null; pattern = null; @@ -569,6 +588,7 @@ public Property process() { LOGGER.debug("Description for property {} has already been contributed by: {}", name, descriptionContributedBy); } } + defaultValue = p.getDefault().orElse(null); min = p.getMin().orElse(min); max = p.getMax().orElse(max); pattern = p.getPattern().orElse(pattern); diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java index 77b85f881e7..bbc526b2293 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/CRDGenerator.java @@ -41,7 +41,7 @@ public class CRDGenerator { private CRDOutput output; private Map infos; - private static final ObjectMapper YAML_MAPPER = new ObjectMapper( + public static final ObjectMapper YAML_MAPPER = new ObjectMapper( new YAMLFactory() .enable(Feature.MINIMIZE_QUOTES) .enable(Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java index d0aea6a64e0..7e7e7304c2d 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java @@ -15,6 +15,7 @@ */ package io.fabric8.crd.generator.v1; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.crd.generator.AbstractJsonSchema; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; @@ -25,6 +26,8 @@ import java.util.List; +import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; + public class JsonSchema extends AbstractJsonSchema { private static final JsonSchema instance = new JsonSchema(); @@ -58,6 +61,13 @@ public JSONSchemaPropsBuilder newBuilder() { public void addProperty(Property property, JSONSchemaPropsBuilder builder, JSONSchemaProps schema, SchemaPropsOptions options) { if (schema != null) { + options.getDefault().ifPresent(s -> { + try { + schema.setDefault(YAML_MAPPER.readTree(s)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot parse default value: '" + s + "' as valid YAML."); + } + }); options.getMin().ifPresent(schema::setMinimum); options.getMax().ifPresent(schema::setMaximum); options.getPattern().ifPresent(schema::setPattern); diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java index 65c04b20155..92e977353d1 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java @@ -15,6 +15,7 @@ */ package io.fabric8.crd.generator.v1beta1; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.crd.generator.AbstractJsonSchema; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaProps; @@ -25,6 +26,8 @@ import java.util.List; +import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; + public class JsonSchema extends AbstractJsonSchema { private static final JsonSchema instance = new JsonSchema(); @@ -59,6 +62,13 @@ public JSONSchemaPropsBuilder newBuilder() { public void addProperty(Property property, JSONSchemaPropsBuilder builder, JSONSchemaProps schema, SchemaPropsOptions options) { if (schema != null) { + options.getDefault().ifPresent(s -> { + try { + schema.setDefault(YAML_MAPPER.readTree(s)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot parse default value: '" + s + "' as valid YAML."); + } + }); options.getMin().ifPresent(schema::setMinimum); options.getMax().ifPresent(schema::setMaximum); options.getPattern().ifPresent(schema::setPattern); diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java index f09a1348a82..ba5715ecddf 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; import io.fabric8.generator.annotation.Max; import io.fabric8.generator.annotation.Min; import io.fabric8.generator.annotation.Nullable; @@ -37,7 +38,8 @@ public class AnnotatedSpec { private int max; private String singleDigit; private String nullable; - @NotNull + private String defaultValue; + @Required private boolean emptySetter; @Required private boolean emptySetter2; @@ -86,6 +88,11 @@ public String getNullable() { return null; } + @Default("my-value") + public String getDefaultValue() { + return "foo"; + } + @JsonProperty public void setEmptySetter(boolean emptySetter) { this.emptySetter = emptySetter; diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java index a44ae180853..57f7c071990 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java @@ -15,6 +15,7 @@ */ package io.fabric8.crd.generator.v1; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.crd.example.annotated.Annotated; import io.fabric8.crd.example.basic.Basic; @@ -32,7 +33,13 @@ import java.util.Map; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class JsonSchemaTest { @@ -72,7 +79,7 @@ void shouldCreateJsonSchemaFromClass() { } @Test - void shouldAugmentPropertiesSchemaFromAnnotations() { + void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingException { TypeDef annotated = Types.typeDefFrom(Annotated.class); JSONSchemaProps schema = JsonSchema.from(annotated); assertNotNull(schema); @@ -80,7 +87,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() { assertEquals(2, properties.size()); final JSONSchemaProps specSchema = properties.get("spec"); Map spec = specSchema.getProperties(); - assertEquals(11, spec.size()); + assertEquals(12, spec.size()); // check descriptions are present assertTrue(spec.containsKey("from-field")); @@ -98,29 +105,40 @@ void shouldAugmentPropertiesSchemaFromAnnotations() { assertTrue(spec.containsKey("anEnum")); final JSONSchemaProps min = spec.get("min"); + assertNull(min.getDefault()); assertEquals(-5.0, min.getMinimum()); assertNull(min.getMaximum()); assertNull(min.getPattern()); assertNull(min.getNullable()); final JSONSchemaProps max = spec.get("max"); + assertNull(max.getDefault()); assertEquals(5.0, max.getMaximum()); assertNull(max.getMinimum()); assertNull(max.getPattern()); assertNull(max.getNullable()); final JSONSchemaProps pattern = spec.get("singleDigit"); + assertNull(pattern.getDefault()); assertEquals("\\b[1-9]\\b", pattern.getPattern()); assertNull(pattern.getMinimum()); assertNull(pattern.getMaximum()); assertNull(pattern.getNullable()); final JSONSchemaProps nullable = spec.get("nullable"); + assertNull(nullable.getDefault()); assertTrue(nullable.getNullable()); assertNull(nullable.getMinimum()); assertNull(nullable.getMaximum()); assertNull(nullable.getPattern()); + final JSONSchemaProps defaultValue = spec.get("defaultValue"); + assertEquals("my-value", YAML_MAPPER.writeValueAsString(defaultValue.getDefault()).trim()); + assertNull(defaultValue.getNullable()); + assertNull(defaultValue.getMinimum()); + assertNull(defaultValue.getMaximum()); + assertNull(defaultValue.getPattern()); + // check required list, should register properties with their modified name if needed final List required = specSchema.getRequired(); assertEquals(3, required.size()); diff --git a/doc/CRD-generator.md b/doc/CRD-generator.md index fb822d3f428..eeb17a90cb3 100644 --- a/doc/CRD-generator.md +++ b/doc/CRD-generator.md @@ -156,6 +156,30 @@ The field will be skipped in the generated CRD and will not appear in the schema If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Min` +```java +public class ExampleSpec { + @Default("foo") + String someValue; +} +``` + +The field will have the `default` property in the generated CRD, such as: + +```yaml + spec: + properties: + someValue: + default: foo + type: string + required: + - someValue + type: object +``` + +### io.fabric8.generator.annotation.Min + +If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Min` + ```java public class ExampleSpec { @Min(-1) diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Default.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Default.java new file mode 100644 index 00000000000..a35b8f7e269 --- /dev/null +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Default.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.generator.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * Java representation of the `default` field of JSONSchemaProps + * https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#jsonschemaprops-v1-apiextensions-k8s-io + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Default { + String value(); +}