cluster/tools/kartongips: init
This forks bitnami/kubecfg into kartongips. The rationale is that we
want to implement hscloud-specific functionality that wouldn't really be
upstreamable into kubecfg (like secret support, mulit-cluster support).
We forked off from github.com/q3k/kubecfg at commit b6817a94492c561ed61a44eeea2d92dcf2e6b8c0.
Change-Id: If5ba513905e0a86f971576fe7061a471c1d8b398
diff --git a/cluster/tools/kartongips/utils/BUILD.bazel b/cluster/tools/kartongips/utils/BUILD.bazel
new file mode 100644
index 0000000..d8724f0
--- /dev/null
+++ b/cluster/tools/kartongips/utils/BUILD.bazel
@@ -0,0 +1,74 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "acquire.go",
+ "bindata.go",
+ "client.go",
+ "importer.go",
+ "meta.go",
+ "nativefuncs.go",
+ "openapi.go",
+ "resolver.go",
+ "sort.go",
+ ],
+ importpath = "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@com_github_elazarl_go_bindata_assetfs//:go_default_library",
+ "@com_github_genuinetools_reg//registry:go_default_library",
+ "@com_github_genuinetools_reg//repoutils:go_default_library",
+ "@com_github_ghodss_yaml//:go_default_library",
+ "@com_github_google_go_jsonnet//:go_default_library",
+ "@com_github_google_go_jsonnet//ast:go_default_library",
+ "@com_github_googleapis_gnostic//openapiv2:go_default_library",
+ "@com_github_sirupsen_logrus//:go_default_library",
+ "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
+ "@io_k8s_apimachinery//pkg/api/meta:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
+ "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+ "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
+ "@io_k8s_apimachinery//pkg/util/runtime:go_default_library",
+ "@io_k8s_apimachinery//pkg/util/yaml:go_default_library",
+ "@io_k8s_apimachinery//pkg/version:go_default_library",
+ "@io_k8s_client_go//discovery:go_default_library",
+ "@io_k8s_client_go//dynamic:go_default_library",
+ "@io_k8s_client_go//rest:go_default_library",
+ "@io_k8s_kube_openapi//pkg/util/proto:go_default_library",
+ "@io_k8s_kube_openapi//pkg/util/proto/validation:go_default_library",
+ "@io_k8s_kubectl//pkg/util/openapi:go_default_library",
+ ],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = [
+ "acquire_test.go",
+ "importer_test.go",
+ "meta_test.go",
+ "nativefuncs_test.go",
+ "openapi_test.go",
+ "sort_test.go",
+ ],
+ embed = [":go_default_library"],
+ deps = [
+ "@com_github_golang_protobuf//proto:go_default_library",
+ "@com_github_google_go_jsonnet//:go_default_library",
+ "@com_github_googleapis_gnostic//openapiv2:go_default_library",
+ "@com_github_sirupsen_logrus//:go_default_library",
+ "@io_k8s_apimachinery//pkg/api/equality:go_default_library",
+ "@io_k8s_apimachinery//pkg/api/meta:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
+ "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
+ "@io_k8s_apimachinery//pkg/util/diff:go_default_library",
+ "@io_k8s_apimachinery//pkg/util/errors:go_default_library",
+ "@io_k8s_apimachinery//pkg/version:go_default_library",
+ "@io_k8s_client_go//discovery:go_default_library",
+ "@io_k8s_client_go//discovery/fake:go_default_library",
+ "@io_k8s_client_go//restmapper:go_default_library",
+ "@io_k8s_client_go//testing:go_default_library",
+ ],
+)
diff --git a/cluster/tools/kartongips/utils/acquire.go b/cluster/tools/kartongips/utils/acquire.go
new file mode 100644
index 0000000..c979316
--- /dev/null
+++ b/cluster/tools/kartongips/utils/acquire.go
@@ -0,0 +1,226 @@
+// 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>"}, 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
+}
diff --git a/cluster/tools/kartongips/utils/acquire_test.go b/cluster/tools/kartongips/utils/acquire_test.go
new file mode 100644
index 0000000..41fe8dc
--- /dev/null
+++ b/cluster/tools/kartongips/utils/acquire_test.go
@@ -0,0 +1,107 @@
+// 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 (
+ "encoding/json"
+ "reflect"
+ "sort"
+ "testing"
+)
+
+func TestJsonWalk(t *testing.T) {
+ fooObj := map[string]interface{}{
+ "apiVersion": "test",
+ "kind": "Foo",
+ }
+ barObj := map[string]interface{}{
+ "apiVersion": "test",
+ "kind": "Bar",
+ }
+
+ tests := []struct {
+ input string
+ result []interface{}
+ error string
+ }{
+ {
+ // nil input
+ input: `null`,
+ result: []interface{}{},
+ },
+ {
+ // single basic object
+ input: `{"apiVersion": "test", "kind": "Foo"}`,
+ result: []interface{}{fooObj},
+ },
+ {
+ // array of objects
+ input: `[{"apiVersion": "test", "kind": "Foo"}, {"apiVersion": "test", "kind": "Bar"}]`,
+ result: []interface{}{barObj, fooObj},
+ },
+ {
+ // object of objects
+ input: `{"foo": {"apiVersion": "test", "kind": "Foo"}, "bar": {"apiVersion": "test", "kind": "Bar"}}`,
+ result: []interface{}{barObj, fooObj},
+ },
+ {
+ // Deeply nested
+ input: `{"foo": [[{"apiVersion": "test", "kind": "Foo"}], {"apiVersion": "test", "kind": "Bar"}]}`,
+ result: []interface{}{barObj, fooObj},
+ },
+ {
+ // Error: nested misplaced value
+ input: `{"foo": {"bar": [null, 42]}}`,
+ error: "Looking for kubernetes object at <top>.foo.bar[1], but instead found float64",
+ },
+ }
+
+ for i, test := range tests {
+ t.Logf("%d: %s", i, test.input)
+ var top interface{}
+ if err := json.Unmarshal([]byte(test.input), &top); err != nil {
+ t.Errorf("Failed to unmarshal %q: %v", test.input, err)
+ continue
+ }
+ objs, err := jsonWalk(&walkContext{label: "<top>"}, top)
+ if test.error != "" {
+ // expect error
+ if err == nil {
+ t.Errorf("Test %d failed to fail", i)
+ } else if err.Error() != test.error {
+ t.Errorf("Test %d failed with %q but expected %q", i, err, test.error)
+ }
+
+ continue
+ }
+
+ // expect success
+ if err != nil {
+ t.Errorf("Test %d failed: %v", i, err)
+ continue
+ }
+ keyFunc := func(i int) string {
+ v := objs[i].(map[string]interface{})
+ return v["kind"].(string)
+ }
+ sort.Slice(objs, func(i, j int) bool {
+ return keyFunc(i) < keyFunc(j)
+ })
+ if !reflect.DeepEqual(objs, test.result) {
+ t.Errorf("Expected %v, got %v", test.result, objs)
+ }
+ }
+}
diff --git a/cluster/tools/kartongips/utils/bindata.go b/cluster/tools/kartongips/utils/bindata.go
new file mode 100644
index 0000000..c9d6dbf
--- /dev/null
+++ b/cluster/tools/kartongips/utils/bindata.go
@@ -0,0 +1,237 @@
+// Code generated by go-bindata.
+// sources:
+// ../lib/kubecfg.libsonnet
+// DO NOT EDIT!
+
+package utils
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _libKubecfgLibsonnet = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x55\x51\x8f\xdb\x36\x13\x7c\xf7\xaf\x18\x18\xdf\x83\x1d\x28\x56\x12\x7c\x40\x01\x17\x01\xea\x26\x29\xea\xf4\x62\xa3\xf6\xa5\xc1\xbd\x79\x4d\xad\x24\xe6\x28\x52\x25\x29\xfb\x8c\xa2\xff\xbd\x20\x25\x9d\xe5\xf3\x1d\x10\xe0\x20\x9c\xb9\xbb\xc3\x99\xd9\xd5\x2a\x4d\xf1\xc1\xd4\x27\x2b\x8b\xd2\xe3\xdd\x9b\xb7\x3f\xe1\xb6\x64\xdc\x37\x7b\x16\x79\x01\x6a\x7c\x69\xac\x1b\xa5\x69\xfb\x07\x00\x37\x52\xb0\x76\x9c\xa1\xd1\x19\x5b\xf8\x92\xb1\xa8\x49\x94\xdc\x47\x12\xfc\xc5\xd6\x49\xa3\xf1\x6e\xf6\x06\x93\x90\x30\xee\x42\xe3\xe9\xcf\x1d\xca\xc9\x34\xa8\xe8\x04\x6d\x3c\x1a\xc7\xf0\xa5\x74\xc8\xa5\x62\xf0\x83\xe0\xda\x43\x6a\x08\x53\xd5\x4a\x92\x16\x8c\xa3\xf4\x65\xbc\xaa\x03\x9a\x75\x30\x77\x1d\x8c\xd9\x7b\x92\x1a\x04\x61\xea\x13\x4c\x3e\xcc\x05\xf9\x33\x7b\xa0\xf4\xbe\x9e\xa7\xe9\xf1\x78\x9c\x51\xe4\x3d\x33\xb6\x48\x55\x9b\xeb\xd2\x9b\xe5\x87\x4f\xab\xed\xa7\xd7\xef\x66\x6f\xce\x55\x5f\xb5\x62\xe7\x60\xf9\xef\x46\x5a\xce\xb0\x3f\x81\xea\x5a\x49\x41\x7b\xc5\x50\x74\x84\xb1\xa0\xc2\x32\x67\xf0\x26\x70\x3f\x5a\xe9\xa5\x2e\x12\x38\x93\xfb\x23\x59\xee\x90\x32\xe9\xbc\x95\xfb\xc6\x5f\x18\xd8\x33\x95\xee\x22\xc1\x68\x90\xc6\x78\xb1\xc5\x72\x3b\xc6\xaf\x8b\xed\x72\x9b\x74\x38\xdf\x96\xb7\xbf\xaf\xbf\xde\xe2\xdb\x62\xb3\x59\xac\x6e\x97\x9f\xb6\x58\x6f\xf0\x61\xbd\xfa\xb8\xbc\x5d\xae\x57\x5b\xac\x7f\xc3\x62\x75\x87\x3f\x96\xab\x8f\x09\x58\xfa\x92\x2d\xf8\xa1\xb6\x41\x87\xb1\x90\xc1\x5a\xce\x7a\x1f\xb7\xcc\x17\x44\x72\xd3\x12\x73\x35\x0b\x99\x4b\x01\x45\xba\x68\xa8\x60\x14\xe6\xc0\x56\x4b\x5d\xa0\x66\x5b\x49\x17\x1a\xed\x40\x3a\xeb\x90\x94\xac\xa4\x27\x1f\x4f\xaf\x04\xce\x46\xa3\x7f\x46\x40\x9a\xa2\x26\xeb\xf8\xb3\x33\x7a\x92\x91\xa7\xe9\xbc\x3d\x70\x31\x79\x17\x8e\x76\x08\x3e\xe8\x02\xe4\x40\xf8\xee\x8c\x46\x66\x44\x53\xb1\xf6\x49\xbc\x2e\xc2\x58\xf6\x8d\xd5\x6d\x99\x65\xd7\xa8\x60\x7a\xcc\xd6\xec\x61\xf6\xdf\x59\xf8\xd9\x08\xe7\xeb\xe6\x73\x38\x9f\xcd\x34\x79\x79\xe0\xc9\xf8\xf1\x7c\x3c\x4d\x46\x03\x66\x77\x54\xa9\x0b\x66\x2f\x11\xbb\x5b\x7c\xb9\x09\x07\x4c\xd5\x33\xb4\x48\xe3\x15\x59\x4b\xa7\x57\xfd\x4c\xbe\x44\xd2\xcd\x80\x05\x9c\xd4\x85\xe2\x16\x23\x22\xf7\x92\x71\x94\x4a\xc1\xf9\xf0\xdc\x73\x87\xcf\x59\xe4\xa0\x11\xaf\x68\xdf\x11\xa3\xbb\x72\x56\x1c\x0a\x1f\xc5\x07\x45\xcf\x89\x0f\xe7\x67\xf1\x15\x69\x99\xb3\xf3\xb1\x33\x07\x52\x0d\x27\x90\x3a\x63\xed\xa7\x73\x08\xa3\x0f\x6c\x7d\xd4\x71\xc9\x1e\xbb\x98\xbb\x6b\x41\xbc\x01\xf5\x26\xb1\x16\x26\x6b\x89\x8e\x6b\xcb\xde\x9f\xc6\x98\x54\xc1\x82\xd7\x4a\x6a\x9e\xe2\xf3\x76\xbd\x4a\x5a\xee\x4c\xa2\x6c\x11\x34\xbb\xe8\x91\xe2\x03\xab\x8e\x40\xfb\xda\xed\xda\x1f\x3b\xb8\x9a\x04\xbb\x20\xef\x65\xce\xef\xff\x3f\x9d\xcf\x31\x19\xc5\xb9\x34\x82\x14\x72\xbc\xbf\xb0\x60\x58\x1b\x96\x53\xc8\xcc\x9f\x08\x1f\x01\x57\xfe\xc4\xf9\x88\x69\x3f\xe4\x4b\xb4\xa4\x45\xb8\xf6\x85\xba\xb6\x5f\x76\x7c\xa8\xec\x99\xde\x0d\x43\xe7\xf6\xb1\x13\x54\xf3\x36\x5e\xb1\xe1\x82\x1f\x26\x6e\x3a\xc7\x9f\x8d\xf1\xdc\x4d\x5f\xc1\x0f\xa8\xd8\x93\x28\xc9\x92\xf0\x6c\x1d\x72\xd3\xe8\x2c\xec\xac\xe8\x66\x9a\xc6\x2f\x40\x3b\xa7\x61\x21\x51\x57\xe5\x4b\xea\xc6\xb0\x22\x2f\xda\x6d\x6c\xac\x2c\xa4\x26\x05\x25\x3d\x5b\x52\x6d\xfd\x19\x3b\x00\x5e\x71\x7a\xa2\xe4\x2a\x7e\x96\x63\xd9\x19\x75\xe0\x65\x45\x05\x4f\x64\x78\x3e\x71\x3b\x33\xe2\x9e\xc3\x32\x0b\x9b\xa9\x73\x36\xb7\xa6\x6a\xcb\xe3\xf1\xdc\x53\x01\xa9\xe3\x4c\x56\xc6\x0e\x56\x5a\x0c\xff\x92\xc9\x82\x9d\x4f\x90\x71\xcd\x3a\x0b\x00\x46\xf7\xdf\xbf\x4e\x8e\xa9\x2a\xd2\x19\xc2\xbc\x22\x57\x54\x44\x59\x43\x6e\x4f\x14\x0d\x43\x43\x31\x05\x3f\x7c\x09\xd6\x4d\xe2\xbf\x49\x47\x78\x3a\xc7\xa6\xdf\x62\xb6\x61\xc8\xbc\x33\x5c\x9e\x5b\x33\x1c\x9d\x19\x36\x7d\x98\x5c\xdc\xe3\xf1\x45\xe7\xd8\xc2\xc2\x84\x45\xdd\x02\xd4\xa8\x49\xdc\x53\xd1\x2d\x84\x49\x7d\xf2\xa5\xd1\xaf\xa5\x2b\xa7\xad\x80\x9e\xcf\x15\xfd\x3e\xf0\x84\xfc\xb6\xd9\x3b\xff\x48\xde\x8a\x04\x96\x6b\xf5\xc8\x7f\xb0\xdf\xc2\xb6\x0b\x31\x12\x52\x17\x03\x84\x38\x65\x56\xb4\xaf\x7b\x48\x98\x01\x9b\x98\x17\x35\xf4\x2d\x0c\x5f\x74\xa9\x85\x6a\x32\xc6\xff\xde\x26\x60\x2f\x1e\x37\x8b\xe5\x3c\x7c\x53\x0c\x5c\xb3\x8f\x83\xc8\x2e\x82\xfc\x98\x25\xfd\x8a\x8f\xbe\x3c\x6f\x49\x54\xf9\x9c\x25\x31\x10\x2c\xf9\x77\xf4\x5f\x00\x00\x00\xff\xff\x3a\x93\xab\x97\x36\x09\x00\x00")
+
+func libKubecfgLibsonnetBytes() ([]byte, error) {
+ return bindataRead(
+ _libKubecfgLibsonnet,
+ "lib/kubecfg.libsonnet",
+ )
+}
+
+func libKubecfgLibsonnet() (*asset, error) {
+ bytes, err := libKubecfgLibsonnetBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "lib/kubecfg.libsonnet", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "lib/kubecfg.libsonnet": libKubecfgLibsonnet,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+var _bintree = &bintree{nil, map[string]*bintree{
+ "lib": &bintree{nil, map[string]*bintree{
+ "kubecfg.libsonnet": &bintree{libKubecfgLibsonnet, map[string]*bintree{}},
+ }},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
+
diff --git a/cluster/tools/kartongips/utils/client.go b/cluster/tools/kartongips/utils/client.go
new file mode 100644
index 0000000..07cf254
--- /dev/null
+++ b/cluster/tools/kartongips/utils/client.go
@@ -0,0 +1,297 @@
+// 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.
+
+// Changes:
+// * Merged updates from https://github.com/kubernetes/client-go/blob/kubernetes-1.18.1/discovery/cached/memory/memcache.go
+// --jjo, 2020-04-09
+
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "sync"
+ "syscall"
+
+ openapi_v2 "github.com/googleapis/gnostic/openapiv2"
+
+ log "github.com/sirupsen/logrus"
+ errorsutil "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/apimachinery/pkg/version"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/dynamic"
+ restclient "k8s.io/client-go/rest"
+)
+
+type cacheEntry struct {
+ resourceList *metav1.APIResourceList
+ err error
+}
+
+// memcachedDiscoveryClient can Invalidate() to stay up-to-date with discovery
+// information.
+//
+// TODO: Switch to a watch interface. Right now it will poll after each
+// Invalidate() call.
+type memcachedDiscoveryClient struct {
+ delegate discovery.DiscoveryInterface
+
+ lock sync.RWMutex
+ groupToServerResources map[string]*cacheEntry
+ groupList *metav1.APIGroupList
+ cacheValid bool
+}
+
+// Error Constants
+var (
+ ErrCacheNotFound = errors.New("not found")
+)
+
+var _ discovery.CachedDiscoveryInterface = &memcachedDiscoveryClient{}
+
+// isTransientConnectionError checks whether given error is "Connection refused" or
+// "Connection reset" error which usually means that apiserver is temporarily
+// unavailable.
+func isTransientConnectionError(err error) bool {
+ urlError, ok := err.(*url.Error)
+ if !ok {
+ return false
+ }
+ opError, ok := urlError.Err.(*net.OpError)
+ if !ok {
+ return false
+ }
+ errno, ok := opError.Err.(syscall.Errno)
+ if !ok {
+ return false
+ }
+ return errno == syscall.ECONNREFUSED || errno == syscall.ECONNRESET
+}
+
+func isTransientError(err error) bool {
+ if isTransientConnectionError(err) {
+ return true
+ }
+
+ if t, ok := err.(errorsutil.APIStatus); ok && t.Status().Code >= 500 {
+ return true
+ }
+
+ return errorsutil.IsTooManyRequests(err)
+}
+
+// ServerResourcesForGroupVersion returns the supported resources for a group and version.
+func (d *memcachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ if !d.cacheValid {
+ if err := d.refreshLocked(); err != nil {
+ return nil, err
+ }
+ }
+ cachedVal, ok := d.groupToServerResources[groupVersion]
+ if !ok {
+ return nil, ErrCacheNotFound
+ }
+
+ if cachedVal.err != nil && isTransientError(cachedVal.err) {
+ r, err := d.serverResourcesForGroupVersion(groupVersion)
+ if err != nil {
+ utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", groupVersion, err))
+ }
+ cachedVal = &cacheEntry{r, err}
+ d.groupToServerResources[groupVersion] = cachedVal
+ }
+
+ return cachedVal.resourceList, cachedVal.err
+}
+
+// ServerResources returns the supported resources for all groups and versions.
+// Deprecated: use ServerGroupsAndResources instead.
+func (d *memcachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) {
+ return discovery.ServerResources(d)
+}
+
+// ServerGroupsAndResources returns the groups and supported resources for all groups and versions.
+func (d *memcachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
+ return discovery.ServerGroupsAndResources(d)
+}
+
+func (d *memcachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ if !d.cacheValid {
+ if err := d.refreshLocked(); err != nil {
+ return nil, err
+ }
+ }
+ return d.groupList, nil
+}
+
+func (d *memcachedDiscoveryClient) RESTClient() restclient.Interface {
+ return d.delegate.RESTClient()
+}
+
+func (d *memcachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
+ return discovery.ServerPreferredResources(d)
+}
+
+func (d *memcachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
+ return discovery.ServerPreferredNamespacedResources(d)
+}
+
+func (d *memcachedDiscoveryClient) ServerVersion() (*version.Info, error) {
+ return d.delegate.ServerVersion()
+}
+
+func (d *memcachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
+ return d.delegate.OpenAPISchema()
+}
+
+func (d *memcachedDiscoveryClient) Fresh() bool {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ // Return whether the cache is populated at all. It is still possible that
+ // a single entry is missing due to transient errors and the attempt to read
+ // that entry will trigger retry.
+ return d.cacheValid
+}
+
+// Invalidate enforces that no cached data that is older than the current time
+// is used.
+func (d *memcachedDiscoveryClient) Invalidate() {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.cacheValid = false
+ d.groupToServerResources = nil
+ d.groupList = nil
+}
+
+// refreshLocked refreshes the state of cache. The caller must hold d.lock for
+// writing.
+func (d *memcachedDiscoveryClient) refreshLocked() error {
+ // TODO: Could this multiplicative set of calls be replaced by a single call
+ // to ServerResources? If it's possible for more than one resulting
+ // APIResourceList to have the same GroupVersion, the lists would need merged.
+ gl, err := d.delegate.ServerGroups()
+ if err != nil || len(gl.Groups) == 0 {
+ utilruntime.HandleError(fmt.Errorf("couldn't get current server API group list: %v", err))
+ return err
+ }
+
+ wg := &sync.WaitGroup{}
+ resultLock := &sync.Mutex{}
+ rl := map[string]*cacheEntry{}
+ for _, g := range gl.Groups {
+ for _, v := range g.Versions {
+ gv := v.GroupVersion
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer utilruntime.HandleCrash()
+
+ r, err := d.serverResourcesForGroupVersion(gv)
+ if err != nil {
+ utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", gv, err))
+ }
+
+ resultLock.Lock()
+ defer resultLock.Unlock()
+ rl[gv] = &cacheEntry{r, err}
+ }()
+ }
+ }
+ wg.Wait()
+
+ d.groupToServerResources, d.groupList = rl, gl
+ d.cacheValid = true
+ return nil
+}
+
+func (d *memcachedDiscoveryClient) serverResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
+ r, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
+ if err != nil {
+ return r, err
+ }
+ if len(r.APIResources) == 0 {
+ return r, fmt.Errorf("Got empty response for: %v", groupVersion)
+ }
+ return r, nil
+}
+
+var _ discovery.CachedDiscoveryInterface = &memcachedDiscoveryClient{}
+
+// MaybeMarkStale calls MarkStale on the discovery client, if the
+// client is a memcachedClient.
+func MaybeMarkStale(d discovery.DiscoveryInterface) {
+ if c, ok := d.(*memcachedDiscoveryClient); ok {
+ c.Invalidate()
+ }
+}
+
+func (c *memcachedDiscoveryClient) MarkStale() {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ log.Debug("Marking cached discovery info (potentially) stale")
+ c.cacheValid = false
+}
+
+// ClientForResource returns the ResourceClient for a given object
+func ClientForResource(client dynamic.Interface, mapper meta.RESTMapper, obj runtime.Object, defNs string) (dynamic.ResourceInterface, error) {
+ gvk := obj.GetObjectKind().GroupVersionKind()
+
+ mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+ if err != nil {
+ return nil, err
+ }
+
+ rc := client.Resource(mapping.Resource)
+
+ switch mapping.Scope.Name() {
+ case meta.RESTScopeNameRoot:
+ return rc, nil
+ case meta.RESTScopeNameNamespace:
+ meta, err := meta.Accessor(obj)
+ if err != nil {
+ return nil, err
+ }
+ namespace := meta.GetNamespace()
+ if namespace == "" {
+ namespace = defNs
+ }
+ return rc.Namespace(namespace), nil
+ default:
+ return nil, fmt.Errorf("unexpected resource scope %q", mapping.Scope)
+ }
+}
+
+// NewmemcachedDiscoveryClient creates a new CachedDiscoveryInterface which caches
+// discovery information in memory and will stay up-to-date if Invalidate is
+// called with regularity.
+//
+// NOTE: The client will NOT resort to live lookups on cache misses.
+func NewMemcachedDiscoveryClient(delegate discovery.DiscoveryInterface) discovery.CachedDiscoveryInterface {
+ return &memcachedDiscoveryClient{
+ delegate: delegate,
+ groupToServerResources: map[string]*cacheEntry{},
+ }
+}
diff --git a/cluster/tools/kartongips/utils/importer.go b/cluster/tools/kartongips/utils/importer.go
new file mode 100644
index 0000000..b456f0d
--- /dev/null
+++ b/cluster/tools/kartongips/utils/importer.go
@@ -0,0 +1,170 @@
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ assetfs "github.com/elazarl/go-bindata-assetfs"
+ jsonnet "github.com/google/go-jsonnet"
+ log "github.com/sirupsen/logrus"
+)
+
+var errNotFound = errors.New("Not found")
+
+var extVarKindRE = regexp.MustCompile("^<(?:extvar|top-level-arg):.+>$")
+
+//go:generate go-bindata -nometadata -ignore .*_test\.|~$DOLLAR -pkg $GOPACKAGE -o bindata.go -prefix ../ ../lib/...
+func newInternalFS(prefix string) http.FileSystem {
+ // Asset/AssetDir returns `fmt.Errorf("Asset %s not found")`,
+ // which does _not_ get mapped to 404 by `http.FileSystem`.
+ // Need to convert to `os.ErrNotExist` explicitly ourselves.
+ mapNotFound := func(err error) error {
+ if err != nil && strings.Contains(err.Error(), "not found") {
+ err = os.ErrNotExist
+ }
+ return err
+ }
+ return &assetfs.AssetFS{
+ Asset: func(path string) ([]byte, error) {
+ ret, err := Asset(path)
+ return ret, mapNotFound(err)
+ },
+ AssetDir: func(path string) ([]string, error) {
+ ret, err := AssetDir(path)
+ return ret, mapNotFound(err)
+ },
+ Prefix: prefix,
+ }
+}
+
+/*
+MakeUniversalImporter creates an importer that handles resolving imports from the filesystem and HTTP/S.
+
+In addition to the standard importer, supports:
+ - URLs in import statements
+ - URLs in library search paths
+
+A real-world example:
+ - You have https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master in your search URLs.
+ - You evaluate a local file which calls `import "ksonnet.beta.2/k.libsonnet"`.
+ - If the `ksonnet.beta.2/k.libsonnet`` is not located in the current working directory, an attempt
+ will be made to follow the search path, i.e. to download
+ https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master/ksonnet.beta.2/k.libsonnet.
+ - Since the downloaded `k.libsonnet`` file turn in contains `import "k8s.libsonnet"`, the import
+ will be resolved as https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master/ksonnet.beta.2/k8s.libsonnet
+ and downloaded from that location.
+*/
+func MakeUniversalImporter(searchURLs []*url.URL) jsonnet.Importer {
+ // Reconstructed copy of http.DefaultTransport (to avoid
+ // modifying the default)
+ t := &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ DualStack: true,
+ }).DialContext,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ }
+
+ t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+ t.RegisterProtocol("internal", http.NewFileTransport(newInternalFS("lib")))
+
+ return &universalImporter{
+ BaseSearchURLs: searchURLs,
+ HTTPClient: &http.Client{Transport: t},
+ cache: map[string]jsonnet.Contents{},
+ }
+}
+
+type universalImporter struct {
+ BaseSearchURLs []*url.URL
+ HTTPClient *http.Client
+ cache map[string]jsonnet.Contents
+}
+
+func (importer *universalImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error) {
+ log.Debugf("Importing %q from %q", importedPath, importedFrom)
+
+ candidateURLs, err := importer.expandImportToCandidateURLs(importedFrom, importedPath)
+ if err != nil {
+ return jsonnet.Contents{}, "", fmt.Errorf("Could not get candidate URLs for when importing %s (imported from %s): %v", importedPath, importedFrom, err)
+ }
+
+ var tried []string
+ for _, u := range candidateURLs {
+ foundAt := u.String()
+ if c, ok := importer.cache[foundAt]; ok {
+ return c, foundAt, nil
+ }
+
+ tried = append(tried, foundAt)
+ importedData, err := importer.tryImport(foundAt)
+ if err == nil {
+ importer.cache[foundAt] = importedData
+ return importedData, foundAt, nil
+ } else if err != errNotFound {
+ return jsonnet.Contents{}, "", err
+ }
+ }
+
+ return jsonnet.Contents{}, "", fmt.Errorf("Couldn't open import %q, no match locally or in library search paths. Tried: %s",
+ importedPath,
+ strings.Join(tried, ";"),
+ )
+}
+
+func (importer *universalImporter) tryImport(url string) (jsonnet.Contents, error) {
+ res, err := importer.HTTPClient.Get(url)
+ if err != nil {
+ return jsonnet.Contents{}, err
+ }
+ defer res.Body.Close()
+ log.Debugf("GET %q -> %s", url, res.Status)
+ if res.StatusCode == http.StatusNotFound {
+ return jsonnet.Contents{}, errNotFound
+ } else if res.StatusCode != http.StatusOK {
+ return jsonnet.Contents{}, fmt.Errorf("error reading content: %s", res.Status)
+ }
+
+ bodyBytes, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return jsonnet.Contents{}, err
+ }
+ return jsonnet.MakeContents(string(bodyBytes)), nil
+}
+
+func (importer *universalImporter) expandImportToCandidateURLs(importedFrom, importedPath string) ([]*url.URL, error) {
+ importedPathURL, err := url.Parse(importedPath)
+ if err != nil {
+ return nil, fmt.Errorf("Import path %q is not valid", importedPath)
+ }
+ if importedPathURL.IsAbs() {
+ return []*url.URL{importedPathURL}, nil
+ }
+
+ importDirURL, err := url.Parse(importedFrom)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid import dir %q: %v", importedFrom, err)
+ }
+
+ candidateURLs := make([]*url.URL, 1, len(importer.BaseSearchURLs)+1)
+ candidateURLs[0] = importDirURL.ResolveReference(importedPathURL)
+
+ for _, u := range importer.BaseSearchURLs {
+ candidateURLs = append(candidateURLs, u.ResolveReference(importedPathURL))
+ }
+
+ return candidateURLs, nil
+}
diff --git a/cluster/tools/kartongips/utils/importer_test.go b/cluster/tools/kartongips/utils/importer_test.go
new file mode 100644
index 0000000..c0b5d9d
--- /dev/null
+++ b/cluster/tools/kartongips/utils/importer_test.go
@@ -0,0 +1,56 @@
+package utils
+
+import (
+ "net/url"
+ "os"
+ "reflect"
+ "testing"
+)
+
+func TestInternalFS(t *testing.T) {
+ fs := newInternalFS("lib")
+ if _, err := fs.Open("kubecfg.libsonnet"); err != nil {
+ t.Errorf("opening kubecfg.libsonnet failed! %v", err)
+ }
+ if _, err := fs.Open("noexist"); !os.IsNotExist(err) {
+ t.Errorf("Incorrect noexist error: %v", err)
+ }
+ if _, err := fs.Open("noexist/foo"); !os.IsNotExist(err) {
+ t.Errorf("Incorrect noexist dir error: %v", err)
+ }
+
+ // This test really belongs somewhere else, but it's easiest
+ // to do here.
+ if _, err := fs.Open("kubecfg_test.jsonnet"); err == nil {
+ t.Errorf("kubecfg_test.jsonnet should not have been embedded")
+ }
+}
+
+func TestExpandImportToCandidateURLs(t *testing.T) {
+ importer := universalImporter{
+ BaseSearchURLs: []*url.URL{
+ {Scheme: "file", Path: "/first/base/search/"},
+ },
+ }
+
+ t.Run("Absolute URL in import statement yields a single candidate", func(t *testing.T) {
+ urls, _ := importer.expandImportToCandidateURLs("dir", "http://absolute.com/import/path")
+ expected := []*url.URL{
+ {Scheme: "http", Host: "absolute.com", Path: "/import/path"},
+ }
+ if !reflect.DeepEqual(urls, expected) {
+ t.Errorf("Expected %v, got %v", expected, urls)
+ }
+ })
+
+ t.Run("Absolute URL in import dir is searched before BaseSearchURLs", func(t *testing.T) {
+ urls, _ := importer.expandImportToCandidateURLs("file:///abs/import/dir/", "relative/file.libsonnet")
+ expected := []*url.URL{
+ {Scheme: "file", Host: "", Path: "/abs/import/dir/relative/file.libsonnet"},
+ {Scheme: "file", Host: "", Path: "/first/base/search/relative/file.libsonnet"},
+ }
+ if !reflect.DeepEqual(urls, expected) {
+ t.Errorf("Expected %v, got %v", expected, urls)
+ }
+ })
+}
diff --git a/cluster/tools/kartongips/utils/meta.go b/cluster/tools/kartongips/utils/meta.go
new file mode 100644
index 0000000..1628c10
--- /dev/null
+++ b/cluster/tools/kartongips/utils/meta.go
@@ -0,0 +1,199 @@
+package utils
+
+import (
+ "compress/gzip"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/version"
+ "k8s.io/client-go/discovery"
+)
+
+// Format v0.0.0(-master+$Format:%h$)
+var gitVersionRe = regexp.MustCompile("v([0-9])+.([0-9])+.[0-9]+.*")
+
+// ServerVersion captures k8s major.minor version in a parsed form
+type ServerVersion struct {
+ Major int
+ Minor int
+}
+
+func parseGitVersion(gitVersion string) (ServerVersion, error) {
+ parsedVersion := gitVersionRe.FindStringSubmatch(gitVersion)
+ if len(parsedVersion) != 3 {
+ return ServerVersion{}, fmt.Errorf("Unable to parse git version %s", gitVersion)
+ }
+ var ret ServerVersion
+ var err error
+ ret.Major, err = strconv.Atoi(parsedVersion[1])
+ if err != nil {
+ return ServerVersion{}, err
+ }
+ ret.Minor, err = strconv.Atoi(parsedVersion[2])
+ if err != nil {
+ return ServerVersion{}, err
+ }
+ return ret, nil
+}
+
+// ParseVersion parses version.Info into a ServerVersion struct
+func ParseVersion(v *version.Info) (ServerVersion, error) {
+ var ret ServerVersion
+ var err error
+ ret.Major, err = strconv.Atoi(v.Major)
+ if err != nil {
+ // Try to parse using GitVersion
+ return parseGitVersion(v.GitVersion)
+ }
+
+ // trim "+" in minor version (happened on GKE)
+ v.Minor = strings.TrimSuffix(v.Minor, "+")
+ ret.Minor, err = strconv.Atoi(v.Minor)
+ if err != nil {
+ // Try to parse using GitVersion
+ return parseGitVersion(v.GitVersion)
+ }
+ return ret, err
+}
+
+// FetchVersion fetches version information from discovery client, and parses
+func FetchVersion(v discovery.ServerVersionInterface) (ret ServerVersion, err error) {
+ version, err := v.ServerVersion()
+ if err != nil {
+ return ServerVersion{}, err
+ }
+ return ParseVersion(version)
+}
+
+// GetDefaultVersion returns a default server version. This value will be updated
+// periodically to match a current/popular version corresponding to the age of this code
+// Current default version: 1.8
+func GetDefaultVersion() ServerVersion {
+ return ServerVersion{Major: 1, Minor: 8}
+}
+
+// Compare returns -1/0/+1 iff v is less than / equal / greater than major.minor
+func (v ServerVersion) Compare(major, minor int) int {
+ a := v.Major
+ b := major
+
+ if a == b {
+ a = v.Minor
+ b = minor
+ }
+
+ var res int
+ if a > b {
+ res = 1
+ } else if a == b {
+ res = 0
+ } else {
+ res = -1
+ }
+ return res
+}
+
+func (v ServerVersion) String() string {
+ return fmt.Sprintf("%d.%d", v.Major, v.Minor)
+}
+
+// SetMetaDataAnnotation sets an annotation value
+func SetMetaDataAnnotation(obj metav1.Object, key, value string) {
+ a := obj.GetAnnotations()
+ if a == nil {
+ a = make(map[string]string)
+ }
+ a[key] = value
+ obj.SetAnnotations(a)
+}
+
+// DeleteMetaDataAnnotation removes an annotation value
+func DeleteMetaDataAnnotation(obj metav1.Object, key string) {
+ a := obj.GetAnnotations()
+ if a != nil {
+ delete(a, key)
+ obj.SetAnnotations(a)
+ }
+}
+
+// SetMetaDataLabel sets an annotation value
+func SetMetaDataLabel(obj metav1.Object, key, value string) {
+ l := obj.GetLabels()
+ if l == nil {
+ l = make(map[string]string)
+ }
+ l[key] = value
+ obj.SetLabels(l)
+}
+
+// DeleteMetaDataLabel removes a label value
+func DeleteMetaDataLabel(obj metav1.Object, key string) {
+ l := obj.GetLabels()
+ if l != nil {
+ delete(l, key)
+ obj.SetLabels(l)
+ }
+}
+
+// ResourceNameFor returns a lowercase plural form of a type, for
+// human messages. Returns lowercased kind if discovery lookup fails.
+func ResourceNameFor(mapper meta.RESTMapper, o runtime.Object) string {
+ gvk := o.GetObjectKind().GroupVersionKind()
+ mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+ if err != nil {
+ log.Debugf("RESTMapper failed for %s (%s), falling back to kind", gvk, err)
+ return strings.ToLower(gvk.Kind)
+ }
+
+ return mapping.Resource.Resource
+}
+
+// FqName returns "namespace.name"
+func FqName(o metav1.Object) string {
+ if o.GetNamespace() == "" {
+ return o.GetName()
+ }
+ return fmt.Sprintf("%s.%s", o.GetNamespace(), o.GetName())
+}
+
+// CompactEncodeObject returns a compact string representation
+// (json->gzip->base64) of an object, intended for use in
+// last-applied-configuration annotation.
+func CompactEncodeObject(o runtime.Object) (string, error) {
+ var buf strings.Builder
+ b64enc := base64.NewEncoder(base64.StdEncoding, &buf)
+ zw := gzip.NewWriter(b64enc)
+ jsenc := json.NewEncoder(zw)
+ jsenc.SetEscapeHTML(false)
+ jsenc.SetIndent("", "")
+
+ if err := jsenc.Encode(o); err != nil {
+ return "", err
+ }
+
+ zw.Close()
+ b64enc.Close()
+
+ return buf.String(), nil
+}
+
+// CompactDecodeObject does the reverse of CompactEncodeObject.
+func CompactDecodeObject(data string, into runtime.Object) error {
+ zr, err := gzip.NewReader(
+ base64.NewDecoder(base64.StdEncoding,
+ strings.NewReader(data)))
+ if err != nil {
+ return err
+ }
+
+ jsdec := json.NewDecoder(zr)
+ return jsdec.Decode(into)
+}
diff --git a/cluster/tools/kartongips/utils/meta_test.go b/cluster/tools/kartongips/utils/meta_test.go
new file mode 100644
index 0000000..e83e3a7
--- /dev/null
+++ b/cluster/tools/kartongips/utils/meta_test.go
@@ -0,0 +1,179 @@
+package utils
+
+import (
+ "testing"
+
+ apiequality "k8s.io/apimachinery/pkg/api/equality"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/util/diff"
+ "k8s.io/apimachinery/pkg/version"
+)
+
+func TestParseVersion(t *testing.T) {
+ tests := []struct {
+ input version.Info
+ expected ServerVersion
+ error bool
+ }{
+ {
+ input: version.Info{Major: "1", Minor: "6"},
+ expected: ServerVersion{Major: 1, Minor: 6},
+ },
+ {
+ input: version.Info{Major: "1", Minor: "70"},
+ expected: ServerVersion{Major: 1, Minor: 70},
+ },
+ {
+ input: version.Info{Major: "1", Minor: "6x"},
+ error: true,
+ },
+ {
+ input: version.Info{Major: "1", Minor: "8+"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "", Minor: "", GitVersion: "v1.8.0"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "1", Minor: "", GitVersion: "v1.8.0"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "", Minor: "8", GitVersion: "v1.8.0"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "", Minor: "", GitVersion: "v1.8.8-test.0"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "1", Minor: "8", GitVersion: "v1.9.0"},
+ expected: ServerVersion{Major: 1, Minor: 8},
+ },
+ {
+ input: version.Info{Major: "", Minor: "", GitVersion: "v1.a"},
+ error: true,
+ },
+ }
+
+ for _, test := range tests {
+ v, err := ParseVersion(&test.input)
+ if test.error {
+ if err == nil {
+ t.Errorf("test %s should have failed and did not", test.input)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("test %v failed: %v", test.input, err)
+ continue
+ }
+ if v != test.expected {
+ t.Errorf("Expected %v, got %v", test.expected, v)
+ }
+ }
+}
+
+func TestVersionCompare(t *testing.T) {
+ v := ServerVersion{Major: 2, Minor: 3}
+ tests := []struct {
+ major, minor, result int
+ }{
+ {major: 1, minor: 0, result: 1},
+ {major: 2, minor: 0, result: 1},
+ {major: 2, minor: 2, result: 1},
+ {major: 2, minor: 3, result: 0},
+ {major: 2, minor: 4, result: -1},
+ {major: 3, minor: 0, result: -1},
+ }
+ for _, test := range tests {
+ res := v.Compare(test.major, test.minor)
+ if res != test.result {
+ t.Errorf("%d.%d => Expected %d, got %d", test.major, test.minor, test.result, res)
+ }
+ }
+}
+
+func TestResourceNameFor(t *testing.T) {
+ obj := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "tests/v1alpha1",
+ "kind": "Test",
+ "metadata": map[string]interface{}{
+ "name": "myname",
+ "namespace": "mynamespace",
+ },
+ },
+ }
+
+ mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{})
+ mapper.Add(schema.GroupVersionKind{Group: "tests", Version: "v1alpha1", Kind: "Test"}, meta.RESTScopeNamespace)
+
+ if n := ResourceNameFor(mapper, obj); n != "tests" {
+ t.Errorf("Got resource name %q for %v", n, obj)
+ }
+
+ obj.SetKind("Unknown")
+ if n := ResourceNameFor(mapper, obj); n != "unknown" {
+ t.Errorf("Got resource name %q for %v", n, obj)
+ }
+
+ obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "unknown", Version: "noversion", Kind: "SomeKind"})
+ if n := ResourceNameFor(mapper, obj); n != "somekind" {
+ t.Errorf("Got resource name %q for %v", n, obj)
+ }
+}
+
+func TestFqName(t *testing.T) {
+ obj := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "tests/v1alpha1",
+ "kind": "Test",
+ "metadata": map[string]interface{}{
+ "name": "myname",
+ },
+ },
+ }
+
+ if n := FqName(obj); n != "myname" {
+ t.Errorf("Got %q for %v", n, obj)
+ }
+
+ obj.SetNamespace("mynamespace")
+ if n := FqName(obj); n != "mynamespace.myname" {
+ t.Errorf("Got %q for %v", n, obj)
+ }
+}
+
+func TestCompactEncodeRoundTrip(t *testing.T) {
+ obj := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "tests/v1alpha1",
+ "kind": "Test",
+ "metadata": map[string]interface{}{
+ "name": "myname",
+ },
+ "foo": true,
+ },
+ }
+
+ data, err := CompactEncodeObject(obj)
+ if err != nil {
+ t.Errorf("CompactEncodeObject returned %v", err)
+ }
+ t.Logf("compact encoding is %d bytes", len(data))
+
+ out := &unstructured.Unstructured{}
+ if err := CompactDecodeObject(data, out); err != nil {
+ t.Errorf("CompactDecodeObject returned %v", err)
+ }
+
+ t.Logf("in: %#v", obj)
+ t.Logf("out: %#v", out)
+ if !apiequality.Semantic.DeepEqual(obj, out) {
+ t.Error("Objects differed: ", diff.ObjectDiff(obj, out))
+ }
+}
diff --git a/cluster/tools/kartongips/utils/nativefuncs.go b/cluster/tools/kartongips/utils/nativefuncs.go
new file mode 100644
index 0000000..d838d17
--- /dev/null
+++ b/cluster/tools/kartongips/utils/nativefuncs.go
@@ -0,0 +1,143 @@
+// 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 (
+ "bytes"
+ "encoding/json"
+ "io"
+ "regexp"
+ "strings"
+
+ goyaml "github.com/ghodss/yaml"
+
+ jsonnet "github.com/google/go-jsonnet"
+ jsonnetAst "github.com/google/go-jsonnet/ast"
+ "k8s.io/apimachinery/pkg/util/yaml"
+)
+
+func resolveImage(resolver Resolver, image string) (string, error) {
+ n, err := ParseImageName(image)
+ if err != nil {
+ return "", err
+ }
+
+ if err := resolver.Resolve(&n); err != nil {
+ return "", err
+ }
+
+ return n.String(), nil
+}
+
+// RegisterNativeFuncs adds kubecfg's native jsonnet functions to provided VM
+func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver) {
+ // TODO(mkm): go-jsonnet 0.12.x now contains native std.parseJson; deprecate and remove this one.
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "parseJson",
+ Params: []jsonnetAst.Identifier{"json"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ data := []byte(args[0].(string))
+ err = json.Unmarshal(data, &res)
+ return
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "parseYaml",
+ Params: []jsonnetAst.Identifier{"yaml"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ ret := []interface{}{}
+ data := []byte(args[0].(string))
+ 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
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "manifestJson",
+ Params: []jsonnetAst.Identifier{"json", "indent"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ value := args[0]
+ indent := int(args[1].(float64))
+ data, err := json.MarshalIndent(value, "", strings.Repeat(" ", indent))
+ if err != nil {
+ return "", err
+ }
+ data = append(data, byte('\n'))
+ return string(data), nil
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "manifestYaml",
+ Params: []jsonnetAst.Identifier{"json"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ value := args[0]
+ output, err := goyaml.Marshal(value)
+ return string(output), err
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "resolveImage",
+ Params: []jsonnetAst.Identifier{"image"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ return resolveImage(resolver, args[0].(string))
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "escapeStringRegex",
+ Params: []jsonnetAst.Identifier{"str"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ return regexp.QuoteMeta(args[0].(string)), nil
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "regexMatch",
+ Params: []jsonnetAst.Identifier{"regex", "string"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ return regexp.MatchString(args[0].(string), args[1].(string))
+ },
+ })
+
+ vm.NativeFunction(&jsonnet.NativeFunction{
+ Name: "regexSubst",
+ Params: []jsonnetAst.Identifier{"regex", "src", "repl"},
+ Func: func(args []interface{}) (res interface{}, err error) {
+ regex := args[0].(string)
+ src := args[1].(string)
+ repl := args[2].(string)
+
+ r, err := regexp.Compile(regex)
+ if err != nil {
+ return "", err
+ }
+ return r.ReplaceAllString(src, repl), nil
+ },
+ })
+}
diff --git a/cluster/tools/kartongips/utils/nativefuncs_test.go b/cluster/tools/kartongips/utils/nativefuncs_test.go
new file mode 100644
index 0000000..cb98f10
--- /dev/null
+++ b/cluster/tools/kartongips/utils/nativefuncs_test.go
@@ -0,0 +1,104 @@
+// 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 (
+ "testing"
+
+ jsonnet "github.com/google/go-jsonnet"
+)
+
+// 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.MakeVM()
+ RegisterNativeFuncs(vm, NewIdentityResolver())
+
+ _, 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.MakeVM()
+ RegisterNativeFuncs(vm, NewIdentityResolver())
+
+ _, 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")
+}
+
+func TestRegexMatch(t *testing.T) {
+ vm := jsonnet.MakeVM()
+ RegisterNativeFuncs(vm, NewIdentityResolver())
+
+ _, err := vm.EvaluateSnippet("failtest", `std.native("regexMatch")("[f", "foo")`)
+ if err == nil {
+ t.Errorf("regexMatch succeeded with invalid regex")
+ }
+
+ x, err := vm.EvaluateSnippet("test", `std.native("regexMatch")("foo.*", "seafood")`)
+ check(t, err, x, "true\n")
+
+ x, err = vm.EvaluateSnippet("test", `std.native("regexMatch")("bar.*", "seafood")`)
+ check(t, err, x, "false\n")
+}
+
+func TestRegexSubst(t *testing.T) {
+ vm := jsonnet.MakeVM()
+ RegisterNativeFuncs(vm, NewIdentityResolver())
+
+ _, err := vm.EvaluateSnippet("failtest", `std.native("regexSubst")("[f",s "foo", "bar")`)
+ if err == nil {
+ t.Errorf("regexSubst succeeded with invalid regex")
+ }
+
+ x, err := vm.EvaluateSnippet("test", `std.native("regexSubst")("a(x*)b", "-ab-axxb-", "T")`)
+ check(t, err, x, "\"-T-T-\"\n")
+
+ x, err = vm.EvaluateSnippet("test", `std.native("regexSubst")("a(x*)b", "-ab-axxb-", "${1}W")`)
+ check(t, err, x, "\"-W-xxW-\"\n")
+}
diff --git a/cluster/tools/kartongips/utils/openapi.go b/cluster/tools/kartongips/utils/openapi.go
new file mode 100644
index 0000000..4708bf1
--- /dev/null
+++ b/cluster/tools/kartongips/utils/openapi.go
@@ -0,0 +1,65 @@
+// 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 (
+ "fmt"
+
+ log "github.com/sirupsen/logrus"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/discovery"
+ "k8s.io/kube-openapi/pkg/util/proto"
+ "k8s.io/kube-openapi/pkg/util/proto/validation"
+ "k8s.io/kubectl/pkg/util/openapi"
+)
+
+// OpenAPISchema represents an OpenAPI schema for a given GroupVersionKind.
+type OpenAPISchema struct {
+ schema proto.Schema
+}
+
+// NewOpenAPISchemaFor returns the OpenAPISchema object ready to validate objects of given GroupVersion
+func NewOpenAPISchemaFor(delegate discovery.OpenAPISchemaInterface, gvk schema.GroupVersionKind) (*OpenAPISchema, error) {
+ log.Debugf("Fetching schema for %v", gvk)
+ doc, err := delegate.OpenAPISchema()
+ if err != nil {
+ return nil, err
+ }
+ res, err := openapi.NewOpenAPIData(doc)
+ if err != nil {
+ return nil, err
+ }
+
+ sc := res.LookupResource(gvk)
+ if sc == nil {
+ gvr := schema.GroupResource{
+ // TODO(mkm): figure out a meaningful group+resource for schemas.
+ Group: "schema",
+ Resource: "schema",
+ }
+ return nil, errors.NewNotFound(gvr, fmt.Sprintf("%s", gvk))
+ }
+ return &OpenAPISchema{schema: sc}, nil
+}
+
+// Validate is the primary entrypoint into this class
+func (s *OpenAPISchema) Validate(obj *unstructured.Unstructured) []error {
+ gvk := obj.GroupVersionKind()
+ log.Infof("validate object %q", gvk)
+ return validation.ValidateModel(obj.UnstructuredContent(), s.schema, fmt.Sprintf("%s.%s", gvk.Version, gvk.Kind))
+}
diff --git a/cluster/tools/kartongips/utils/openapi_test.go b/cluster/tools/kartongips/utils/openapi_test.go
new file mode 100644
index 0000000..3f0a5d0
--- /dev/null
+++ b/cluster/tools/kartongips/utils/openapi_test.go
@@ -0,0 +1,94 @@
+// 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 (
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/protobuf/proto"
+ openapi_v2 "github.com/googleapis/gnostic/openapiv2"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ utilerrors "k8s.io/apimachinery/pkg/util/errors"
+)
+
+type schemaFromFile struct {
+ dir string
+}
+
+func (s schemaFromFile) OpenAPISchema() (*openapi_v2.Document, error) {
+ var doc openapi_v2.Document
+ b, err := ioutil.ReadFile(filepath.Join(s.dir, "schema.pb"))
+ if err != nil {
+ return nil, err
+ }
+ if err := proto.Unmarshal(b, &doc); err != nil {
+ return nil, err
+ }
+ return &doc, nil
+}
+
+func TestValidate(t *testing.T) {
+ schemaReader := schemaFromFile{dir: filepath.FromSlash("../testdata")}
+ s, err := NewOpenAPISchemaFor(schemaReader, schema.GroupVersionKind{Version: "v1", Kind: "Service"})
+ if err != nil {
+ t.Fatalf("Error reading schema: %v", err)
+ }
+
+ valid := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Service",
+ "spec": map[string]interface{}{
+ "ports": []interface{}{
+ map[string]interface{}{"port": 80},
+ },
+ },
+ },
+ }
+ if errs := s.Validate(valid); len(errs) != 0 {
+ t.Errorf("schema errors from valid object: %v", errs)
+ }
+
+ invalid := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Service",
+ "spec": map[string]interface{}{
+ "bogus": false,
+ "ports": []interface{}{
+ map[string]interface{}{"port": "bogus"},
+ },
+ },
+ },
+ }
+ errs := s.Validate(invalid)
+ if len(errs) == 0 {
+ t.Error("no schema errors from invalid object :(")
+ }
+ err = utilerrors.NewAggregate(errs)
+ t.Logf("Invalid object produced error: %v", err)
+
+ if !strings.Contains(err.Error(), `invalid type for io.k8s.api.core.v1.ServicePort.port: got "string", expected "integer"`) {
+ t.Errorf("Wrong error1 produced from invalid object: %v", err)
+ }
+ if !strings.Contains(err.Error(), `ValidationError(v1.Service.spec): unknown field "bogus" in io.k8s.api.core.v1.ServiceSpec`) {
+ t.Errorf("Wrong error2 produced from invalid object: %q", err)
+ }
+}
diff --git a/cluster/tools/kartongips/utils/resolver.go b/cluster/tools/kartongips/utils/resolver.go
new file mode 100644
index 0000000..9a9cc84
--- /dev/null
+++ b/cluster/tools/kartongips/utils/resolver.go
@@ -0,0 +1,165 @@
+// 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 (
+ "bytes"
+ "context"
+ "fmt"
+
+ "github.com/genuinetools/reg/registry"
+ "github.com/genuinetools/reg/repoutils"
+)
+
+const defaultRegistry = "registry-1.docker.io"
+
+// ImageName represents the parts of a docker image name
+type ImageName struct {
+ // eg: "myregistryhost:5000/fedora/httpd:version1.0"
+ Registry string // "myregistryhost:5000"
+ Repository string // "fedora"
+ Name string // "httpd"
+ Tag string // "version1.0"
+ Digest string
+}
+
+// String implements the Stringer interface
+func (n ImageName) String() string {
+ buf := bytes.Buffer{}
+ if n.Registry != "" {
+ buf.WriteString(n.Registry)
+ buf.WriteString("/")
+ }
+ if n.Repository != "" {
+ buf.WriteString(n.Repository)
+ buf.WriteString("/")
+ }
+ buf.WriteString(n.Name)
+ if n.Digest != "" {
+ buf.WriteString("@")
+ buf.WriteString(n.Digest)
+ } else {
+ buf.WriteString(":")
+ buf.WriteString(n.Tag)
+ }
+ return buf.String()
+}
+
+// RegistryRepoName returns the "repository" as used in the registry URL
+func (n ImageName) RegistryRepoName() string {
+ repo := n.Repository
+ if repo == "" {
+ repo = "library"
+ }
+ return fmt.Sprintf("%s/%s", repo, n.Name)
+}
+
+// RegistryURL returns the deduced base URL of the registry for this image
+func (n ImageName) RegistryURL() string {
+ reg := n.Registry
+ if reg == "" {
+ reg = defaultRegistry
+ }
+ return fmt.Sprintf("https://%s", reg)
+}
+
+// ParseImageName parses a docker image into an ImageName struct.
+func ParseImageName(image string) (ImageName, error) {
+ ret := ImageName{}
+
+ img, err := registry.ParseImage(image)
+ if err != nil {
+ return ret, err
+ }
+
+ ret.Registry = img.Domain
+ ret.Name = img.Path
+ ret.Digest = img.Digest.String()
+ ret.Tag = img.Tag
+
+ return ret, nil
+}
+
+// Resolver is able to resolve docker image names into more specific forms
+type Resolver interface {
+ Resolve(image *ImageName) error
+}
+
+// NewIdentityResolver returns a resolver that does only trivial
+// :latest canonicalisation
+func NewIdentityResolver() Resolver {
+ return identityResolver{}
+}
+
+type identityResolver struct{}
+
+func (r identityResolver) Resolve(image *ImageName) error {
+ return nil
+}
+
+// NewRegistryResolver returns a resolver that looks up a docker
+// registry to resolve digests
+func NewRegistryResolver(opt registry.Opt) Resolver {
+ return ®istryResolver{
+ opt: opt,
+ cache: make(map[string]string),
+ }
+}
+
+type registryResolver struct {
+ opt registry.Opt
+ cache map[string]string
+}
+
+func (r *registryResolver) Resolve(n *ImageName) error {
+ // TODO: get context from caller.
+ ctx := context.Background()
+
+ if n.Digest != "" {
+ // Already has explicit digest
+ return nil
+ }
+
+ if digest, ok := r.cache[n.String()]; ok {
+ n.Digest = digest
+ return nil
+ }
+
+ img, err := registry.ParseImage(n.String())
+ if err != nil {
+ return fmt.Errorf("unable to parse image name: %v", err)
+ }
+
+ auth, err := repoutils.GetAuthConfig("", "", img.Domain)
+ if err != nil {
+ return fmt.Errorf("unable to get auth config for registry: %v", err)
+ }
+
+ c, err := registry.New(ctx, auth, r.opt)
+ if err != nil {
+ return fmt.Errorf("unable to create registry client: %v", err)
+ }
+
+ digest, err := c.Digest(ctx, img)
+ if err != nil {
+ return fmt.Errorf("unable to get digest from the registry: %v", err)
+ }
+
+ n.Digest = digest.String()
+ r.cache[n.String()] = n.Digest
+
+ return nil
+}
diff --git a/cluster/tools/kartongips/utils/sort.go b/cluster/tools/kartongips/utils/sort.go
new file mode 100644
index 0000000..dc9838c
--- /dev/null
+++ b/cluster/tools/kartongips/utils/sort.go
@@ -0,0 +1,159 @@
+// 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 (
+ "sort"
+
+ log "github.com/sirupsen/logrus"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/discovery"
+ "k8s.io/kube-openapi/pkg/util/proto"
+)
+
+var (
+ gkTpr = schema.GroupKind{Group: "extensions", Kind: "ThirdPartyResource"}
+ gkCrd = schema.GroupKind{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}
+ gkValidatingWebhook = schema.GroupKind{Group: "admissionregistration.k8s.io", Kind: "ValidatingWebhookConfiguration"}
+ gkMutatingWebhook = schema.GroupKind{Group: "admissionregistration.k8s.io", Kind: "MutatingWebhookConfiguration"}
+)
+
+// a podSpecVisitor traverses a schema tree and records whether the schema
+// contains a PodSpec resource.
+type podSpecVisitor bool
+
+func (v *podSpecVisitor) VisitKind(k *proto.Kind) {
+ if k.GetPath().String() == "io.k8s.api.core.v1.PodSpec" {
+ *v = true
+ return
+ }
+ for _, f := range k.Fields {
+ f.Accept(v)
+ if *v == true {
+ return
+ }
+ }
+}
+
+func (v *podSpecVisitor) VisitReference(s proto.Reference) { s.SubSchema().Accept(v) }
+func (v *podSpecVisitor) VisitArray(s *proto.Array) { s.SubType.Accept(v) }
+func (v *podSpecVisitor) VisitMap(s *proto.Map) { s.SubType.Accept(v) }
+func (v *podSpecVisitor) VisitPrimitive(p *proto.Primitive) {}
+
+var podSpecCache = map[string]podSpecVisitor{}
+
+func containsPodSpec(disco discovery.OpenAPISchemaInterface, gvk schema.GroupVersionKind) bool {
+ result, ok := podSpecCache[gvk.String()]
+ if ok {
+ return bool(result)
+ }
+
+ oapi, err := NewOpenAPISchemaFor(disco, gvk)
+ if err != nil {
+ log.Debugf("error fetching schema for %s: %v", gvk, err)
+ return false
+ }
+
+ oapi.schema.Accept(&result)
+ podSpecCache[gvk.String()] = result
+
+ return bool(result)
+}
+
+// Arbitrary numbers used to do a simple topological sort of resources.
+func depTier(disco discovery.OpenAPISchemaInterface, mapper meta.RESTMapper, o schema.ObjectKind) (int, error) {
+ gvk := o.GroupVersionKind()
+ gk := gvk.GroupKind()
+ if gk == gkTpr || gk == gkCrd {
+ // Special case (first): these create other types
+ return 10, nil
+ } else if gk == gkValidatingWebhook || gk == gkMutatingWebhook {
+ // Special case (last): these require operational services
+ return 200, nil
+ }
+
+ mapping, err := mapper.RESTMapping(gk, gvk.Version)
+ if err != nil {
+ log.Debugf("unable to fetch resource for %s (%v), continuing", gvk, err)
+ return 50, nil
+ }
+
+ if mapping.Scope.Name() == meta.RESTScopeNameRoot {
+ // Place global before namespaced
+ return 20, nil
+ } else if containsPodSpec(disco, gvk) {
+ // (Potentially) starts a pod, so place last
+ return 100, nil
+ } else {
+ // Everything else
+ return 50, nil
+ }
+}
+
+// DependencyOrder is a `sort.Interface` that *best-effort* sorts the
+// objects so that known dependencies appear earlier in the list. The
+// idea is to prevent *some* of the "crash-restart" loops when
+// creating inter-dependent resources.
+func DependencyOrder(disco discovery.OpenAPISchemaInterface, mapper meta.RESTMapper, list []*unstructured.Unstructured) (sort.Interface, error) {
+ sortKeys := make([]int, len(list))
+ for i, item := range list {
+ var err error
+ sortKeys[i], err = depTier(disco, mapper, item.GetObjectKind())
+ if err != nil {
+ return nil, err
+ }
+ }
+ log.Debugf("sortKeys is %v", sortKeys)
+ return &mappedSort{sortKeys: sortKeys, items: list}, nil
+}
+
+type mappedSort struct {
+ sortKeys []int
+ items []*unstructured.Unstructured
+}
+
+func (l *mappedSort) Len() int { return len(l.items) }
+func (l *mappedSort) Swap(i, j int) {
+ l.sortKeys[i], l.sortKeys[j] = l.sortKeys[j], l.sortKeys[i]
+ l.items[i], l.items[j] = l.items[j], l.items[i]
+}
+func (l *mappedSort) Less(i, j int) bool {
+ if l.sortKeys[i] != l.sortKeys[j] {
+ return l.sortKeys[i] < l.sortKeys[j]
+ }
+ // Fall back to alpha sort, to give persistent order
+ return AlphabeticalOrder(l.items).Less(i, j)
+}
+
+// AlphabeticalOrder is a `sort.Interface` that sorts the
+// objects by namespace/name/kind alphabetical order
+type AlphabeticalOrder []*unstructured.Unstructured
+
+func (l AlphabeticalOrder) Len() int { return len(l) }
+func (l AlphabeticalOrder) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+func (l AlphabeticalOrder) Less(i, j int) bool {
+ a, b := l[i], l[j]
+
+ if a.GetNamespace() != b.GetNamespace() {
+ return a.GetNamespace() < b.GetNamespace()
+ }
+ if a.GetName() != b.GetName() {
+ return a.GetName() < b.GetName()
+ }
+ return a.GetKind() < b.GetKind()
+}
diff --git a/cluster/tools/kartongips/utils/sort_test.go b/cluster/tools/kartongips/utils/sort_test.go
new file mode 100644
index 0000000..784ba13
--- /dev/null
+++ b/cluster/tools/kartongips/utils/sort_test.go
@@ -0,0 +1,167 @@
+// 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 (
+ "path/filepath"
+ "reflect"
+ "sort"
+ "testing"
+
+ openapi_v2 "github.com/googleapis/gnostic/openapiv2"
+ log "github.com/sirupsen/logrus"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/discovery"
+ fakedisco "k8s.io/client-go/discovery/fake"
+ "k8s.io/client-go/restmapper"
+ ktesting "k8s.io/client-go/testing"
+)
+
+type FakeDiscovery struct {
+ fakedisco.FakeDiscovery
+ schemaGetter discovery.OpenAPISchemaInterface
+}
+
+func NewFakeDiscovery(schemaGetter discovery.OpenAPISchemaInterface) *FakeDiscovery {
+ fakePtr := &ktesting.Fake{}
+ return &FakeDiscovery{
+ FakeDiscovery: fakedisco.FakeDiscovery{Fake: fakePtr},
+ schemaGetter: schemaGetter,
+ }
+}
+
+func (c *FakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
+ action := ktesting.ActionImpl{}
+ action.Verb = "get"
+ c.Fake.Invokes(action, nil)
+
+ return c.schemaGetter.OpenAPISchema()
+}
+
+func TestDepSort(t *testing.T) {
+ log.SetLevel(log.DebugLevel)
+
+ disco := NewFakeDiscovery(schemaFromFile{dir: filepath.FromSlash("../testdata")})
+ disco.Resources = []*metav1.APIResourceList{
+ {
+ GroupVersion: "v1",
+ APIResources: []metav1.APIResource{
+ {
+ Name: "configmaps",
+ Kind: "ConfigMap",
+ Namespaced: true,
+ },
+ {
+ Name: "namespaces",
+ Kind: "Namespace",
+ Namespaced: false,
+ },
+ {
+ Name: "replicationcontrollers",
+ Kind: "ReplicationController",
+ Namespaced: true,
+ },
+ },
+ },
+ }
+
+ mapper := restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{{
+ Group: metav1.APIGroup{
+ Name: "",
+ Versions: []metav1.GroupVersionForDiscovery{{
+ GroupVersion: "v1",
+ Version: "v1",
+ }},
+ },
+ VersionedResources: map[string][]metav1.APIResource{
+ "v1": disco.Resources[0].APIResources,
+ },
+ }})
+
+ newObj := func(apiVersion, kind string) *unstructured.Unstructured {
+ return &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": apiVersion,
+ "kind": kind,
+ },
+ }
+ }
+
+ objs := []*unstructured.Unstructured{
+ newObj("v1", "ReplicationController"),
+ newObj("v1", "ConfigMap"),
+ newObj("v1", "Namespace"),
+ newObj("admissionregistration.k8s.io/v1beta1", "MutatingWebhookConfiguration"),
+ newObj("bogus/v1", "UnknownKind"),
+ newObj("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition"),
+ }
+
+ sorter, err := DependencyOrder(disco, mapper, objs)
+ if err != nil {
+ t.Fatalf("DependencyOrder error: %v", err)
+ }
+ sort.Sort(sorter)
+
+ for i, o := range objs {
+ t.Logf("obj[%d] after sort is %v", i, o.GroupVersionKind())
+ }
+
+ if objs[0].GetKind() != "CustomResourceDefinition" {
+ t.Error("CRD should be sorted first")
+ }
+ if objs[1].GetKind() != "Namespace" {
+ t.Error("Namespace should be sorted second")
+ }
+ if objs[4].GetKind() != "ReplicationController" {
+ t.Error("RC should be sorted after non-pod objects")
+ }
+ if objs[5].GetKind() != "MutatingWebhookConfiguration" {
+ t.Error("Webhook should be sorted last")
+ }
+}
+
+func TestAlphaSort(t *testing.T) {
+ newObj := func(ns, name, kind string) *unstructured.Unstructured {
+ o := unstructured.Unstructured{}
+ o.SetNamespace(ns)
+ o.SetName(name)
+ o.SetKind(kind)
+ return &o
+ }
+
+ objs := []*unstructured.Unstructured{
+ newObj("default", "mysvc", "Deployment"),
+ newObj("", "default", "StorageClass"),
+ newObj("", "default", "ClusterRole"),
+ newObj("default", "mydeploy", "Deployment"),
+ newObj("default", "mysvc", "Secret"),
+ }
+
+ expected := []*unstructured.Unstructured{
+ objs[2],
+ objs[1],
+ objs[3],
+ objs[0],
+ objs[4],
+ }
+
+ sort.Sort(AlphabeticalOrder(objs))
+
+ if !reflect.DeepEqual(objs, expected) {
+ t.Errorf("actual != expected: %v != %v", objs, expected)
+ }
+}