From 0e1fdfab5512293a349cfeb97bdaffcd226354b1 Mon Sep 17 00:00:00 2001 From: Angus Lees Date: Mon, 11 Sep 2017 18:42:43 +1000 Subject: [PATCH] Expose basic cluster information to jsonnet This change adds two new jsonnet native functions: - `kubernetesVersion` returns (eg) `[1, 7]` for a K8s 1.7 server. - `kubernetesGroupVersionSupported` returns a bool indicating whether a given GroupVersion is supported by the current server Note the latter does not provide a way to iterate over all supported GroupVersions, for privacy. --- cmd/apply.go | 2 +- cmd/delete.go | 2 +- cmd/diff.go | 2 +- cmd/root.go | 11 +++--- cmd/show.go | 7 +++- cmd/update.go | 2 +- cmd/validate.go | 2 +- integration/show_test.go | 76 +++++++++++++++++++++++++++++++++++++++ lib/kubecfg.libsonnet | 10 ++++++ lib/kubecfg_test.jsonnet | 3 ++ template/expander.go | 9 +++-- utils/nativefuncs.go | 41 +++++++++++++++++++-- utils/nativefuncs_test.go | 30 +++++++++++++--- 13 files changed, 177 insertions(+), 20 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index ab0065b3..777c36d3 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -100,7 +100,7 @@ var applyCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, args, c.Discovery) if err != nil { return err } diff --git a/cmd/delete.go b/cmd/delete.go index 5ebe5ba3..0f9bc876 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -55,7 +55,7 @@ var deleteCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, args, c.Discovery) if err != nil { return err } diff --git a/cmd/diff.go b/cmd/diff.go index 887631a0..0032f27a 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -53,7 +53,7 @@ var diffCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, args, c.Discovery) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 7b8c2036..a4f0b2fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,11 +25,10 @@ import ( "path/filepath" "strings" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" @@ -170,7 +169,7 @@ func (f *logFormatter) Format(e *log.Entry) ([]byte, error) { return buf.Bytes(), nil } -func newExpander(cmd *cobra.Command) (*template.Expander, error) { +func newExpander(cmd *cobra.Command, disco discovery.DiscoveryInterface) (*template.Expander, error) { flags := cmd.Flags() spec := template.Expander{} var err error @@ -211,6 +210,8 @@ func newExpander(cmd *cobra.Command) (*template.Expander, error) { return nil, err } + spec.Discovery = disco + return &spec, nil } @@ -273,7 +274,7 @@ func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) { // the user passes a list of files, we will expand all templates in those files, // while if a user passes an environment name, we will expand all component // files using that environment. -func expandEnvCmdObjs(cmd *cobra.Command, args []string) ([]*unstructured.Unstructured, error) { +func expandEnvCmdObjs(cmd *cobra.Command, args []string, disco discovery.DiscoveryInterface) ([]*unstructured.Unstructured, error) { env, fileNames, err := parseEnvCmd(cmd, args) if err != nil { return nil, err @@ -284,7 +285,7 @@ func expandEnvCmdObjs(cmd *cobra.Command, args []string) ([]*unstructured.Unstru return nil, err } - expander, err := newExpander(cmd) + expander, err := newExpander(cmd, disco) if err != nil { return nil, err } diff --git a/cmd/show.go b/cmd/show.go index 9e4ac5c5..778f8df6 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -45,7 +45,12 @@ var showCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + _, disco, err := restClientPool(cmd) + if err != nil { + return err + } + + objs, err := expandEnvCmdObjs(cmd, args, disco) if err != nil { return err } diff --git a/cmd/update.go b/cmd/update.go index ffe4514c..4714a466 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -81,7 +81,7 @@ local configuration. Accepts JSON, YAML, or Jsonnet.`, return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, args, c.Discovery) if err != nil { return err } diff --git a/cmd/validate.go b/cmd/validate.go index f05f75e9..970cec68 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -39,7 +39,7 @@ var validateCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, args, c.Discovery) if err != nil { return err } diff --git a/integration/show_test.go b/integration/show_test.go index e9ee7ef0..b9bde79c 100644 --- a/integration/show_test.go +++ b/integration/show_test.go @@ -3,10 +3,14 @@ package integration import ( + "encoding/json" "io/ioutil" "os" "os/exec" "path/filepath" + "strconv" + + "k8s.io/client-go/discovery" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -111,4 +115,76 @@ object: {"foo": "bar"} }) }) }) + + Context("against current cluster", func() { + var disco discovery.DiscoveryInterface + var jsonout interface{} + + BeforeEach(func() { + var err error + disco, err = discovery.NewDiscoveryClientForConfig(clusterConfigOrDie()) + Expect(err).NotTo(HaveOccurred()) + + args = append(args, + "-J", filepath.FromSlash("../lib"), + "-o", "json", + ) + }) + + JustBeforeEach(func() { + err := json.Unmarshal(output, &jsonout) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("kubernetesVersion()", func() { + BeforeEach(func() { + input = ` +local kubecfg = import "kubecfg.libsonnet"; +{ + apiVersion: "test", + kind: "Result", + result: kubecfg.kubernetesVersion(), +}` + }) + It("should find current cluster version", func() { + info, err := disco.ServerVersion() + Expect(err).NotTo(HaveOccurred()) + major, err := strconv.ParseFloat(info.Major, 64) + Expect(err).NotTo(HaveOccurred()) + minor, err := strconv.ParseFloat(info.Minor, 64) + Expect(err).NotTo(HaveOccurred()) + + result := jsonout.(map[string]interface{})["result"] + Expect(result).To(HaveLen(2)) + x := result.([]interface{}) + Expect(x[0]).To(Equal(major)) + Expect(x[1]).To(Equal(minor)) + }) + }) + + Context("kubernetesGroupVersionSupported", func() { + BeforeEach(func() { + input = ` +local kubecfg = import "kubecfg.libsonnet"; +local res = { + [gv]: kubecfg.kubernetesGroupVersionSupported(gv) for gv in + ["", "v1", "authentication.k8s.io/v1", "bogus/v42"] +}; +{apiVersion: "test", kind: "Result", result: res} +` + }) + + It("should query for supported GroupVersions", func() { + result := jsonout.(map[string]interface{})["result"] + + Expect(result).To(HaveLen(4)) + x := result.(map[string]interface{}) + Expect(x[""]).To(BeFalse()) + Expect(x["v1"]).To(BeTrue()) + Expect(x["authentication.k8s.io/v1"]).To(BeTrue()) + Expect(x["bogus/v42"]).To(BeFalse()) + }) + + }) + }) }) diff --git a/lib/kubecfg.libsonnet b/lib/kubecfg.libsonnet index 9215a753..1f7b395e 100644 --- a/lib/kubecfg.libsonnet +++ b/lib/kubecfg.libsonnet @@ -65,4 +65,14 @@ // to refer to submatches. Regex is as implemented in golang regexp // package (python-ish). regexSubst:: std.native("regexSubst"), + + // kubernetesVersion(): Return [major, minor] representing the + // Kubernetes server version. + kubernetesVersion:: std.native("kubernetesVersion"), + + // kubernetesGroupVersionSupported(gv): Return true iff the + // Kubernetes server advertises support for the specified API + // "group/version" (eg: "storage.k8s.io/v1beta1"). Legacy core is + // "v1" (and hopefully always supported). + kubernetesGroupVersionSupported:: std.native("kubernetesGroupVersionSupported"), } diff --git a/lib/kubecfg_test.jsonnet b/lib/kubecfg_test.jsonnet index 1e288153..f97294a5 100644 --- a/lib/kubecfg_test.jsonnet +++ b/lib/kubecfg_test.jsonnet @@ -65,6 +65,9 @@ assert r == "f\\[o" : "got " + r; local r = kubecfg.regexSubst("e", "tree", "oll"); assert r == "trolloll" : "got " + r; +// NB: kubernetes* functions are tested via integration tests, since +// they require access to a k8s server. + // Kubecfg wants to see something that looks like a k8s object { apiVersion: "test", diff --git a/template/expander.go b/template/expander.go index d414db8c..49890604 100644 --- a/template/expander.go +++ b/template/expander.go @@ -6,11 +6,12 @@ import ( "os" "strings" + log "github.com/sirupsen/logrus" + jsonnet "github.com/strickyak/jsonnet_cgo" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/discovery" "github.com/ksonnet/kubecfg/utils" - log "github.com/sirupsen/logrus" - jsonnet "github.com/strickyak/jsonnet_cgo" ) type Expander struct { @@ -23,6 +24,8 @@ type Expander struct { Resolver string FailAction string + + Discovery discovery.DiscoveryInterface } func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) { @@ -116,7 +119,7 @@ func (spec *Expander) jsonnetVM() (*jsonnet.VM, error) { if err != nil { return nil, err } - utils.RegisterNativeFuncs(vm, resolver) + utils.RegisterNativeFuncs(vm, resolver, spec.Discovery) return vm, nil } diff --git a/utils/nativefuncs.go b/utils/nativefuncs.go index 30fe7cc8..1b770430 100644 --- a/utils/nativefuncs.go +++ b/utils/nativefuncs.go @@ -20,12 +20,13 @@ import ( "encoding/json" "io" "regexp" + "strconv" "strings" goyaml "github.com/ghodss/yaml" - jsonnet "github.com/strickyak/jsonnet_cgo" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/discovery" ) func resolveImage(resolver Resolver, image string) (string, error) { @@ -42,7 +43,7 @@ func resolveImage(resolver Resolver, image string) (string, error) { } // RegisterNativeFuncs adds kubecfg's native jsonnet functions to provided VM -func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver) { +func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver, disco discovery.DiscoveryInterface) { // NB: libjsonnet native functions can only pass primitive // types, so some functions json-encode the arg. These // "*FromJson" functions will be replaced by regular native @@ -105,4 +106,40 @@ func RegisterNativeFuncs(vm *jsonnet.VM, resolver Resolver) { } return r.ReplaceAllString(src, repl), nil }) + + vm.NativeCallback("kubernetesVersion", []string{}, func() ([]interface{}, error) { + info, err := disco.ServerVersion() + if err != nil { + return nil, err + } + + // kubernetes/pkg/version/base.go says: + // gitMajor: major version, always numeric + // gitMinor: minor version, numeric possibly followed by "+" + major, err := strconv.Atoi(info.Major) + if err != nil { + return nil, err + } + minor, err := strconv.Atoi(strings.TrimSuffix(info.Minor, "+")) + if err != nil { + return nil, err + } + return []interface{}{major, minor}, nil + }) + + vm.NativeCallback("kubernetesGroupVersionSupported", []string{"gv"}, func(gv string) (bool, error) { + groups, err := disco.ServerGroups() + if err != nil { + return false, err + } + + for _, g := range groups.Groups { + for _, v := range g.Versions { + if v.GroupVersion == gv { + return true, nil + } + } + } + return false, nil + }) } diff --git a/utils/nativefuncs_test.go b/utils/nativefuncs_test.go index 8e6d7a97..1f3d04c0 100644 --- a/utils/nativefuncs_test.go +++ b/utils/nativefuncs_test.go @@ -19,6 +19,9 @@ import ( "testing" jsonnet "github.com/strickyak/jsonnet_cgo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakedisco "k8s.io/client-go/discovery/fake" + ktesting "k8s.io/client-go/testing" ) // check there is no err, and a == b. @@ -30,10 +33,29 @@ func check(t *testing.T, err error, actual, expected string) { } } +func registerNativeFuncs(vm *jsonnet.VM) { + fake := &ktesting.Fake{ + Resources: []*metav1.APIResourceList{ + { + GroupVersion: "tests/v1alpha1", + APIResources: []metav1.APIResource{ + { + Name: "tests", + Kind: "Test", + }, + }, + }, + }, + } + disco := &fakedisco.FakeDiscovery{Fake: fake} + + RegisterNativeFuncs(vm, NewIdentityResolver(), disco) +} + func TestParseJson(t *testing.T) { vm := jsonnet.Make() defer vm.Destroy() - RegisterNativeFuncs(vm, NewIdentityResolver()) + registerNativeFuncs(vm) _, err := vm.EvaluateSnippet("failtest", `std.native("parseJson")("barf{")`) if err == nil { @@ -52,7 +74,7 @@ func TestParseJson(t *testing.T) { func TestParseYaml(t *testing.T) { vm := jsonnet.Make() defer vm.Destroy() - RegisterNativeFuncs(vm, NewIdentityResolver()) + registerNativeFuncs(vm) _, err := vm.EvaluateSnippet("failtest", `std.native("parseYaml")("[barf")`) if err == nil { @@ -76,7 +98,7 @@ func TestParseYaml(t *testing.T) { func TestRegexMatch(t *testing.T) { vm := jsonnet.Make() defer vm.Destroy() - RegisterNativeFuncs(vm, NewIdentityResolver()) + registerNativeFuncs(vm) _, err := vm.EvaluateSnippet("failtest", `std.native("regexMatch")("[f", "foo")`) if err == nil { @@ -93,7 +115,7 @@ func TestRegexMatch(t *testing.T) { func TestRegexSubst(t *testing.T) { vm := jsonnet.Make() defer vm.Destroy() - RegisterNativeFuncs(vm, NewIdentityResolver()) + registerNativeFuncs(vm) _, err := vm.EvaluateSnippet("failtest", `std.native("regexSubst")("[f",s "foo", "bar")`) if err == nil {