Skip to content

Commit

Permalink
wiring up new top level options
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Apr 26, 2024
1 parent 71a7c1d commit c61f03b
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
public abstract class AbstractCustomResourceHandler {

public abstract void handle(CustomResourceInfo config);
public abstract void handle(CustomResourceInfo config, ResolvingContext resolvingContext);

public abstract Stream<HasMetadata> finish();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private T resolveRoot(Class<?> definition) {
return resolveObject(new LinkedHashMap<>(), schemaSwaps, schema, "kind", "apiVersion", "metadata");
}
return resolveProperty(new LinkedHashMap<>(), schemaSwaps, null,
resolvingContext.serializationConfig.constructType(definition), schema);
resolvingContext.objectMapper.getSerializationConfig().constructType(definition), schema);
}

/**
Expand Down Expand Up @@ -282,9 +282,12 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
final InternalSchemaSwaps swaps = schemaSwaps;

GeneratorObjectSchema gos = (GeneratorObjectSchema) jacksonSchema.asObjectSchema();
AnnotationIntrospector ai = resolvingContext.serializationConfig.getAnnotationIntrospector();
BeanDescription bd = resolvingContext.serializationConfig.introspect(gos.javaType);
boolean preserveUnknownFields = bd.findAnyGetter() != null || bd.findAnySetterAccessor() != null;
AnnotationIntrospector ai = resolvingContext.objectMapper.getSerializationConfig().getAnnotationIntrospector();
BeanDescription bd = resolvingContext.objectMapper.getSerializationConfig().introspect(gos.javaType);
boolean preserveUnknownFields = false;
if (resolvingContext.implicitPreserveUnknownFields) {
preserveUnknownFields = bd.findAnyGetter() != null || bd.findAnySetterAccessor() != null;
}

consumeRepeatingAnnotation(gos.javaType.getRawClass(), SchemaSwap.class, ss -> {
swaps.registerSwap(gos.javaType.getRawClass(),
Expand Down Expand Up @@ -333,7 +336,7 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
continue;
}
propertySchema = toJsonSchema(propertyMetadata.schemaFrom);
type = resolvingContext.serializationConfig.constructType(propertyMetadata.schemaFrom);
type = resolvingContext.objectMapper.getSerializationConfig().constructType(propertyMetadata.schemaFrom);
}

T schema = resolveProperty(visited, schemaSwaps, name, type, propertySchema);
Expand Down Expand Up @@ -423,7 +426,7 @@ private T resolveProperty(LinkedHashMap<String, String> visited, InternalSchemaS
} else if (jacksonSchema instanceof ReferenceSchema) {
// de-reference the reference schema - these can be naturally non-cyclic, for example siblings
ReferenceSchema ref = (ReferenceSchema) jacksonSchema;
GeneratorObjectSchema referenced = resolvingContext.seen.get(ref.get$ref());
GeneratorObjectSchema referenced = resolvingContext.uriToJacksonSchema.get(ref.get$ref());
Utils.checkNotNull(referenced, "Could not find previously generated schema");
jacksonSchema = referenced;
} else if (type.isMapLikeType()) {
Expand Down Expand Up @@ -469,11 +472,10 @@ private Set<String> findIngoredEnumConstants(JavaType type) {
Set<String> toIgnore = new HashSet<>();
for (Field field : fields) {
if (field.isEnumConstant() && field.getAnnotation(JsonIgnore.class) != null) {
// hack to figure out the enum constant
// hack to figure out the enum constant - this guards against some using both JsonIgnore and JsonProperty
try {
Object value = field.get(null);
toIgnore.add(resolvingContext.kubernetesSerialization
.unmarshal(resolvingContext.kubernetesSerialization.asJson(value), String.class));
toIgnore.add(resolvingContext.objectMapper.convertValue(value, String.class));
} catch (IllegalArgumentException | IllegalAccessException e) {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
Expand All @@ -53,6 +56,8 @@ public class CRDGenerator {
private final Map<String, AbstractCustomResourceHandler> handlers = new HashMap<>(2);
private CRDOutput<? extends OutputStream> output;
private boolean parallel;
private boolean implicitPreserveUnknownFields;
private ObjectMapper objectMapper;
private Map<String, CustomResourceInfo> infos;

// TODO: why not rely on the standard fabric8 yaml mapping
Expand All @@ -77,11 +82,21 @@ public CRDGenerator withOutput(CRDOutput<? extends OutputStream> output) {
return this;
}

public CRDGenerator withImplicitPreserveUnknownFields(boolean implicitPreserveUnknownFields) {
this.implicitPreserveUnknownFields = implicitPreserveUnknownFields;
return this;
}

public CRDGenerator withParallelGenerationEnabled(boolean parallel) {
this.parallel = parallel;
return this;
}

public CRDGenerator withObjectMapper(ObjectMapper mapper) {
this.objectMapper = mapper;
return this;
}

public CRDGenerator forCRDVersions(List<String> versions) {
return versions != null && !versions.isEmpty() ? forCRDVersions(versions.toArray(new String[0]))
: this;
Expand Down Expand Up @@ -154,6 +169,13 @@ public CRDGenerationInfo detailedGenerate() {
forCRDVersions(CustomResourceHandler.VERSION);
}

final ResolvingContext context;
if (this.objectMapper == null) {
context = ResolvingContext.defaultResolvingContext(implicitPreserveUnknownFields);
} else {
context = new ResolvingContext(this.objectMapper, implicitPreserveUnknownFields);
}

for (CustomResourceInfo info : infos.values()) {
if (info != null) {
if (LOGGER.isInfoEnabled()) {
Expand All @@ -162,7 +184,13 @@ public CRDGenerationInfo detailedGenerate() {
info.specClassName().orElse("undetermined"),
info.statusClassName().orElse("undetermined"));
}
handlers.values().forEach(h -> h.handle(info));

if (parallel) {
handlers.values().stream().map(h -> ForkJoinPool.commonPool().submit(() -> h.handle(info, context.forkContext())))
.collect(Collectors.toList()).stream().forEach(ForkJoinTask::join);
} else {
handlers.values().stream().forEach(h -> h.handle(info, context));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
Expand All @@ -31,10 +30,15 @@
import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Encapsulates the stateful Jackson details that allow for crd to be fully resolved by our logic
* - holds an association of uris to already generated jackson schemas
* - holds a Jackson SchemaGenerator which is not thread-safe
*/
public class ResolvingContext {

static final class GeneratorObjectSchema extends ObjectSchema {
Expand Down Expand Up @@ -76,15 +80,15 @@ public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) {
// so that we may directly mark preserve unknown
JsonObjectFormatVisitor result = super.expectObjectFormat(convertedType);
((GeneratorObjectSchema) schema).javaType = convertedType;
seen.putIfAbsent(this.visitorContext.getSeenSchemaUri(convertedType), (GeneratorObjectSchema) schema);
uriToJacksonSchema.putIfAbsent(this.visitorContext.getSeenSchemaUri(convertedType), (GeneratorObjectSchema) schema);
return result;
}
}

