Skip to content

Commit d3d1a6a

Browse files
committed
feat: Jackson module to handle Go (de)serialization nuances
Signed-off-by: Marc Nuri <[email protected]>
1 parent 250638e commit d3d1a6a

File tree

20 files changed

+202
-318
lines changed

20 files changed

+202
-318
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
#### Bugs
66
* Fix #6038: Support for Gradle configuration cache
7+
* Fix #6110: VolumeSource (and other file mode fields) in Octal are correctly interpreted
78

89
#### Improvements
910
* Fix #6008: removing the optional dependency on bouncy castle
1011
* Fix #5264: Remove deprecated `Config.errorMessages` field
11-
* Fix #6110: VolumeSource's Octal `defaultMode` notation in yaml not properly converted to json
1212

1313
#### Dependency Upgrade
1414
* Fix #6052: Removed dependency on no longer maintained com.github.mifmif:generex

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesSerialization.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import io.fabric8.kubernetes.api.model.runtime.RawExtension;
4040
import io.fabric8.kubernetes.client.KubernetesClientException;
4141
import io.fabric8.kubernetes.internal.KubernetesDeserializer;
42+
import io.fabric8.kubernetes.model.jackson.GoCompatibilityModule;
4243
import io.fabric8.kubernetes.model.jackson.UnmatchedFieldTypeModule;
4344
import org.snakeyaml.engine.v2.api.Dump;
4445
import org.snakeyaml.engine.v2.api.DumpSettings;
@@ -89,7 +90,7 @@ public KubernetesSerialization(ObjectMapper mapper, boolean searchClassloaders)
8990
}
9091

9192
protected void configureMapper(ObjectMapper mapper) {
92-
mapper.registerModules(new JavaTimeModule(), unmatchedFieldTypeModule);
93+
mapper.registerModules(new JavaTimeModule(), new GoCompatibilityModule(), unmatchedFieldTypeModule);
9394
mapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
9495
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
9596
mapper.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
2121
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
2222
import io.fabric8.kubernetes.api.model.KubernetesResource;
23+
import io.fabric8.kubernetes.model.jackson.GoCompatibilityModule;
2324
import io.fabric8.kubernetes.model.jackson.UnmatchedFieldTypeModule;
2425

2526
import java.io.InputStream;
@@ -88,7 +89,7 @@ public static ObjectMapper yamlMapper() {
8889
if (YAML_MAPPER == null) {
8990
YAML_MAPPER = new ObjectMapper(
9091
new YAMLFactory().disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID));
91-
YAML_MAPPER.registerModules(UNMATCHED_FIELD_TYPE_MODULE);
92+
YAML_MAPPER.registerModules(new GoCompatibilityModule(), UNMATCHED_FIELD_TYPE_MODULE);
9293
}
9394
}
9495
}

kubernetes-client-api/src/test/resources/serialization/cronjob-octal.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ spec:
3737
- name: application-code
3838
configMap:
3939
name: conf
40-
defaultMode: 0555
40+
defaultMode: 0o555
4141
items:
4242
- key: key1
4343
path: target
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,11 @@
1515
*/
1616
package io.fabric8.kubernetes.model.jackson;
1717

18-
import com.fasterxml.jackson.databind.JsonNode;
18+
import com.fasterxml.jackson.databind.module.SimpleModule;
1919

