From 1f85d5ddd37c2db883067aa88e4825c9a22d094f Mon Sep 17 00:00:00 2001
From: Angus Lees <gus@inodes.org>
Date: Thu, 27 Jul 2017 16:44:17 +1000
Subject: [PATCH] Add manifestJson and manifestYaml functions

jsonnet's `std.toString()` returns "compact" JSON.  Sometimes this is
not appropriate, particularly when generating large output that might
need to be viewed/debugged by a human.

This change implements manifestYaml and manifestJson functions, which
generate YAML and "pretty" JSON respectively.

A slight implementation wrinkle is that libjsonnet currently only
supports passing primitive (scalar) types to native functions, so
these arguments need to be JSON-serialised/unserialised across the
native function call boundary.
---
 lib/kubecfg.libsonnet    | 21 +++++++++++++++++++++
 lib/kubecfg_test.jsonnet | 35 +++++++++++++++++++++++++++++++----
 utils/nativefuncs.go     | 27 +++++++++++++++++++++++++++
 3 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/lib/kubecfg.libsonnet b/lib/kubecfg.libsonnet
index 43448e45..9215a753 100644
--- a/lib/kubecfg.libsonnet
+++ b/lib/kubecfg.libsonnet
@@ -13,6 +13,12 @@
 //    See the License for the specific language governing permissions and
 //    limitations under the License.
 
+// NB: libjsonnet native functions can only pass primitive types, so
+// some functions json-encode the arg.  These "*FromJson" functions
+// will be replaced by regular native version when libjsonnet is able
+// to support this.  This file strives to hide this implementation
+// detail.
+
 {
   // parseJson(data): parses the `data` string as a json document, and
   // returns the resulting jsonnet object.
@@ -24,6 +30,21 @@
   // element.
   parseYaml:: std.native("parseYaml"),
 
+  // manifestJson(value, indent): convert the jsonnet object `value`
+  // to a string encoded as "pretty" (multi-line) JSON, with each
+  // nesting level indented by `indent` spaces.
+  manifestJson(value, indent=4):: (
+    local f = std.native("manifestJsonFromJson");
+    f(std.toString(value), indent)
+  ),
+
+  // manifestYaml(value): convert the jsonnet object `value` to a
+  // string encoded as a single YAML document.
+  manifestYaml(value):: (
+    local f = std.native("manifestYamlFromJson");
+    f(std.toString(value))
+  ),
+
   // escapeStringRegex(s): Quote the regex metacharacters found in s.
   // The result is a regex that will match the original literal
   // characters.
diff --git a/lib/kubecfg_test.jsonnet b/lib/kubecfg_test.jsonnet
index fafd8afe..1e288153 100644
--- a/lib/kubecfg_test.jsonnet
+++ b/lib/kubecfg_test.jsonnet
@@ -27,16 +27,43 @@ baz: xyzzy
 ");
 assert x == [[3, 4], {foo: "bar", baz: "xyzzy"}] : "got " + x;
 
+local x = kubecfg.manifestJson({foo: "bar", baz: [3, 4]});
+assert x == '{
+    "baz": [
+        3,
+        4
+    ],
+    "foo": "bar"
+}
+' : "got " + x;
+
+local x = kubecfg.manifestJson({foo: "bar", baz: [3, 4]}, indent=2);
+assert x == '{
+  "baz": [
+    3,
+    4
+  ],
+  "foo": "bar"
+}
+' : "got " + x;
+
+local x = kubecfg.manifestYaml({foo: "bar", baz: [3, 4]});
+assert x == "baz:
+- 3
+- 4
+foo: bar
+" : "got " + x;
+
 local i = kubecfg.resolveImage("busybox");
 assert i == "busybox:latest" : "got " + i;
 
 assert kubecfg.regexMatch("o$", "foo");
 
-local r1 = kubecfg.escapeStringRegex("f[o");
-assert r1 == "f\\[o" : "got " + r1;
+local r = kubecfg.escapeStringRegex("f[o");
+assert r == "f\\[o" : "got " + r;
 
-local r2 = kubecfg.regexSubst("e", "tree", "oll");
-assert r2 == "trolloll" : "got " + r2;
+local r = kubecfg.regexSubst("e", "tree", "oll");
+assert r == "trolloll" : "got " + r;
 
 // Kubecfg wants to see something that looks like a k8s object
 {
diff --git a/utils/nativefuncs.go b/utils/nativefuncs.go
index 13569130..30fe7cc8 100644
--- a/utils/nativefuncs.go
+++ b/utils/nativefuncs.go
@@ -20,6 +20,9 @@ import (
 	"encoding/json"
 	"io"
 	"regexp"
+	"strings"
+
+	goyaml "github.com/ghodss/yaml"
 
 	jsonnet "github.com/strickyak/jsonnet_cgo"
 	"k8s.io/apimachinery/pkg/util/yaml"
@@ -40,6 +43,11 @@ func resolveImage(resolver Resolver, image string) (string, error) {
 
 // RegisterNativeFuncs adds kubecfg's native jsonnet functions to provided VM
 func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver) {
+	// NB: libjsonnet native functions can only pass primitive
+	// types, so some functions json-encode the arg.  These
+	// "*FromJson" functions will be replaced by regular native
+	// version when libjsonnet is able to support this.
+
 	vm.NativeCallback("parseJson", []string{"json"}, func(data []byte) (res interface{}, err error) {
 		err = json.Unmarshal(data, &res)
 		return
@@ -61,6 +69,25 @@ func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver) {
 		return ret, nil
 	})
 
+	vm.NativeCallback("manifestJsonFromJson", []string{"json", "indent"}, func(data []byte, indent int) (string, error) {
+		data = bytes.TrimSpace(data)
+		buf := bytes.Buffer{}
+		if err := json.Indent(&buf, data, "", strings.Repeat(" ", indent)); err != nil {
+			return "", err
+		}
+		buf.WriteString("\n")
+		return buf.String(), nil
+	})
+
+	vm.NativeCallback("manifestYamlFromJson", []string{"json"}, func(data []byte) (string, error) {
+		var input interface{}
+		if err := json.Unmarshal(data, &input); err != nil {
+			return "", err
+		}
+		output, err := goyaml.Marshal(input)
+		return string(output), err
+	})
+
 	vm.NativeCallback("resolveImage", []string{"image"}, func(image string) (string, error) {
 		return resolveImage(resolver, image)
 	})
-- 
GitLab