Skip to content

Commit a5ed2a7

Browse files
author
John Daniels
committed
Make sure that we can roundtrip from Yaml and back into Yaml.
Changes made: 1. Added custom Representer to control output 2. Added custom Represent implementations for IntOrString and byte arrays. 3. Made sure that we never output tags for any of our data, since in general Kubernetes doesn't use Yaml tags. 4. Made sure that we don't print null values in our Yaml. 5. Enforce that certain keys, such as apiVersion, that are typically at the top of Kubernetes Yaml files, are outputted that way..
1 parent 2105513 commit a5ed2a7

File tree

3 files changed

+158
-13
lines changed

3 files changed

+158
-13
lines changed

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

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,30 @@
1919
import java.io.IOException;
2020
import java.io.Reader;
2121
import java.io.StringReader;
22-
import java.util.*;
22+
import java.io.Writer;
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.Comparator;
26+
import java.util.HashMap;
27+
import java.util.Iterator;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.Set;
2331
import okio.ByteString;
2432
import org.apache.commons.lang3.tuple.MutablePair;
2533
import org.apache.commons.lang3.tuple.Pair;
2634
import org.slf4j.Logger;
2735
import org.slf4j.LoggerFactory;
36+
import org.yaml.snakeyaml.DumperOptions;
2837
import org.yaml.snakeyaml.constructor.Constructor;
38+
import org.yaml.snakeyaml.introspector.Property;
39+
import org.yaml.snakeyaml.nodes.MappingNode;
2940
import org.yaml.snakeyaml.nodes.Node;
41+
import org.yaml.snakeyaml.nodes.NodeTuple;
3042
import org.yaml.snakeyaml.nodes.ScalarNode;
43+
import org.yaml.snakeyaml.nodes.Tag;
44+
import org.yaml.snakeyaml.representer.Represent;
45+
import org.yaml.snakeyaml.representer.Representer;
3146

