Skip to content

Commit 72a08ef

Browse files
Merge pull request #298 from johndaniels/master
Correctly load base64-encoded strings from Secret Yaml files.
2 parents d96b730 + a5ed2a7 commit 72a08ef

File tree

3 files changed

+190
-8
lines changed

3 files changed

+190
-8
lines changed

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

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,30 @@
1919
import java.io.IOException;
2020
import java.io.Reader;
2121
import java.io.StringReader;
22+
import java.io.Writer;
2223
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.Comparator;
2326
import java.util.HashMap;
27+
import java.util.Iterator;
2428
import java.util.List;
2529
import java.util.Map;
2630
import java.util.Set;
31+
import okio.ByteString;
2732
import org.apache.commons.lang3.tuple.MutablePair;
2833
import org.apache.commons.lang3.tuple.Pair;
2934
import org.slf4j.Logger;
3035
import org.slf4j.LoggerFactory;
36+
import org.yaml.snakeyaml.DumperOptions;
3137
import org.yaml.snakeyaml.constructor.Constructor;
38+
import org.yaml.snakeyaml.introspector.Property;
39+
import org.yaml.snakeyaml.nodes.MappingNode;
3240
import org.yaml.snakeyaml.nodes.Node;
41+
import org.yaml.snakeyaml.nodes.NodeTuple;
3342
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;
3446

3547
public class Yaml {
3648
private static Map<String, Class<?>> classes = new HashMap<>();
@@ -262,13 +274,56 @@ public static List<Object> loadAll(Reader reader) throws IOException {
262274
return list;
263275
}
264276

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+
265317
/** Defines constructor logic for custom types in this library. */
266318
public static class CustomConstructor extends Constructor {
267319
@Override
268320
protected Object constructObject(Node node) {
269321
if (node.getType() == IntOrString.class) {
270322
return constructIntOrString((ScalarNode) node);
271323
}
324+
if (node.getType() == byte[].class) {
325+
return constructByteArray((ScalarNode) node);
326+
}
272327
return super.constructObject(node);
273328
}
274329

@@ -279,11 +334,99 @@ private IntOrString constructIntOrString(ScalarNode node) {
279334
return new IntOrString(node.getValue());
280335
}
281336
}
337+
338+
private byte[] constructByteArray(ScalarNode node) {
339+
return ByteString.decodeBase64(node.getValue()).toByteArray();
340+
}
341+
}
342+
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+
}
282425
}
283426

284427
/** @return An instantiated SnakeYaml Object. */
285428
public static org.yaml.snakeyaml.Yaml getSnakeYaml() {
286-
return new org.yaml.snakeyaml.Yaml(new CustomConstructor());
429+
return new org.yaml.snakeyaml.Yaml(new CustomConstructor(), new CustomRepresenter());
287430
}
288431

289432
/**

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
import com.google.common.io.Resources;
1818
import io.kubernetes.client.models.AppsV1beta1Deployment;
1919
import io.kubernetes.client.models.V1ObjectMeta;
20+
import io.kubernetes.client.models.V1Secret;
2021
import io.kubernetes.client.models.V1Service;
2122
import io.kubernetes.client.models.V1ServicePort;
2223
import java.io.File;
2324
import java.io.IOException;
2425
import java.io.StringReader;
2526
import java.lang.reflect.Method;
27+
import java.nio.charset.StandardCharsets;
28+
import java.nio.file.Files;
29+
import java.nio.file.Paths;
2630
import java.util.List;
2731
import org.junit.Test;
2832

@@ -123,10 +127,21 @@ public void testLoadAllFile() throws Exception {
123127
assertEquals("apps/v1beta1", deploy.getApiVersion());
124128
assertEquals("Deployment", deploy.getKind());
125129
assertEquals("helloworld", deploy.getMetadata().getName());
130+
} else if (type.equals("V1Secret")) {
131+
V1Secret secret = (V1Secret) object;
132+
assertEquals("Secret", secret.getKind());
133+
assertEquals("secret", secret.getMetadata().getName());
134+
assertEquals("Opaque", secret.getType());
135+
assertEquals(
136+
"hello", new String(secret.getData().get("secret-data"), StandardCharsets.UTF_8));
126137
} else {
127138
throw new Exception("some thing wrong happened");
128139
}
129140
}
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);
130145
}
131146

132147
@Test
@@ -149,4 +164,21 @@ public void testLoadIntOrString() {
149164
assertNull("Unexpected exception: " + ex.toString(), ex);
150165
}
151166
}
167+
168+
@Test
169+
public void testLoadBytes() {
170+
try {
171+
String strInput = "data:\n hello: aGVsbG8=";
172+
173+
V1Secret secret = Yaml.loadAs(strInput, V1Secret.class);
174+
175+
assertEquals(
176+
"Incorrect value loaded for Base64 encoded secret",
177+
"hello",
178+
new String(secret.getData().get("hello"), StandardCharsets.UTF_8));
179+
180+
} catch (Exception ex) {
181+
assertNull("Unexpected exception: " + ex.toString(), ex);
182+
}
183+
}
152184
}

util/src/test/resources/test.yaml

Lines changed: 14 additions & 7 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,9 +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"
35-
30+
- args:
31+
- -http=127.0.0.1:8080
32+
image: gcr.io/hightowerlabs/helloworld:0.0.1
33+
imagePullPolicy: Always
34+
name: helloworld
35+
---
36+
apiVersion: v1
37+
kind: Secret
38+
metadata:
39+
name: secret
40+
type: Opaque
41+
data:
42+
secret-data: aGVsbG8=

0 commit comments

Comments
 (0)