Skip to content

Commit

Permalink
fixes a bug that CRD's openapi extension cannot be serialzied
Browse files Browse the repository at this point in the history
  • Loading branch information
yue9944882 committed Jul 28, 2021
1 parent 9af5889 commit 287455c
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 2 deletions.
96 changes: 94 additions & 2 deletions util/src/main/java/io/kubernetes/client/util/Yaml.java
Expand Up @@ -12,27 +12,36 @@
*/
package io.kubernetes.client.util;

import com.google.gson.annotations.SerializedName;
import io.kubernetes.client.common.KubernetesType;
import io.kubernetes.client.custom.IntOrString;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.models.V1JSONSchemaProps;
import io.kubernetes.client.openapi.models.V1beta1JSONSchemaProps;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.introspector.Property;
Expand Down Expand Up @@ -366,10 +375,93 @@ public static org.yaml.snakeyaml.Yaml getSnakeYaml() {
}

private static org.yaml.snakeyaml.Yaml getSnakeYaml(Class<?> type) {
BaseConstructor constructor = new SafeConstructor();
Representer representer = new CustomRepresenter();
if (type != null) {
return new org.yaml.snakeyaml.Yaml(new CustomConstructor(type), new CustomRepresenter());
constructor = new CustomConstructor(type);
}
return new org.yaml.snakeyaml.Yaml(new SafeConstructor(), new CustomRepresenter());
substituteProperties(constructor, representer);
return new org.yaml.snakeyaml.Yaml(constructor, representer);
}

private static void substituteProperties(BaseConstructor constructor, Representer representer) {
substituteCRDOpenApiSchemaProps(V1JSONSchemaProps.class, constructor, representer);
substituteCRDOpenApiSchemaProps(V1beta1JSONSchemaProps.class, constructor, representer);
}

private static void substituteCRDOpenApiSchemaProps(
Class jsonPropsClass, BaseConstructor constructor, Representer representer) {
// TODO: Are there more api classes need these explicit substitution below
TypeDescription desc =
substitudePropertiesWithGsonAnnotation(
jsonPropsClass,
"x-kubernetes-embedded-resource",
"x-kubernetes-int-or-string",
"x-kubernetes-list-map-keys",
"x-kubernetes-list-type",
"x-kubernetes-map-type",
"x-kubernetes-preserve-unknown-fields");
constructor.addTypeDescription(desc);
representer.addTypeDescription(desc);
}

private static TypeDescription substitudePropertiesWithGsonAnnotation(
Class modelClass, String... targetGsonAnnotations) {
TypeDescription desc = new TypeDescription(modelClass);
List<String> excluding = new ArrayList<>();
for (String targetGsonAnnotation : targetGsonAnnotations) {
Field field =
Arrays.stream(modelClass.getDeclaredFields())
.filter(f -> f.getAnnotation(SerializedName.class) != null)
.filter(
f -> targetGsonAnnotation.equals(f.getAnnotation(SerializedName.class).value()))
.findAny()
.orElseThrow(
() ->
new IllegalArgumentException(
"Api model class "
+ modelClass.getSimpleName()
+ " doesn't have field with Gson @SerializedName with value "
+ targetGsonAnnotation));
Method getterMethod =
tryFindGetterMethod(modelClass, field)
.orElseThrow(
() ->
new IllegalArgumentException(
"Cannot find getter method for "
+ targetGsonAnnotation
+ " on api model class "
+ modelClass.getSimpleName()));
Method setterMethod =
tryFindSetterMethod(modelClass, field)
.orElseThrow(
() ->
new IllegalArgumentException(
"Cannot find setter method for "
+ targetGsonAnnotation
+ " on api model class "
+ modelClass.getSimpleName()));

desc.substituteProperty(
targetGsonAnnotation, field.getType(), getterMethod.getName(), setterMethod.getName());
excluding.add(field.getName());
}
desc.setExcludes(excluding.toArray(new String[0]));
return desc;
}

private static Optional<Method> tryFindGetterMethod(Class modelClass, Field targetField) {
return Arrays.stream(modelClass.getDeclaredMethods())
.filter(f -> f.getName().startsWith("get"))
.filter(f -> f.getName().equalsIgnoreCase("get" + targetField.getName()))
.findAny();
}

private static Optional<Method> tryFindSetterMethod(Class modelClass, Field targetField) {
return Arrays.stream(modelClass.getDeclaredMethods())
.filter(f -> f.getName().startsWith("set"))
.filter(f -> f.getName().equalsIgnoreCase("set" + targetField.getName()))
.findAny();
}

/**
Expand Down
22 changes: 22 additions & 0 deletions util/src/test/java/io/kubernetes/client/util/YamlTest.java
Expand Up @@ -16,12 +16,14 @@
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import io.kubernetes.client.Resources;
import io.kubernetes.client.common.KubernetesType;
import io.kubernetes.client.openapi.models.V1CustomResourceDefinition;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Pod;
Expand Down Expand Up @@ -49,6 +51,8 @@ public class YamlTest {

private static final URL TAGGED_FILE = Resources.getResource("pod-tag.yaml");

private static final URL CRD_INT_OR_STRING_FILE = Resources.getResource("crd-int-or-string.yaml");

private static final String[] kinds =
new String[] {
"Pod",
Expand Down Expand Up @@ -280,4 +284,22 @@ public void testLoadAsYamlCantConstructObjects() {
}
assertFalse("Object should not be constructed!", TestPoJ.hasBeenConstructed());
}

@Test
public void testLoadDumpCRDWithIntOrStringExtension() {
String data = Resources.toString(CRD_INT_OR_STRING_FILE, UTF_8);
V1CustomResourceDefinition crd = Yaml.loadAs(data, V1CustomResourceDefinition.class);
assertNotNull(crd);
assertTrue(
crd.getSpec()
.getVersions()
.get(0)
.getSchema()
.getOpenAPIV3Schema()
.getProperties()
.get("foo")
.getxKubernetesIntOrString());
String dumped = Yaml.dump(crd);
assertEquals(data, dumped);
}
}
36 changes: 36 additions & 0 deletions util/src/test/resources/crd-int-or-string.yaml
@@ -0,0 +1,36 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.stable.example.com
spec:
group: stable.example.com
names:
kind: CronTab
plural: crontabs
shortNames:
- ct
singular: crontab
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
type: object
properties:
foo:
anyOf:
- type: integer
- type: string
pattern: ^.*
x-kubernetes-int-or-string: true
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
served: true
storage: true

0 comments on commit 287455c

Please sign in to comment.