Skip to content

Commit f5efa47

Browse files
authored
Merge pull request kubernetes-client#1804 from yue9944882/bugfix/crd-props-yaml-codec
Bugfix: CRD's openapi schema extension cannot be properly serialized
2 parents 2aba7e8 + e5b1341 commit f5efa47

3 files changed

Lines changed: 192 additions & 4 deletions

File tree

util/src/main/java/io/kubernetes/client/util/Yaml.java

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,36 @@
1212
*/
1313
package io.kubernetes.client.util;
1414

15+
import com.google.gson.annotations.SerializedName;
1516
import io.kubernetes.client.common.KubernetesType;
1617
import io.kubernetes.client.custom.IntOrString;
1718
import io.kubernetes.client.custom.Quantity;
19+
import io.kubernetes.client.openapi.models.V1JSONSchemaProps;
20+
import io.kubernetes.client.openapi.models.V1beta1JSONSchemaProps;
1821
import java.io.File;
1922
import java.io.FileReader;
2023
import java.io.IOException;
2124
import java.io.Reader;
2225
import java.io.StringReader;
2326
import java.io.Writer;
27+
import java.lang.reflect.Field;
28+
import java.lang.reflect.Method;
2429
import java.time.OffsetDateTime;
2530
import java.util.ArrayList;
31+
import java.util.Arrays;
2632
import java.util.Collections;
2733
import java.util.Comparator;
2834
import java.util.Iterator;
2935
import java.util.List;
3036
import java.util.Map;
37+
import java.util.Optional;
3138
import java.util.Set;
3239
import okio.ByteString;
3340
import org.slf4j.Logger;
3441
import org.slf4j.LoggerFactory;
3542
import org.yaml.snakeyaml.DumperOptions;
43+
import org.yaml.snakeyaml.TypeDescription;
44+
import org.yaml.snakeyaml.constructor.BaseConstructor;
3645
import org.yaml.snakeyaml.constructor.Constructor;
3746
import org.yaml.snakeyaml.constructor.SafeConstructor;
3847
import org.yaml.snakeyaml.introspector.Property;
@@ -44,6 +53,7 @@
4453
import org.yaml.snakeyaml.representer.Represent;
4554
import org.yaml.snakeyaml.representer.Representer;
4655

56+
/** The type Yaml. */
4757
public class Yaml {
4858

4959
static final Logger logger = LoggerFactory.getLogger(Yaml.class);
@@ -359,17 +369,137 @@ protected NodeTuple representJavaBeanProperty(
359369
}
360370
}
361371

362-
/** @return An instantiated SnakeYaml Object. */
372+
/**
373+
* Instantiate a snake yaml with a default {@link SafeConstructor}.
374+
*
375+
* <p>DEPRECATED: Use the parameterized "getSnakeYaml" constructing method below to get rid of
376+
* vulnerability from dynamic type serialization.
377+
*
378+
* @return the snake yaml
379+
*/
363380
@Deprecated
364381
public static org.yaml.snakeyaml.Yaml getSnakeYaml() {
365382
return getSnakeYaml(null);
366383
}
367384