final JsonSchemaGenerator generator;
final SerializationConfig serializationConfig;
final KubernetesSerialization kubernetesSerialization;
final Map<String, GeneratorObjectSchema> seen = new HashMap<>();
final ObjectMapper objectMapper;
final Map<String, GeneratorObjectSchema> uriToJacksonSchema;
final boolean implicitPreserveUnknownFields;

private static class AccessibleKubernetesSerialization extends KubernetesSerialization {

Expand All @@ -97,16 +101,26 @@ public ObjectMapper getMapper() {

private static AccessibleKubernetesSerialization DEFAULT_KUBERNETES_SERIALIZATION;

public static ResolvingContext defaultResolvingContext() {
public static ResolvingContext defaultResolvingContext(boolean implicitPreserveUnknownFields) {
if (DEFAULT_KUBERNETES_SERIALIZATION == null) {
DEFAULT_KUBERNETES_SERIALIZATION = new AccessibleKubernetesSerialization();
}
return new ResolvingContext(DEFAULT_KUBERNETES_SERIALIZATION.getMapper(), DEFAULT_KUBERNETES_SERIALIZATION);
return new ResolvingContext(DEFAULT_KUBERNETES_SERIALIZATION.getMapper(), implicitPreserveUnknownFields);
}

public ResolvingContext forkContext() {
return new ResolvingContext(objectMapper, uriToJacksonSchema, implicitPreserveUnknownFields);
}

public ResolvingContext(ObjectMapper mapper, boolean implicitPreserveUnknownFields) {
this(mapper, new ConcurrentHashMap<>(), implicitPreserveUnknownFields);
}

public ResolvingContext(ObjectMapper mapper, KubernetesSerialization kubernetesSerialization) {
serializationConfig = mapper.getSerializationConfig();
this.kubernetesSerialization = kubernetesSerialization;
private ResolvingContext(ObjectMapper mapper, Map<String, GeneratorObjectSchema> uriToJacksonSchema,
boolean implicitPreserveUnknownFields) {
this.uriToJacksonSchema = uriToJacksonSchema;
this.objectMapper = mapper;
this.implicitPreserveUnknownFields = implicitPreserveUnknownFields;
generator = new JsonSchemaGenerator(mapper, new WrapperFactory() {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,24 @@

import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CustomResourceHandler extends AbstractCustomResourceHandler {

private List<CustomResourceDefinition> crds = new CopyOnWriteArrayList<>();
private Queue<CustomResourceDefinition> crds = new ConcurrentLinkedQueue<>();

public static final String VERSION = "v1";

@Override
public void handle(CustomResourceInfo config) {
public void handle(CustomResourceInfo config, ResolvingContext resolvingContext) {
final String name = config.crdName();
final String version = config.version();

JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), config.definition());
JsonSchema resolver = new JsonSchema(resolvingContext, config.definition());
JSONSchemaProps schema = resolver.getSchema();

CustomResourceDefinitionVersionBuilder builder = new CustomResourceDefinitionVersionBuilder()
Expand Down Expand Up @@ -146,16 +147,16 @@ private CustomResourceDefinition combine(List<CustomResourceDefinition> definiti
.map(CustomResourceDefinitionVersion::getName)
.collect(Collectors.toList());

if (storageVersions.size() > 1) {
if (storageVersions.size() > 1) {
throw new IllegalStateException(String.format(
"'%s' custom resource has versions %s marked as storage. Only one version can be marked as storage per custom resource.",
primary.getMetadata().getName(), storageVersions));
}
}

versions = KubernetesVersionPriority.sortByPriority(versions, CustomResourceDefinitionVersion::getName);
versions = KubernetesVersionPriority.sortByPriority(versions, CustomResourceDefinitionVersion::getName);

//TODO: we could double check that the top-level metadata is consistent across all versions
return new CustomResourceDefinitionBuilder(primary).editSpec().withVersions(versions).endSpec().build();
//TODO: we could double check that the top-level metadata is consistent across all versions
return new CustomResourceDefinitionBuilder(primary).editSpec().withVersions(versions).endSpec().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static class V1JSONSchemaProps extends JSONSchemaProps implements Kuberne
}

public static JSONSchemaProps from(Class<?> definition) {
return new JsonSchema(ResolvingContext.defaultResolvingContext(), definition).getSchema();
return new JsonSchema(ResolvingContext.defaultResolvingContext(false), definition).getSchema();
}

public JsonSchema(ResolvingContext resolvingContext, Class<?> definition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ public enum AnnotatedEnum {
non("N"),
@JsonProperty("oui")
es("O"),
@JsonProperty("foo")
@JsonIgnore
Maybe("Maybe");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.fabric8.crdv2.example.extraction.NestedSchemaSwap;
import io.fabric8.crdv2.example.json.ContainingJson;
import io.fabric8.crdv2.example.person.Person;
import io.fabric8.crdv2.generator.ResolvingContext;
import io.fabric8.kubernetes.api.model.AnyType;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Quantity;
Expand Down Expand Up @@ -188,7 +189,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti

@Test
void shouldProduceKubernetesPreserveFields() {
JSONSchemaProps schema = JsonSchema.from(ContainingJson.class);
JSONSchemaProps schema = new JsonSchema(ResolvingContext.defaultResolvingContext(true), ContainingJson.class).getSchema();
assertNotNull(schema);
Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
final JSONSchemaProps specSchema = properties.get("spec");
Expand Down Expand Up @@ -344,7 +345,7 @@ public void setValues(Map<String, Object> values) {

@Test
void testPreserveUnknown() {
JSONSchemaProps schema = JsonSchema.from(PreserveUnknown.class);
JSONSchemaProps schema = new JsonSchema(ResolvingContext.defaultResolvingContext(true), PreserveUnknown.class).getSchema();
assertNotNull(schema);
assertEquals(0, schema.getProperties().size());
assertEquals(Boolean.TRUE, schema.getXKubernetesPreserveUnknownFields());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ class SpecReplicasPathTest {

@Test
public void shoudDetectSpecReplicasPath() throws Exception {
JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), WebServerWithStatusProperty.class);
JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(false), WebServerWithStatusProperty.class);
Optional<String> path = resolver.getSinglePath(SpecReplicas.class);
assertTrue(path.isPresent());
assertEquals(".replicas", path.get());
}

@Test
public void shoudDetectNestedSpecReplicasPath() throws Exception {
JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(), WebServerWithSpec.class);
JsonSchema resolver = new JsonSchema(ResolvingContext.defaultResolvingContext(false), WebServerWithSpec.class);
Optional<String> path = resolver.getSinglePath(SpecReplicas.class);
assertTrue(path.isPresent());
assertEquals(".spec.replicas", path.get());
Expand Down

0 comments on commit c61f03b

Please sign in to comment.