20-
import java.util.regex.Matcher;
21-
import java.util.regex.Pattern;
20+
public class GoCompatibilityModule extends SimpleModule {
2221

23-
public class IntegerOctalHandlerUtil {
24-
private static final Pattern OCTAL_NUMBER = Pattern.compile("(0[oO]?)[0-7]+");
25-
26-
private IntegerOctalHandlerUtil() {
27-
}
28-
29-
public static Integer createIntegerValue(JsonNode node) {
30-
String textValue = node.textValue();
31-
if (textValue != null) {
32-
Matcher octalNumberMatcher = OCTAL_NUMBER.matcher(textValue);
33-
if (octalNumberMatcher.matches()) {
34-
return Integer.valueOf(textValue.substring(octalNumberMatcher.group(1).length()), 8);
35-
}
36-
}
37-
return node.intValue();
22+
public GoCompatibilityModule() {
23+
addDeserializer(Integer.class, new GoIntegerDeserializer());
3824
}
3925
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.model.jackson;
17+
18+
import com.fasterxml.jackson.core.JsonParser;
19+
import com.fasterxml.jackson.databind.BeanProperty;
20+
import com.fasterxml.jackson.databind.DeserializationContext;
21+
import com.fasterxml.jackson.databind.JsonDeserializer;
22+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
23+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
24+
25+
import java.io.IOException;
26+
import java.util.Arrays;
27+
import java.util.HashSet;
28+
import java.util.Set;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
public class GoIntegerDeserializer extends StdDeserializer<Integer> implements ContextualDeserializer {
33+
34+
private static final Pattern OCTAL = Pattern.compile("(0[oO]?)([0-7]+)");
35+
private static final GoIntegerDeserializer APPLICABLE_INSTANCE = new GoIntegerDeserializer(true);
36+
private static final Set<String> APPLICABLE_FIELDS = new HashSet<>(Arrays.asList("mode", "defaultMode"));
37+
38+
private final boolean applicable;
39+
40+
protected GoIntegerDeserializer() {
41+
this(false);
42+
}
43+
44+
private GoIntegerDeserializer(boolean applicable) {
45+
super(Integer.class);
46+
this.applicable = applicable;
47+
}
48+
49+
@Override
50+
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
51+
final String value = p.readValueAs(String.class);
52+
if (value == null) {
53+
return null;
54+
}
55+
if (applicable) {
56+
final Matcher matcher = OCTAL.matcher(value);
57+
if (matcher.find()) {
58+
return Integer.valueOf(matcher.group(2), 8);
59+
}
60+
}
61+
return _parseInteger(ctxt, value);
62+
}
63+
64+
@Override
65+
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
66+
if (APPLICABLE_FIELDS.contains(property.getName())) {
67+
return APPLICABLE_INSTANCE;
68+
}
69+
return this;
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.model.jackson;
17+
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
20+
import lombok.Data;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Nested;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.TestInstance;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
29+
import java.util.stream.Stream;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
33+
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
34+
35+
class GoIntegerDeserializerTest {
36+
37+
private ObjectMapper context;
38+
39+
@BeforeEach
40+
void setUp() {
41+
context = new ObjectMapper();
42+
context.registerModule(new GoCompatibilityModule());
43+
}
44+
45+
@Test
46+
void defaultConstructorDoesntParseOctals() throws Exception {
47+
final Integer result = new GoIntegerDeserializer()
48+
.deserialize(context.createParser("\"0555\""), context.getDeserializationContext());
49+
assertThat(result).isEqualTo(555);
50+
}
51+
52+
@Nested
53+
@TestInstance(PER_CLASS)
54+
class Applicable {
55+
56+
@ParameterizedTest(name = "{index}: with '{'\"{0}\": {1}'}' parses as {2}")
57+
@MethodSource
58+
void parsesOctals(String fieldName, String content, Integer expected) throws Exception {
59+
final IntegerFieldsContainer result = context
60+
.readValue(String.format("{\"%s\": %s}", fieldName, content), IntegerFieldsContainer.class);
61+
assertThat(result).hasFieldOrPropertyWithValue(fieldName, expected);
62+
}
63+
64+
private Stream<Arguments> parsesOctals() {
65+
return Stream.of("mode", "defaultMode")
66+
.flatMap(field -> Stream.of(
67+
Arguments.of(field, "null", null),
68+
Arguments.of(field, "\"0555\"", 365),
69+
Arguments.of(field, "\"0o555\"", 365),
70+
Arguments.of(field, "\"0O555\"", 365),
71+
Arguments.of(field, "\"555\"", 555),
72+
Arguments.of(field, "\"0888\"", 888),
73+
Arguments.of(field, "\"0o12\"", 10),
74+
Arguments.of(field, "\"0O12\"", 10)));
75+
}
76+
}
77+
78+
//
79+
@Nested
80+
class NotApplicable {
81+
82+
@Test
83+
void parsesOctalsAsDecimal() throws Exception {
84+
final IntegerFieldsContainer result = context
85+
.readValue("{\"notApplicable\": \"0555\"}", IntegerFieldsContainer.class);
86+
assertThat(result).hasFieldOrPropertyWithValue("notApplicable", 555);
87+
}
88+
89+
@Test
90+
void throwsExceptionForInvalidOctal() {
91+
assertThatThrownBy(() -> context.readValue("{\"mode\": \"0o955\"}", IntegerFieldsContainer.class))
92+
.isInstanceOf(InvalidFormatException.class);
93+
}
94+
95+
@Test
96+
void throwsExceptionForOctalWithSeparator() {
97+
assertThatThrownBy(() -> context.readValue("{\"notApplicable\": \"0o555\"}", IntegerFieldsContainer.class))
98+
.isInstanceOf(InvalidFormatException.class);
99+
}
100+
}
101+
102+
@Data
103+
private static final class IntegerFieldsContainer {
104+
private Integer mode;
105+
private Integer defaultMode;
106+
private Integer notApplicable;
107+
}
108+
}

kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/IntegerOctalHandlerUtilTest.java

-44
This file was deleted.

kubernetes-model-generator/kubernetes-model-core/cmd/generate/generate.go

-12
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,6 @@ func main() {
152152
Serializer: "io.fabric8.kubernetes.api.model.MicroTimeSerDes.Serializer.class",
153153
Deserializer: "io.fabric8.kubernetes.api.model.MicroTimeSerDes.Deserializer.class",
154154
},
155-
"kubernetes_core_ConfigMapVolumeSource": &schemagen.JavaSerDeDescriptor{
156-
Deserializer: "io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceDeserializer.class",
157-
},
158-
"kubernetes_core_SecretVolumeSource": &schemagen.JavaSerDeDescriptor{
159-
Deserializer: "io.fabric8.kubernetes.api.model.SecretVolumeSourceDeserializer.class",
160-
},
161-
"kubernetes_core_DownwardAPIVolumeSource": &schemagen.JavaSerDeDescriptor{
162-
Deserializer: "io.fabric8.kubernetes.api.model.DownwardAPIVolumeSourceDeserializer.class",
163-
},
164-
"kubernetes_core_ProjectedVolumeSource": &schemagen.JavaSerDeDescriptor{
165-
Deserializer: "io.fabric8.kubernetes.api.model.ProjectedVolumeSourceDeserializer.class",
166-
},
167155
}
168156

169157
for definitionKey, descriptor := range serdes {

kubernetes-model-generator/kubernetes-model-core/src/generated/java/io/fabric8/kubernetes/api/model/ConfigMapVolumeSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import lombok.ToString;
2020
import lombok.experimental.Accessors;
2121

22-
@JsonDeserialize(using = io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceDeserializer.class)
22+
@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
2323
@JsonInclude(JsonInclude.Include.NON_NULL)
2424
@JsonPropertyOrder({
2525
"defaultMode",

kubernetes-model-generator/kubernetes-model-core/src/generated/java/io/fabric8/kubernetes/api/model/DownwardAPIVolumeSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import lombok.ToString;
2020
import lombok.experimental.Accessors;
2121

22-
@JsonDeserialize(using = io.fabric8.kubernetes.api.model.DownwardAPIVolumeSourceDeserializer.class)
22+
@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
2323
@JsonInclude(JsonInclude.Include.NON_NULL)
2424
@JsonPropertyOrder({
2525
"defaultMode",

kubernetes-model-generator/kubernetes-model-core/src/generated/java/io/fabric8/kubernetes/api/model/ProjectedVolumeSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import lombok.ToString;
2020
import lombok.experimental.Accessors;
2121

22-
@JsonDeserialize(using = io.fabric8.kubernetes.api.model.ProjectedVolumeSourceDeserializer.class)
22+
@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
2323
@JsonInclude(JsonInclude.Include.NON_NULL)
2424
@JsonPropertyOrder({
2525
"defaultMode",

kubernetes-model-generator/kubernetes-model-core/src/generated/java/io/fabric8/kubernetes/api/model/SecretVolumeSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import lombok.ToString;
2020
import lombok.experimental.Accessors;
2121

22-
@JsonDeserialize(using = io.fabric8.kubernetes.api.model.SecretVolumeSourceDeserializer.class)
22+
@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
2323
@JsonInclude(JsonInclude.Include.NON_NULL)
2424
@JsonPropertyOrder({
2525
"defaultMode",

0 commit comments

Comments
 (0)