368-
private static org.yaml.snakeyaml.Yaml getSnakeYaml(Class<?> type) {
385+
/**
386+
* Instantiate a snake yaml with the target model type specified..
387+
*
388+
* @param type the target model type
389+
* @param typeDescriptions additional type descriptions for customizing the serializer
390+
* @return the new snake yaml instance
391+
*/
392+
public static org.yaml.snakeyaml.Yaml getSnakeYaml(
393+
Class<?> type, TypeDescription... typeDescriptions) {
394+
BaseConstructor constructor = new SafeConstructor();
395+
Representer representer = new CustomRepresenter();
369396
if (type != null) {
370-
return new org.yaml.snakeyaml.Yaml(new CustomConstructor(type), new CustomRepresenter());
397+
constructor = new CustomConstructor(type);
398+
}
399+
// register builtin type descriptions
400+
registerBuiltinGsonCompatibles(constructor, representer);
401+
for (TypeDescription desc : typeDescriptions) {
402+
registerCustomTypeDescriptions(constructor, representer, desc);
403+
}
404+
return new org.yaml.snakeyaml.Yaml(constructor, representer);
405+
}
406+
407+
/**
408+
* Instantiate a new {@link TypeDescription} which will load the {@link SerializedName} via
409+
* reflection so that yaml serialization can work for the custom gson serialized name.
410+
*
411+
* @param modelClass the kubenretes api model class
412+
* @param gsonTaggedFields the custom serialized names tagged by gson
413+
* @return the type description
414+
*/
415+
public static TypeDescription newGsonCompatibleTypeDescription(
416+
Class modelClass, String... gsonTaggedFields) {
417+
TypeDescription desc = new TypeDescription(modelClass);
418+
List<String> excluding = new ArrayList<>();
419+
for (String targetGsonAnnotation : gsonTaggedFields) {
420+
Field field =
421+
Arrays.stream(modelClass.getDeclaredFields())
422+
.filter(f -> f.getAnnotation(SerializedName.class) != null)
423+
.filter(
424+
f -> targetGsonAnnotation.equals(f.getAnnotation(SerializedName.class).value()))
425+
.findAny()
426+
.orElseThrow(
427+
() ->
428+
new IllegalArgumentException(
429+
"Api model class "
430+
+ modelClass.getSimpleName()
431+
+ " doesn't have field with Gson @SerializedName with value "
432+
+ targetGsonAnnotation));
433+
Method getterMethod =
434+
tryFindGetterMethod(modelClass, field)
435+
.orElseThrow(
436+
() ->
437+
new IllegalArgumentException(
438+
"Cannot find getter method for "
439+
+ targetGsonAnnotation
440+
+ " on api model class "
441+
+ modelClass.getSimpleName()));
442+
Method setterMethod =
443+
tryFindSetterMethod(modelClass, field)
444+
.orElseThrow(
445+
() ->
446+
new IllegalArgumentException(
447+
"Cannot find setter method for "
448+
+ targetGsonAnnotation
449+
+ " on api model class "
450+
+ modelClass.getSimpleName()));
451+
452+
desc.substituteProperty(
453+
targetGsonAnnotation, field.getType(), getterMethod.getName(), setterMethod.getName());
454+
excluding.add(field.getName());
371455
}
372-
return new org.yaml.snakeyaml.Yaml(new SafeConstructor(), new CustomRepresenter());
456+
desc.setExcludes(excluding.toArray(new String[0]));
457+
return desc;
458+
}
459+
460+
private static void registerBuiltinGsonCompatibles(
461+
BaseConstructor constructor, Representer representer) {
462+
// TODO: Are there more builtin api classes need these explicit substitution below
463+
String[] crdOpenApiExtensions =
464+
new String[] {
465+
"x-kubernetes-embedded-resource",
466+
"x-kubernetes-int-or-string",
467+
"x-kubernetes-list-map-keys",
468+
"x-kubernetes-list-type",
469+
"x-kubernetes-map-type",
470+
"x-kubernetes-preserve-unknown-fields",
471+
};
472+
registerCustomTypeDescriptions(
473+
constructor,
474+
representer,
475+
newGsonCompatibleTypeDescription(V1JSONSchemaProps.class, crdOpenApiExtensions),
476+
newGsonCompatibleTypeDescription(V1beta1JSONSchemaProps.class, crdOpenApiExtensions));
477+
}
478+
479+
private static void registerCustomTypeDescriptions(
480+
BaseConstructor constructor,
481+
Representer representer,
482+
TypeDescription... addtionalTypeDescriptions) {
483+
Arrays.stream(addtionalTypeDescriptions)
484+
.forEach(
485+
desc -> {
486+
constructor.addTypeDescription(desc);
487+
representer.addTypeDescription(desc);
488+
});
489+
}
490+
491+
private static Optional<Method> tryFindGetterMethod(Class modelClass, Field targetField) {
492+
return Arrays.stream(modelClass.getDeclaredMethods())
493+
.filter(f -> f.getName().startsWith("get"))
494+
.filter(f -> f.getName().equalsIgnoreCase("get" + targetField.getName()))
495+
.findAny();
496+
}
497+
498+
private static Optional<Method> tryFindSetterMethod(Class modelClass, Field targetField) {
499+
return Arrays.stream(modelClass.getDeclaredMethods())
500+
.filter(f -> f.getName().startsWith("set"))
501+
.filter(f -> f.getName().equalsIgnoreCase("set" + targetField.getName()))
502+
.findAny();
373503
}
374504

375505
/**

util/src/test/java/io/kubernetes/client/util/YamlTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
import static org.hamcrest.CoreMatchers.equalTo;
1717
import static org.junit.Assert.assertEquals;
1818
import static org.junit.Assert.assertFalse;
19+
import static org.junit.Assert.assertNotNull;
1920
import static org.junit.Assert.assertNull;
2021
import static org.junit.Assert.assertThat;
2122
import static org.junit.Assert.assertTrue;
2223

2324
import io.kubernetes.client.Resources;
2425
import io.kubernetes.client.common.KubernetesType;
26+
import io.kubernetes.client.openapi.models.V1CustomResourceDefinition;
2527
import io.kubernetes.client.openapi.models.V1Deployment;
2628
import io.kubernetes.client.openapi.models.V1ObjectMeta;
2729
import io.kubernetes.client.openapi.models.V1Pod;
@@ -49,6 +51,8 @@ public class YamlTest {
4951

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

54+
private static final URL CRD_INT_OR_STRING_FILE = Resources.getResource("crd-int-or-string.yaml");
55+
5256
private static final String[] kinds =
5357
new String[] {
5458
"Pod",
@@ -280,4 +284,22 @@ public void testLoadAsYamlCantConstructObjects() {
280284
}
281285
assertFalse("Object should not be constructed!", TestPoJ.hasBeenConstructed());
282286
}
287+
288+
@Test
289+
public void testLoadDumpCRDWithIntOrStringExtension() {
290+
String data = Resources.toString(CRD_INT_OR_STRING_FILE, UTF_8);
291+
V1CustomResourceDefinition crd = Yaml.loadAs(data, V1CustomResourceDefinition.class);
292+
assertNotNull(crd);
293+
assertTrue(
294+
crd.getSpec()
295+
.getVersions()
296+
.get(0)
297+
.getSchema()
298+
.getOpenAPIV3Schema()
299+
.getProperties()
300+
.get("foo")
301+
.getxKubernetesIntOrString());
302+
String dumped = Yaml.dump(crd);
303+
assertEquals(data, dumped);
304+
}
283305
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: crontabs.stable.example.com
5+
spec:
6+
group: stable.example.com
7+
names:
8+
kind: CronTab
9+
plural: crontabs
10+
shortNames:
11+
- ct
12+
singular: crontab
13+
scope: Namespaced
14+
versions:
15+
- name: v1
16+
schema:
17+
openAPIV3Schema:
18+
type: object
19+
properties:
20+
foo:
21+
anyOf:
22+
- type: integer
23+
- type: string
24+
pattern: ^.*
25+
x-kubernetes-int-or-string: true
26+
spec:
27+
type: object
28+
properties:
29+
cronSpec:
30+
type: string
31+
image:
32+
type: string
33+
replicas:
34+
type: integer
35+
served: true
36+
storage: true

0 commit comments

Comments
 (0)