From 9ddea4388378b1aa03dbd3ee90807ba60b3ed5e6 Mon Sep 17 00:00:00 2001 From: Angus Lees <gus@inodes.org> Date: Tue, 16 May 2017 16:25:59 +1000 Subject: [PATCH] Add ability to read json/yaml/jsonnet input files File format is recognised via simple file extension check. Any "List" objects found will be expanded into their constituent items, so single-element formats like json and jsonnet can use a v1.List to capture multiple resource objects. --- cmd/root.go | 34 +++++------ cmd/show.go | 40 ++++++++----- cmd/show_test.go | 4 +- main.go | 1 - testdata/lib/test.libsonnet | 3 + testdata/simple.json | 5 ++ testdata/test.jsonnet | 10 +++- testdata/test.yaml | 3 + utils/acquire.go | 110 ++++++++++++++++++++++++++++++++++++ 9 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 testdata/simple.json create mode 100644 testdata/test.yaml create mode 100644 utils/acquire.go diff --git a/cmd/root.go b/cmd/root.go index fa60984f..d149d1a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,13 +1,16 @@ package cmd import ( - "encoding/json" goflag "flag" + "fmt" "path/filepath" "github.com/golang/glog" "github.com/spf13/cobra" jsonnet "github.com/strickyak/jsonnet_cgo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/bitnami/kubecfg/utils" ) func init() { @@ -40,30 +43,27 @@ func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) { return nil, err } for _, p := range filepath.SplitList(jpath) { - glog.V(2).Infoln("Adding jsonnet path", p) + glog.V(2).Infoln("Adding jsonnet search path", p) vm.JpathAdd(p) } return vm, nil } -func evalFile(vm *jsonnet.VM, file string) (interface{}, error) { - var err error - jsonstr := "" - if file != "" { - jsonstr, err = vm.EvaluateFile(file) - if err != nil { - return nil, err - } - } - - glog.V(4).Infof("jsonnet result is: %s\n", jsonstr) - - var jsobj interface{} - err = json.Unmarshal([]byte(jsonstr), &jsobj) +func readObjs(cmd *cobra.Command, paths []string) ([]metav1.Object, error) { + vm, err := JsonnetVM(cmd) if err != nil { return nil, err } + defer vm.Destroy() - return jsobj, nil + res := []metav1.Object{} + for _, path := range paths { + objs, err := utils.Read(vm, path) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %v", path, err) + } + res = append(res, utils.FlattenToV1(objs)...) + } + return res, nil } diff --git a/cmd/show.go b/cmd/show.go index 82f8c0ef..9735cb41 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -10,8 +10,6 @@ import ( func init() { RootCmd.AddCommand(showCmd) - showCmd.PersistentFlags().StringP("file", "f", "", "Input jsonnet file") - showCmd.MarkFlagFilename("file", "jsonnet", "libsonnet") showCmd.PersistentFlags().StringP("format", "o", "yaml", "Output format. Supported values are: json, yaml") } @@ -28,11 +26,7 @@ var showCmd = &cobra.Command{ } defer vm.Destroy() - file, err := flags.GetString("file") - if err != nil { - return err - } - jsobj, err := evalFile(vm, file) + objs, err := readObjs(cmd, args) if err != nil { return err } @@ -43,16 +37,36 @@ var showCmd = &cobra.Command{ } switch format { case "yaml": - buf, err := yaml.Marshal(jsobj) - if err != nil { - return err + for _, obj := range objs { + fmt.Fprintln(out, "---") + // Urgh. Go via json because we need + // to trigger the custom scheme + // encoding. + buf, err := json.Marshal(obj) + if err != nil { + return err + } + o := map[string]interface{}{} + if err := json.Unmarshal(buf, &o); err != nil { + return err + } + buf, err = yaml.Marshal(o) + if err != nil { + return err + } + out.Write(buf) } - out.Write(buf) case "json": enc := json.NewEncoder(out) enc.SetIndent("", " ") - if err := enc.Encode(&jsobj); err != nil { - return err + for _, obj := range objs { + // TODO: this is not valid framing for JSON + if len(objs) > 1 { + fmt.Fprintln(out, "---") + } + if err := enc.Encode(obj); err != nil { + return err + } } default: return fmt.Errorf("Unknown --format: %s", format) diff --git a/cmd/show_test.go b/cmd/show_test.go index 7ce55fb7..55017a7b 100644 --- a/cmd/show_test.go +++ b/cmd/show_test.go @@ -39,6 +39,8 @@ func TestShow(t *testing.T) { // Use the fact that JSON is also valid YAML .. expected := ` { + "apiVersion": "v0alpha1", + "kind": "TestObject", "nil": null, "bool": true, "number": 42, @@ -56,8 +58,8 @@ func TestShow(t *testing.T) { output := cmdOutput(t, []string{"show", "-J", filepath.FromSlash("../testdata/lib"), - "-f", filepath.FromSlash("../testdata/test.jsonnet"), "-o", format, + filepath.FromSlash("../testdata/test.jsonnet"), }) t.Log("output is", output) diff --git a/main.go b/main.go index 2ce6712d..c0677115 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ func main() { cmd.Version = version if err := cmd.RootCmd.Execute(); err != nil { - fmt.Println("got error") fmt.Println("Error:", err) os.Exit(1) } diff --git a/testdata/lib/test.libsonnet b/testdata/lib/test.libsonnet index 6dd03fe6..50e063e1 100644 --- a/testdata/lib/test.libsonnet +++ b/testdata/lib/test.libsonnet @@ -1,4 +1,7 @@ { + apiVersion: "v0alpha1", + kind: "TestObject", + nil: null, bool: true, number: 42, diff --git a/testdata/simple.json b/testdata/simple.json new file mode 100644 index 00000000..6416b9be --- /dev/null +++ b/testdata/simple.json @@ -0,0 +1,5 @@ +{ + apiVersion: "v0alpha1", + kind: "JsonObject" + foo: "bar", +} diff --git a/testdata/test.jsonnet b/testdata/test.jsonnet index 935c6df1..f98a72df 100644 --- a/testdata/test.jsonnet +++ b/testdata/test.jsonnet @@ -1,5 +1,11 @@ local test = import "test.libsonnet"; -test { - string: "bar", +{ + apiVersion: "v1", + kind: "List", + items: [ + test { + string: "bar", + } + ], } diff --git a/testdata/test.yaml b/testdata/test.yaml new file mode 100644 index 00000000..c30e1838 --- /dev/null +++ b/testdata/test.yaml @@ -0,0 +1,3 @@ +apiVersion: v0alpha0 +kind: YamlObject +foo: bar diff --git a/utils/acquire.go b/utils/acquire.go new file mode 100644 index 00000000..01731a45 --- /dev/null +++ b/utils/acquire.go @@ -0,0 +1,110 @@ +package utils + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/golang/glog" + jsonnet "github.com/strickyak/jsonnet_cgo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apimachinery/pkg/runtime" +) + +// Read fetches and decodes K8s objects by path. +// TODO: Replace this with something supporting more sophisticated +// content negotiation. +func Read(vm *jsonnet.VM, path string) ([]runtime.Unstructured, error) { + ext := filepath.Ext(path) + if ext == ".json" { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return jsonReader(f) + } else if ext == ".yaml" { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return yamlReader(f) + } else if ext == ".jsonnet" { + return jsonnetReader(vm, path) + } + + return nil, fmt.Errorf("Unknown file extension: %s", path) +} + +func jsonReader(r io.Reader) ([]runtime.Unstructured, error) { + data, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + obj, _, err := unstructuredv1.UnstructuredJSONScheme.Decode(data, nil, nil) + if err != nil { + return nil, err + } + return []runtime.Unstructured{obj.(runtime.Unstructured)}, nil +} + +func yamlReader(r io.ReadCloser) ([]runtime.Unstructured, error) { + decoder := yaml.NewDocumentDecoder(r) + ret := []runtime.Unstructured{} + buf := []byte{} + for { + _, err := decoder.Read(buf) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + jsondata, err := yaml.ToJSON(buf) + if err != nil { + return nil, err + } + obj, _, err := unstructuredv1.UnstructuredJSONScheme.Decode(jsondata, nil, nil) + if err != nil { + return nil, err + } + ret = append(ret, obj.(runtime.Unstructured)) + } + return ret, nil +} + +func jsonnetReader(vm *jsonnet.VM, path string) ([]runtime.Unstructured, error) { + jsonstr, err := vm.EvaluateFile(path) + if err != nil { + return nil, err + } + + glog.V(4).Infof("jsonnet result is: %s\n", jsonstr) + + return jsonReader(strings.NewReader(jsonstr)) +} + +// FlattenToV1 expands any List-type objects into their members, and +// cooerces everything to metav1.Objects. Panics if coercion +// encounters an unexpected object type. +func FlattenToV1(objs []runtime.Unstructured) []metav1.Object { + ret := make([]metav1.Object, 0, len(objs)) + for _, obj := range objs { + switch o := obj.(type) { + case *unstructuredv1.UnstructuredList: + for _, item := range o.Items { + ret = append(ret, &item) + } + case *unstructuredv1.Unstructured: + ret = append(ret, o) + default: + panic("Unexpected unstructured object type") + } + } + return ret +} -- GitLab