Skip to content

Commit

Permalink
fix fabric8io#3972: moving template handling to a custom deserializer
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Jul 22, 2022
1 parent 11d20aa commit d9f03a4
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.utils.serialization.UnmatchedFieldTypeModule;
import io.fabric8.kubernetes.model.jackson.UnmatchedFieldTypeModule;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

Expand Down Expand Up @@ -422,17 +421,8 @@ public static <T> T clone(T resource) {
// if full serialization seems too expensive, there is also
//return (T) JSON_MAPPER.convertValue(resource, resource.getClass());
try {
return JSON_MAPPER.readValue(
JSON_MAPPER.writeValueAsString(resource), new TypeReference<T>() {
@Override
public Type getType() {
if (resource instanceof KubernetesResource) {
// Force KubernetesResource so that the KubernetesDeserializer takes over any resource configured deserializer
return resource instanceof GenericKubernetesResource ? resource.getClass() : KubernetesResource.class;
}
return resource.getClass();
}
});
return (T) JSON_MAPPER.readValue(
JSON_MAPPER.writeValueAsString(resource), resource.getClass());
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
Expand Down
4 changes: 4 additions & 0 deletions kubernetes-model-generator/kubernetes-model-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.fabric8.kubernetes.model.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ TYPE })
@Retention(RUNTIME)
public @interface Resource {

/**
* Allows to specify which API group the annotated class is defined under. Together with version, this
* determines the `apiVersion` field associated with the annotated resource.
* See https://kubernetes.io/docs/reference/using-api/#api-groups for more details.
*/
String group();

/**
* Specifies the kind value should be used to refer to instance of the annotated class. If not provided,
* a default value is computed based on the annotated class name. See HasMetadata#getKind for more details.
*/
String kind() default "";

/**
* Specifies the plural form associated with a Custom Resource. If not provided, it will default to a computed value.
* See HasMetadata#getPlural for more details.
*/
String plural() default "";

boolean namespaced() default true;

String version();

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.utils.serialization;
package io.fabric8.kubernetes.model.jackson;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
Expand All @@ -29,10 +29,12 @@
* Variant of {@link BeanPropertyWriter} which prevents property values present in the {@link AnnotatedMember} anyGetter
* to be serialized twice.
*
* <p> Any property that's present in the anyGetter is ignored upon serialization. The values present in the anyGetter
* <p>
* Any property that's present in the anyGetter is ignored upon serialization. The values present in the anyGetter
* take precedence over those stored in the Bean's fields.
*
* <p> This BeanPropertyWriter implementation is intended to be used in combination with
* <p>
* This BeanPropertyWriter implementation is intended to be used in combination with
* the {@link SettableBeanPropertyDelegate} to allow the propagation of deserialized properties that don't match the
* target field types.
*/
Expand Down Expand Up @@ -64,7 +66,7 @@ public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider
delegate.serializeAsField(bean, gen, prov);
} else if (Boolean.TRUE.equals(logDuplicateWarning.get())) {
logger.warn("Value in field '{}' ignored in favor of value in additionalProperties ({}) for {}",
delegate.getName(), valueInAnyGetter, bean.getClass().getName());
delegate.getName(), valueInAnyGetter, bean.getClass().getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.utils.serialization;
package io.fabric8.kubernetes.model.jackson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationConfig;
Expand All @@ -25,7 +25,6 @@
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import io.fabric8.kubernetes.internal.KubernetesDeserializer;

import java.io.IOException;
import java.lang.annotation.Annotation;
Expand All @@ -34,44 +33,45 @@
/**
* This concrete sub-class encapsulates a {@link SettableBeanProperty} delegate that is always tried first.
*
* <p> A fall-back mechanism is implemented in the deserializeAndSet methods to allow field values that don't match the
* <p>
* A fall-back mechanism is implemented in the deserializeAndSet methods to allow field values that don't match the
* target type to be preserved in the anySetter method if exists.
*/
public class SettableBeanPropertyDelegate extends SettableBeanProperty {

private final SettableBeanProperty delegate;
private final SettableAnyProperty anySetter;
private final transient Supplier<Boolean> restrictToTemplates;
private final transient Supplier<Boolean> useAnySetter;

SettableBeanPropertyDelegate(SettableBeanProperty delegate, SettableAnyProperty anySetter, Supplier<Boolean> restrictToTemplates) {
SettableBeanPropertyDelegate(SettableBeanProperty delegate, SettableAnyProperty anySetter, Supplier<Boolean> useAnySetter) {
super(delegate);
this.delegate = delegate;
this.anySetter = anySetter;
this.restrictToTemplates = restrictToTemplates;
this.useAnySetter = useAnySetter;
}

/**
* {@inheritDoc}
*/
@Override
public SettableBeanProperty withValueDeserializer(JsonDeserializer<?> deser) {
return new SettableBeanPropertyDelegate(delegate.withValueDeserializer(deser), anySetter, restrictToTemplates);
return new SettableBeanPropertyDelegate(delegate.withValueDeserializer(deser), anySetter, useAnySetter);
}

/**
* {@inheritDoc}
*/
@Override
public SettableBeanProperty withName(PropertyName newName) {
return new SettableBeanPropertyDelegate(delegate.withName(newName), anySetter, restrictToTemplates);
return new SettableBeanPropertyDelegate(delegate.withName(newName), anySetter, useAnySetter);
}

/**
* {@inheritDoc}
*/
@Override
public SettableBeanProperty withNullProvider(NullValueProvider nva) {
return new SettableBeanPropertyDelegate(delegate.withNullProvider(nva), anySetter, restrictToTemplates);
return new SettableBeanPropertyDelegate(delegate.withNullProvider(nva), anySetter, useAnySetter);
}

/**
Expand Down Expand Up @@ -117,13 +117,16 @@ public boolean isIgnorable() {
/**
* Method called to deserialize appropriate value, given parser (and context), and set it using appropriate mechanism.
*
* <p> Deserialization is first tried through the delegate. In case a {@link MismatchedInputException} is caught,
* <p>
* Deserialization is first tried through the delegate. In case a {@link MismatchedInputException} is caught,
* the field is stored in the bean's {@link SettableAnyProperty} anySetter field if it exists.
*
* <p> This allows deserialization processes propagate values that initially don't match the target bean type for the
* <p>
* This allows deserialization processes propagate values that initially don't match the target bean type for the
* applicable field.
*
* <p> An example use-case is the use of placeholders (e.g. {@code ${aValue}}) in a field.
* <p>
* An example use-case is the use of placeholders (e.g. {@code ${aValue}}) in a field.
*/
@Override
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException {
Expand Down Expand Up @@ -171,9 +174,6 @@ private boolean shouldUseAnySetter() {
if (anySetter == null) {
return false;
}
if (Boolean.TRUE.equals(restrictToTemplates.get()) ) {
return KubernetesDeserializer.isInTemplate();
}
return true;
return Boolean.TRUE.equals(useAnySetter.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.utils.serialization;
package io.fabric8.kubernetes.model.jackson;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
Expand All @@ -40,6 +40,8 @@ public class UnmatchedFieldTypeModule extends SimpleModule {
private boolean logWarnings;
private boolean restrictToTemplates;

private static final ThreadLocal<Boolean> IN_TEMPLATE = ThreadLocal.withInitial(() -> false);

public UnmatchedFieldTypeModule() {
this(true, true);
}
Expand All @@ -50,19 +52,22 @@ public UnmatchedFieldTypeModule(boolean logWarnings, boolean restrictToTemplates
setDeserializerModifier(new BeanDeserializerModifier() {

@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(p ->
builder.addOrReplaceProperty(new SettableBeanPropertyDelegate(p, builder.getAnySetter(), UnmatchedFieldTypeModule.this::isRestrictToTemplates) {
}, true));
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(p -> builder.addOrReplaceProperty(
new SettableBeanPropertyDelegate(p, builder.getAnySetter(), UnmatchedFieldTypeModule.this::useAnySetter) {
}, true));
return builder;
}
});
setSerializerModifier(new BeanSerializerModifier() {
@Override
public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc, BeanSerializerBuilder builder) {
builder.setProperties(builder.getProperties().stream().map(p ->
new BeanPropertyWriterDelegate(p, builder.getBeanDescription().findAnyGetter(), UnmatchedFieldTypeModule.this::isLogWarnings))
.collect(Collectors.toList()));
public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc,
BeanSerializerBuilder builder) {
builder.setProperties(builder.getProperties().stream()
.map(p -> new BeanPropertyWriterDelegate(p, builder.getBeanDescription().findAnyGetter(),
UnmatchedFieldTypeModule.this::isLogWarnings))
.collect(Collectors.toList()));
return builder;
}
});
Expand All @@ -85,6 +90,10 @@ boolean isRestrictToTemplates() {
return restrictToTemplates;
}

boolean useAnySetter() {
return !restrictToTemplates || isInTemplate();
}

/**
* Sets if the DeserializerModifier should only be applied to Templates or object trees contained in Templates.
*
Expand All @@ -93,4 +102,17 @@ boolean isRestrictToTemplates() {
public void setRestrictToTemplates(boolean restrictToTemplates) {
this.restrictToTemplates = restrictToTemplates;
}

public static boolean isInTemplate() {
return Boolean.TRUE.equals(IN_TEMPLATE.get());
}

public static void setInTemplate() {
IN_TEMPLATE.set(true);
}

public static void removeInTemplate() {
IN_TEMPLATE.remove();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.utils.serialization;
package io.fabric8.kubernetes.model.jackson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.deser.SettableAnyProperty;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.fabric8.kubernetes.model.jackson.SettableBeanPropertyDelegate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -46,7 +47,7 @@ class SettableBeanPropertyDelegateTest {
void setUp() {
delegateMock = mock(SettableBeanProperty.class, RETURNS_DEEP_STUBS);
anySetterMock = mock(SettableAnyProperty.class);
settableBeanPropertyDelegate = new SettableBeanPropertyDelegate(delegateMock, anySetterMock, () -> false);
settableBeanPropertyDelegate = new SettableBeanPropertyDelegate(delegateMock, anySetterMock, () -> true);
}

@Test
Expand All @@ -58,10 +59,10 @@ void withValueDeserializer() {
final SettableBeanProperty result = settableBeanPropertyDelegate.withValueDeserializer(null);
// Then
assertThat(result)
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
}

@Test
Expand All @@ -73,10 +74,10 @@ void withName() {
final SettableBeanProperty result = settableBeanPropertyDelegate.withName(null);
// Then
assertThat(result)
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
}

@Test
Expand All @@ -88,10 +89,10 @@ void withNullProvider() {
final SettableBeanProperty result = settableBeanPropertyDelegate.withNullProvider(null);
// Then
assertThat(result)
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
.isInstanceOf(SettableBeanPropertyDelegate.class)
.isNotSameAs(settableBeanPropertyDelegate)
.hasFieldOrPropertyWithValue("anySetter", anySetterMock)
.hasFieldOrPropertyWithValue("delegate", delegateMock);
}

@Test
Expand Down Expand Up @@ -186,9 +187,9 @@ void deserializeSetAndReturnWithException() throws IOException {
final Object instance = new Object();
when(delegateMock.getName()).thenReturn("the-property");
when(delegateMock.deserializeSetAndReturn(any(), any(), eq(instance)))
.thenThrow(MismatchedInputException.from(null, Integer.class, "The Mocked Exception"));
.thenThrow(MismatchedInputException.from(null, Integer.class, "The Mocked Exception"));
doThrow(MismatchedInputException.from(null, Integer.class, "The Mocked Exception"))
.when(delegateMock).deserializeAndSet(any(), any(), eq(instance));
.when(delegateMock).deserializeAndSet(any(), any(), eq(instance));
// When
final Object result = settableBeanPropertyDelegate.deserializeSetAndReturn(mock(JsonParser.class), null, instance);
// Then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client.utils.serialization;
package io.fabric8.kubernetes.model.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -22,6 +22,7 @@
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.model.jackson.UnmatchedFieldTypeModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down
Loading

0 comments on commit d9f03a4

Please sign in to comment.