3247
public class Yaml {
3348
private static Map<String, Class<?>> classes = new HashMap<>();
@@ -259,6 +274,46 @@ public static List<Object> loadAll(Reader reader) throws IOException {
259274
return list;
260275
}
261276

277+
/**
278+
* Takes an API object and returns a YAML String representing that object.
279+
*
280+
* @param object The API object to dump.
281+
* @return A YAML String representing the API object.
282+
*/
283+
public static String dump(Object object) {
284+
return getSnakeYaml().dump(object);
285+
}
286+
287+
/**
288+
* Takes an API object and writes a YAML string representing that object to the writer.
289+
*
290+
* @param object The API object to dump
291+
* @param writer The writer to write the YAML to.
292+
*/
293+
public static void dump(Object object, Writer writer) {
294+
getSnakeYaml().dump(object, writer);
295+
}
296+
297+
/**
298+
* Takes an Iterator of YAML API objects and returns a YAML string representing all of them
299+
*
300+
* @param data The list of YAML API objects
301+
* @return A String representing the list of YAML API objects.
302+
*/
303+
public static String dumpAll(Iterator<? extends Object> data) {
304+
return getSnakeYaml().dumpAll(data);
305+
}
306+
307+
/**
308+
* Takes an Iterator of YAML API objects and writes a YAML String representing all of them.
309+
*
310+
* @param data The list of YAML API objects.
311+
* @param output The writer to output the YAML String to.
312+
*/
313+
public static void dumpAll(Iterator<? extends Object> data, Writer output) {
314+
getSnakeYaml().dumpAll(data, output);
315+
}
316+
262317
/** Defines constructor logic for custom types in this library. */
263318
public static class CustomConstructor extends Constructor {
264319
@Override
@@ -285,9 +340,93 @@ private byte[] constructByteArray(ScalarNode node) {
285340
}
286341
}
287342

343+
public static class CustomRepresenter extends Representer {
344+
public CustomRepresenter() {
345+
this.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
346+
this.representers.put(IntOrString.class, new RepresentIntOrString());
347+
this.representers.put(byte[].class, new RepresentByteArray());
348+
}
349+
350+
private class RepresentIntOrString implements Represent {
351+
@Override
352+
public Node representData(Object data) {
353+
IntOrString intOrString = (IntOrString) data;
354+
if (intOrString.isInteger()) {
355+
return CustomRepresenter.this.representData(intOrString.getIntValue());
356+
} else {
357+
return CustomRepresenter.this.representData(intOrString.getStrValue());
358+
}
359+
}
360+
}
361+
362+
private class RepresentByteArray implements Represent {
363+
@Override
364+
public Node representData(Object data) {
365+
String value = ByteString.of((byte[]) data).base64();
366+
return representScalar(Tag.STR, value);
367+
}
368+
}
369+
370+
/**
371+
* This returns the ordering of properties that by convention should appear at the beginning of
372+
* a Yaml object in Kubernetes.
373+
*/
374+
private int getPropertyPosition(String property) {
375+
switch (property) {
376+
case "apiVersion":
377+
return 0;
378+
case "kind":
379+
return 1;
380+
case "metadata":
381+
return 2;
382+
case "spec":
383+
return 3;
384+
case "type":
385+
return 4;
386+
default:
387+
return Integer.MAX_VALUE;
388+
}
389+
}
390+
391+
@Override
392+
protected MappingNode representJavaBean(Set<Property> properties, Object javaBean) {
393+
MappingNode node = super.representJavaBean(properties, javaBean);
394+
// Always set the tag to MAP so that SnakeYaml doesn't print out the class name as a tag.
395+
node.setTag(Tag.MAP);
396+
// Sort the output of our map so that we put certain keys, such as apiVersion, first.
397+
Collections.sort(
398+
node.getValue(),
399+
new Comparator<NodeTuple>() {
400+
@Override
401+
public int compare(NodeTuple a, NodeTuple b) {
402+
String nameA = ((ScalarNode) a.getKeyNode()).getValue();
403+
String nameB = ((ScalarNode) b.getKeyNode()).getValue();
404+
int intCompare =
405+
Integer.compare(getPropertyPosition(nameA), getPropertyPosition(nameB));
406+
if (intCompare != 0) {
407+
return intCompare;
408+
} else {
409+
return nameA.compareTo(nameB);
410+
}
411+
}
412+
});
413+
return node;
414+
}
415+
416+
@Override
417+
protected NodeTuple representJavaBeanProperty(
418+
Object javaBean, Property property, Object propertyValue, Tag customTag) {
419+
// returning null for a null property value means we won't output it in the Yaml
420+
if (propertyValue == null) {
421+
return null;
422+
}
423+
return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
424+
}
425+
}
426+
288427
/** @return An instantiated SnakeYaml Object. */
289428
public static org.yaml.snakeyaml.Yaml getSnakeYaml() {
290-
return new org.yaml.snakeyaml.Yaml(new CustomConstructor());
429+
return new org.yaml.snakeyaml.Yaml(new CustomConstructor(), new CustomRepresenter());
291430
}
292431

293432
/**

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@
1515
import static org.junit.Assert.*;
1616

1717
import com.google.common.io.Resources;
18-
import io.kubernetes.client.models.*;
18+
import io.kubernetes.client.models.AppsV1beta1Deployment;
19+
import io.kubernetes.client.models.V1ObjectMeta;
20+
import io.kubernetes.client.models.V1Secret;
21+
import io.kubernetes.client.models.V1Service;
22+
import io.kubernetes.client.models.V1ServicePort;
1923
import java.io.File;
2024
import java.io.IOException;
2125
import java.io.StringReader;
2226
import java.lang.reflect.Method;
2327
import java.nio.charset.StandardCharsets;
28+
import java.nio.file.Files;
29+
import java.nio.file.Paths;
2430
import java.util.List;
2531
import org.junit.Test;
2632

@@ -132,6 +138,10 @@ public void testLoadAllFile() throws Exception {
132138
throw new Exception("some thing wrong happened");
133139
}
134140
}
141+
String result = Yaml.dumpAll(list.iterator());
142+
String expected =
143+
new String(Files.readAllBytes(Paths.get(TEST_YAML_FILE_PATH)), StandardCharsets.UTF_8);
144+
assertEquals(expected, result);
135145
}
136146

137147
@Test

util/src/test/resources/test.yaml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
apiVersion: v1
22
kind: Service
33
metadata:
4-
name: mock
54
labels:
65
app: mock
6+
name: mock
77
spec:
88
ports:
99
- port: 99
@@ -27,20 +27,16 @@ spec:
2727
name: helloworld
2828
spec:
2929
containers:
30-
- name: helloworld
31-
image: gcr.io/hightowerlabs/helloworld:0.0.1
32-
imagePullPolicy: Always
33-
args:
34-
- "-http=127.0.0.1:8080"
30+
- args:
31+
- -http=127.0.0.1:8080
32+
image: gcr.io/hightowerlabs/helloworld:0.0.1
33+
imagePullPolicy: Always
34+
name: helloworld
3535
---
3636
apiVersion: v1
3737
kind: Secret
38-
3938
metadata:
4039
name: secret
41-
4240
type: Opaque
43-
4441
data:
4542
secret-data: aGVsbG8=
46-

0 commit comments

Comments
 (0)