From 8f76ab4f5335c23f57be87811a9e4c72f5dd92ad Mon Sep 17 00:00:00 2001
From: Angus Lees <gus@inodes.org>
Date: Thu, 22 Jun 2017 09:27:50 +1000
Subject: [PATCH] Add native convenience functions to parse JSON and YAML

Fixes #12
---
 Makefile                  |  7 ++++-
 README.md                 |  1 +
 cmd/root.go               |  2 ++
 lib/kubecfg.libsonnet     | 11 ++++++++
 lib/kubecfg_test.jsonnet  | 20 +++++++++++++
 utils/nativefuncs.go      | 33 ++++++++++++++++++++++
 utils/nativefuncs_test.go | 59 +++++++++++++++++++++++++++++++++++++++
 7 files changed, 132 insertions(+), 1 deletion(-)
 create mode 100644 lib/kubecfg.libsonnet
 create mode 100644 lib/kubecfg_test.jsonnet
 create mode 100644 utils/nativefuncs.go
 create mode 100644 utils/nativefuncs_test.go

diff --git a/Makefile b/Makefile
index 4f36f1fd..48969bc1 100644
--- a/Makefile
+++ b/Makefile
@@ -12,9 +12,14 @@ all: kubecfg
 kubecfg:
 	$(GO) build $(GO_FLAGS) .
 
-test:
+test: gotest jsonnettest
+
+gotest:
 	$(GO) test $(GO_FLAGS) $(GO_PACKAGES)
 
+jsonnettest: kubecfg lib/kubecfg_test.jsonnet
+	./kubecfg -J lib show lib/kubecfg_test.jsonnet
+
 vet:
 	$(GO) vet $(GO_FLAGS) $(GO_PACKAGES)
 
diff --git a/README.md b/README.md
index 7adc1f01..969bebf8 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ avoid an immediate `Killed: 9`.
 - Supports JSON, YAML or jsonnet files (by file suffix).
 - Best-effort sorts objects before updating, so that dependencies are
   pushed to the server before objects that refer to them.
+- Additional jsonnet builtin functions. See `lib/kubecfg.libsonnet`.
 
 ## Infrastructure-as-code Philosophy
 
diff --git a/cmd/root.go b/cmd/root.go
index 1bf86f61..9be10471 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -91,6 +91,8 @@ func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
 		vm.ExtVar(kv[0], kv[1])
 	}
 
+	utils.RegisterNativeFuncs(vm)
+
 	return vm, nil
 }
 
diff --git a/lib/kubecfg.libsonnet b/lib/kubecfg.libsonnet
new file mode 100644
index 00000000..a6e271b4
--- /dev/null
+++ b/lib/kubecfg.libsonnet
@@ -0,0 +1,11 @@
+{
+  // parseJson(data): parses the `data` string as a json document, and
+  // returns the resulting jsonnet object.
+  parseJson:: std.native("parseJson"),
+
+  // parseYaml(data): parse the `data` string as a YAML stream, and
+  // returns an *array* of the resulting jsonnet objects.  A single
+  // YAML document will still be returned as an array with one
+  // element.
+  parseYaml:: std.native("parseYaml"),
+}
diff --git a/lib/kubecfg_test.jsonnet b/lib/kubecfg_test.jsonnet
new file mode 100644
index 00000000..8184ee4b
--- /dev/null
+++ b/lib/kubecfg_test.jsonnet
@@ -0,0 +1,20 @@
+// Run me with `../kubecfg show kubecfg_test.jsonnet`
+local kubecfg = import "kubecfg.libsonnet";
+
+assert kubecfg.parseJson("[3, 4]") == [3, 4];
+
+local x = kubecfg.parseYaml("---
+- 3
+- 4
+---
+foo: bar
+baz: xyzzy
+");
+assert x == [[3, 4], {foo: "bar", baz: "xyzzy"}] : "got " + x;
+
+// Kubecfg wants to see something that looks like a k8s object
+{
+  apiVersion: "test",
+  kind: "Result",
+  result: "SUCCESS"
+}
diff --git a/utils/nativefuncs.go b/utils/nativefuncs.go
new file mode 100644
index 00000000..a6b4731e
--- /dev/null
+++ b/utils/nativefuncs.go
@@ -0,0 +1,33 @@
+package utils
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+
+	jsonnet "github.com/strickyak/jsonnet_cgo"
+	"k8s.io/client-go/pkg/util/yaml"
+)
+
+func RegisterNativeFuncs(vm *jsonnet.VM) {
+	vm.NativeCallback("parseJson", []string{"json"}, func(data []byte) (res interface{}, err error) {
+		err = json.Unmarshal(data, &res)
+		return
+	})
+
+	vm.NativeCallback("parseYaml", []string{"yaml"}, func(data []byte) ([]interface{}, error) {
+		ret := []interface{}{}
+		d := yaml.NewYAMLToJSONDecoder(bytes.NewReader(data))
+		for {
+			var doc interface{}
+			if err := d.Decode(&doc); err != nil {
+				if err == io.EOF {
+					break
+				}
+				return nil, err
+			}
+			ret = append(ret, doc)
+		}
+		return ret, nil
+	})
+}
diff --git a/utils/nativefuncs_test.go b/utils/nativefuncs_test.go
new file mode 100644
index 00000000..6bcbc771
--- /dev/null
+++ b/utils/nativefuncs_test.go
@@ -0,0 +1,59 @@
+package utils
+
+import (
+	"testing"
+
+	jsonnet "github.com/strickyak/jsonnet_cgo"
+)
+
+// check there is no err, and a == b.
+func check(t *testing.T, err error, actual, expected string) {
+	if err != nil {
+		t.Errorf("Expected %q, got error: %q", expected, err.Error())
+	} else if actual != expected {
+		t.Errorf("Expected %q, got %q", expected, actual)
+	}
+}
+
+func TestParseJson(t *testing.T) {
+	vm := jsonnet.Make()
+	defer vm.Destroy()
+	RegisterNativeFuncs(vm)
+
+	_, err := vm.EvaluateSnippet("failtest", `std.native("parseJson")("barf{")`)
+	if err == nil {
+		t.Errorf("parseJson succeeded on invalid json")
+	}
+
+	x, err := vm.EvaluateSnippet("test", `std.native("parseJson")("null")`)
+	check(t, err, x, "null\n")
+
+	x, err = vm.EvaluateSnippet("test", `
+    local a = std.native("parseJson")('{"foo": 3, "bar": 4}');
+    a.foo + a.bar`)
+	check(t, err, x, "7\n")
+}
+
+func TestParseYaml(t *testing.T) {
+	vm := jsonnet.Make()
+	defer vm.Destroy()
+	RegisterNativeFuncs(vm)
+
+	_, err := vm.EvaluateSnippet("failtest", `std.native("parseYaml")("[barf")`)
+	if err == nil {
+		t.Errorf("parseYaml succeeded on invalid yaml")
+	}
+
+	x, err := vm.EvaluateSnippet("test", `std.native("parseYaml")("")`)
+	check(t, err, x, "[ ]\n")
+
+	x, err = vm.EvaluateSnippet("test", `
+    local a = std.native("parseYaml")("foo:\n- 3\n- 4\n")[0];
+    a.foo[0] + a.foo[1]`)
+	check(t, err, x, "7\n")
+
+	x, err = vm.EvaluateSnippet("test", `
+    local a = std.native("parseYaml")("---\nhello\n---\nworld");
+    a[0] + a[1]`)
+	check(t, err, x, "\"helloworld\"\n")
+}
-- 
GitLab