diff --git a/Makefile b/Makefile index 4f36f1fd07a98e975a86a9f81b1fed663c4d001a..48969bc158c3b57592b29650bf89cc967900ef14 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 7adc1f01390076007748c8dbec36e822c89a299e..969bebf86d46bdd91420bd821e759b23c897b02c 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 1bf86f617d6668a8baf05479f77b0a976432f7b0..9be10471047b2c7959fdc517b3142e190c0c940b 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 0000000000000000000000000000000000000000..a6e271b4ae99f0a7e0759fffc224d4863313d8a4 --- /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 0000000000000000000000000000000000000000..8184ee4b6f948acd1fa27b82cd53dd6cab6795e2 --- /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 0000000000000000000000000000000000000000..a6b4731ebc2c3bf06e39f95001e02e06a24b860b --- /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 0000000000000000000000000000000000000000..6bcbc7710593003ee90b08b74af2e2e0343169e3 --- /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") +}