Skip to content

Commit

Permalink
fix: Stack overflow on multi maps (and other non-trivial generic clas…
Browse files Browse the repository at this point in the history
…ses)

Logic in sundrio that is responsible for replacing generic type arguments on methods and properties with their concrete instantiations from subclasses contained a bug, in which it looped infinitely if the same type argument name was used at multiple classes in the hierarchy. A common occurence were multimaps.

A very similar code was also present in crd-generator, also suffering from the same bug.

Sundrio has been updated to account for this situation, and updated utilities from sundrio have been introduced to crd-generator, which replace the previous implementation of teh same algorithm.

Fixes fabric8io#4357
  • Loading branch information
xRodney committed Oct 12, 2022
1 parent 177de1c commit eed73e1
Show file tree
Hide file tree
Showing 5 changed files with 25 additions and 55 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#### Bugs
* Fix #4369: Informers will retry with a backoff on list/watch failure as they did in 5.12 and prior.
* Fix #4350: SchemaSwap annotation is now repeatable and is applied multiple times if classes are used more than once in the class hierarchy.
* Fix #4413: Stack overflow on multi maps (and other non-trivial generic classes)
* Fix #3733: The authentication command from the .kube/config won't be discarded if no arguments are specified
* Fix #4441: corrected patch base handling for the patch methods available from a Resource - resource(item).patch() will be evaluated as resource(latest).patch(item). Also undeprecated patch(item), which is consistent with leaving patch(context, item) undeprecated as well. For consistency with the other operations (such as edit), patch(item) will use the context item as the base when available, or the server side item when not. This means that patch(item) is only the same as resource(item).patch() when the patch(item) is called when the context item is missing or is the same as the latest.
* Fix #4442: TokenRefreshInterceptor doesn't overwrite existing OAuth token with empty string
Expand All @@ -25,6 +26,7 @@
* Fix #4243: Update Tekton triggers model to v0.20.2
* Fix #4383: bump snakeyaml from 1.30 to 1.31
* Fix #4347: Update Kubernetes Model to v1.25.0
* Fix #4413: update sundrio to 0.94

#### New Features
* Fix #4398: add annotation @PreserveUnknownFields for marking generated field have `x-kubernetes-preserve-unknown-fields: true` defined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
import io.sundr.model.*;
import io.sundr.model.functions.GetDefinition;
import io.sundr.model.repo.DefinitionRepository;
import io.sundr.model.visitors.ApplyTypeParamMappingToMethod;
import io.sundr.model.visitors.ApplyTypeParamMappingToProperty;
import io.sundr.model.visitors.ApplyTypeParamMappingToTypeArguments;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -80,6 +84,10 @@ private static TypeDef projectDefinition(ClassRef ref) {
}

List<TypeParamDef> parameters = definition.getParameters();
if (parameters.size() != arguments.size()) {
throw new IllegalStateException("Incompatible reference " + ref + " to " + definition);
}

Map<String, TypeRef> mappings = new HashMap<>();
for (int i = 0; i < arguments.size(); i++) {
String name = parameters.get(i).getName();
Expand All @@ -88,62 +96,12 @@ private static TypeDef projectDefinition(ClassRef ref) {
}

return new TypeDefBuilder(definition)
.accept(mapClassRefArguments(mappings), mapGenericProperties(mappings))
.accept(new ApplyTypeParamMappingToTypeArguments(mappings)) // existing type arguments must be handled before methods and properties
.accept(new ApplyTypeParamMappingToProperty(mappings),
new ApplyTypeParamMappingToMethod(mappings))
.build();
}

/**
* Map generic properties to known {@link TypeRef} based on the specified mappings.
* Example: Given a property {@code T size} and a map containing {@code T -> Integer} the final
* property will be: {@code Integer type}.
* @param mappings A map that maps class arguments names to types.
* @return a visitors that performs the actual mapping.
*/
private static TypedVisitor<PropertyBuilder> mapGenericProperties(Map<String, TypeRef> mappings) {
return new TypedVisitor<PropertyBuilder>() {
@Override
public void visit(PropertyBuilder property) {
TypeRef typeRef = property.buildTypeRef();
if (typeRef instanceof TypeParamRef) {
TypeParamRef typeParamRef = (TypeParamRef) typeRef;
String key = typeParamRef.getName();
TypeRef paramRef = mappings.get(key);
if (paramRef != null) {
property.withTypeRef(paramRef);
}
}
}
};
}

/**
* Map arguments, of {@link ClassRef} instances to known {@link TypeRef} based on the specified mappings.
* Example: Given a class reference to {@code Shape<T>} and a map containing {@code T -> Integer}
* the final reference will be: {@code Shape<Integer> type}.
* @param mappings A map that maps class arguments names to types.
* @return a visitors that performs the actual mapping.
*/
private static TypedVisitor<ClassRefBuilder> mapClassRefArguments(Map<String, TypeRef> mappings) {
return new TypedVisitor<ClassRefBuilder>() {
@Override
public void visit(ClassRefBuilder c) {
List<TypeRef> arguments = new ArrayList<>();
for (TypeRef arg : c.buildArguments()) {
TypeRef mappedRef = arg;
if (arg instanceof TypeParamRef) {
TypeParamRef typeParamRef = (TypeParamRef) arg;
TypeRef mapping = mappings.get(typeParamRef.getName());
if (mapping != null) {
mappedRef = mapping;
}
}
arguments.add(mappedRef);
}
c.withArguments(arguments);
}
};
}

private static Set<ClassRef> projectSuperClasses(TypeDef definition) {
List<ClassRef> extendsList = definition.getExtendsList();
return extendsList.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.fabric8.crd.example.map;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -32,4 +33,7 @@ public Map<String, Map<String, List<Boolean>>> getTest2() {
return test2;
}

private MyMultiMap<String, String> test3;

class MyMultiMap<K, V> extends HashMap<K, List<V>> {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ void mapPropertyShouldHaveCorrectValueType() {
final Map<String, JSONSchemaProps> specProps = version.getSchema().getOpenAPIV3Schema()
.getProperties().get("spec").getProperties();

assertEquals(2, specProps.size());
assertEquals(3, specProps.size());

checkMapProp(specProps, "test", "array");
String arrayType = specProps.get("test").getAdditionalProperties().getSchema().getItems().getSchema().getType();
Expand All @@ -286,6 +286,12 @@ void mapPropertyShouldHaveCorrectValueType() {
String valueType = valueSchema.getType();
assertEquals("array", valueType);

// this check is currently failing, because multimaps are incorrectly processed as if they were normal maps
// (class MultiMap<K,V> implements Map<K,List<V>> is treated like just Map<K,V>)
// checkMapProp(specProps, "test3", "object");
final JSONSchemaProps props = specProps.get("test3");
assertEquals("object", props.getType());

assertEquals("boolean", valueSchema.getItems().getSchema().getType());
});
}
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<!-- Core versions -->
<sundrio.version>0.93.0</sundrio.version>
<sundrio.version>0.94-bugfix-multi-maps3-SNAPSHOT</sundrio.version>
<okhttp.version>3.12.12</okhttp.version>
<okhttp.bundle.version>3.12.1_1</okhttp.bundle.version>
<okio.version>1.15.0</okio.version>
Expand Down

0 comments on commit eed73e1

Please sign in to comment.