// Copyright 2017 The kubecfg authors // // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package utils import ( "bufio" "encoding/json" "fmt" "io" "io/ioutil" "net/url" "os" "path/filepath" jsonnet "github.com/google/go-jsonnet" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/yaml" ) // 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.Object, 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.Object, error) { data, err := ioutil.ReadAll(r) if err != nil { return nil, err } obj, _, err := unstructured.UnstructuredJSONScheme.Decode(data, nil, nil) if err != nil { return nil, err } return []runtime.Object{obj}, nil } func yamlReader(r io.ReadCloser) ([]runtime.Object, error) { decoder := yaml.NewYAMLReader(bufio.NewReader(r)) ret := []runtime.Object{} for { bytes, err := decoder.Read() if err == io.EOF { break } else if err != nil { return nil, err } if len(bytes) == 0 { continue } jsondata, err := yaml.ToJSON(bytes) if err != nil { return nil, err } obj, _, err := unstructured.UnstructuredJSONScheme.Decode(jsondata, nil, nil) if err != nil { return nil, err } ret = append(ret, obj) } return ret, nil } type walkContext struct { parent *walkContext label string } func (c *walkContext) String() string { parent := "" if c.parent != nil { parent = c.parent.String() } return parent + c.label } func jsonWalk(parentCtx *walkContext, obj interface{}) ([]interface{}, error) { switch o := obj.(type) { case nil: return []interface{}{}, nil case map[string]interface{}: if o["kind"] != nil && o["apiVersion"] != nil { return []interface{}{o}, nil } ret := []interface{}{} for k, v := range o { ctx := walkContext{ parent: parentCtx, label: "." + k, } children, err := jsonWalk(&ctx, v) if err != nil { return nil, err } ret = append(ret, children...) } return ret, nil case []interface{}: ret := make([]interface{}, 0, len(o)) for i, v := range o { ctx := walkContext{ parent: parentCtx, label: fmt.Sprintf("[%d]", i), } children, err := jsonWalk(&ctx, v) if err != nil { return nil, err } ret = append(ret, children...) } return ret, nil default: return nil, fmt.Errorf("Looking for kubernetes object at %s, but instead found %T", parentCtx, o) } } func jsonnetReader(vm *jsonnet.VM, path string) ([]runtime.Object, error) { // TODO: Read via Importer, so we support HTTP, etc for first // file too. abs, err := filepath.Abs(path) if err != nil { return nil, err } pathUrl := &url.URL{Scheme: "file", Path: filepath.ToSlash(abs)} bytes, err := ioutil.ReadFile(path) if err != nil { return nil, err } jsonstr, err := vm.EvaluateSnippet(pathUrl.String(), string(bytes)) if err != nil { return nil, err } log.Debugf("jsonnet result is: %s", jsonstr) var top interface{} if err = json.Unmarshal([]byte(jsonstr), &top); err != nil { return nil, err } objs, err := jsonWalk(&walkContext{label: ""}, top) if err != nil { return nil, err } ret := make([]runtime.Object, 0, len(objs)) for _, v := range objs { obj := &unstructured.Unstructured{Object: v.(map[string]interface{})} if obj.IsList() { // TODO: Use obj.ToList with newer apimachinery list := &unstructured.UnstructuredList{ Object: obj.Object, } err := obj.EachListItem(func(item runtime.Object) error { castItem := item.(*unstructured.Unstructured) list.Items = append(list.Items, *castItem) return nil }) if err != nil { return nil, err } ret = append(ret, list) } else { ret = append(ret, obj) } } return ret, nil } // FlattenToV1 expands any List-type objects into their members, and // cooerces everything to v1.Unstructured. Panics if coercion // encounters an unexpected object type. func FlattenToV1(objs []runtime.Object) []*unstructured.Unstructured { ret := make([]*unstructured.Unstructured, 0, len(objs)) for _, obj := range objs { switch o := obj.(type) { case *unstructured.UnstructuredList: for i := range o.Items { ret = append(ret, &o.Items[i]) } case *unstructured.Unstructured: ret = append(ret, o) default: panic("Unexpected unstructured object type") } } return ret }