From d564d4bf2d6c6a9781439db231c4aaa1d7a916f8 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Tue, 22 Oct 2019 03:00:26 +0000 Subject: [PATCH 1/9] first lookup template function implementation Signed-off-by: raffaelespazzoli --- pkg/engine/funcs.go | 1 + pkg/engine/lookup_func.go | 118 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 pkg/engine/lookup_func.go diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index dac105e74..eb4a398f4 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -53,6 +53,7 @@ func funcMap() template.FuncMap { "fromYaml": fromYAML, "toJson": toJSON, "fromJson": fromJSON, + "lookup" : lookup, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go new file mode 100644 index 000000000..fe127d8f3 --- /dev/null +++ b/pkg/engine/lookup_func.go @@ -0,0 +1,118 @@ +package engine + +import ( + "flag" + "log" + "os" + "path/filepath" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +var config *rest.Config +var addLookupFunction = false + +func init() { + // try the out-cluster config, this will default to the in-cluster config is not successful + var kubeconfig *string + if home := homeDir(); home != "" { + kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } else { + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + } + flag.Parse() + + // use the current context in kubeconfig + config1, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) + if err == nil { + addLookupFunction = true + config = config1 + } + return +} + +func homeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.Getenv("USERPROFILE") // windows +} + +func lookup(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { + var client dynamic.ResourceInterface + c, err := getDynamicClientOnKind(apiversion, resource) + if err != nil { + return map[string]interface{}{}, err + } + if namespace != "" { + client = c.Namespace(namespace) + } else { + client = c + } + if name != "" { + //this will return a single object + obj, err := client.Get(name, metav1.GetOptions{}) + if err != nil { + return map[string]interface{}{}, err + } + return obj.Object, nil + } + //this will return a list + obj, err := client.List(metav1.ListOptions{}) + if err != nil { + return map[string]interface{}{}, err + } + return obj.Object, nil + +} + +// GetDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. +func getDynamicClientOnKind(apiversion string, kind string) (dynamic.NamespaceableResourceInterface, error) { + gvk := schema.FromAPIVersionAndKind(apiversion, kind) + apiRes, err := getAPIReourceForGVK(gvk) + if err != nil { + log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) + return nil, err + } + gvr := schema.GroupVersionResource{ + Group: apiRes.Group, + Version: apiRes.Version, + Resource: apiRes.Name, + } + intf, err := dynamic.NewForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to get dynamic client %s", err) + return nil, err + } + res := intf.Resource(gvr) + return res, nil +} + +func getAPIReourceForGVK(gvk schema.GroupVersionKind) (metav1.APIResource, error) { + res := metav1.APIResource{} + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to create discovery client %s", err) + return res, err + } + resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) + if err != nil { + log.Printf("[ERROR] unable to retrieve resouce list for: %s , error: %s", gvk.GroupVersion().String(), err) + return res, err + } + for _, resource := range resList.APIResources { + if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { + res = resource + res.Group = gvk.Group + res.Version = gvk.Version + break + } + } + return res, nil +} From 30d245a0e30804b154574a90a23e697ab3724dca Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Wed, 23 Oct 2019 06:33:27 -0400 Subject: [PATCH 2/9] added check on namespaced resource Signed-off-by: raffaelespazzoli --- pkg/engine/lookup_func.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index fe127d8f3..fd3768292 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -46,11 +46,11 @@ func homeDir() string { func lookup(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { var client dynamic.ResourceInterface - c, err := getDynamicClientOnKind(apiversion, resource) + c, namespaced, err := getDynamicClientOnKind(apiversion, resource) if err != nil { return map[string]interface{}{}, err } - if namespace != "" { + if namespaced && namespace != "" { client = c.Namespace(namespace) } else { client = c @@ -73,12 +73,12 @@ func lookup(apiversion string, resource string, namespace string, name string) ( } // GetDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. -func getDynamicClientOnKind(apiversion string, kind string) (dynamic.NamespaceableResourceInterface, error) { +func getDynamicClientOnKind(apiversion string, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { gvk := schema.FromAPIVersionAndKind(apiversion, kind) apiRes, err := getAPIReourceForGVK(gvk) if err != nil { log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) - return nil, err + return nil, false, err } gvr := schema.GroupVersionResource{ Group: apiRes.Group, @@ -88,10 +88,10 @@ func getDynamicClientOnKind(apiversion string, kind string) (dynamic.Namespaceab intf, err := dynamic.NewForConfig(config) if err != nil { log.Printf("[ERROR] unable to get dynamic client %s", err) - return nil, err + return nil, false, err } res := intf.Resource(gvr) - return res, nil + return res, apiRes.Namespaced, nil } func getAPIReourceForGVK(gvk schema.GroupVersionKind) (metav1.APIResource, error) { From 02ce01b2415fe270a760c984f2e0158b18476573 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Wed, 23 Oct 2019 07:51:53 -0400 Subject: [PATCH 3/9] fixed circle ci issues Signed-off-by: raffaelespazzoli --- pkg/engine/funcs.go | 2 +- pkg/engine/lookup_func.go | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index eb4a398f4..6ff10ea72 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -53,7 +53,7 @@ func funcMap() template.FuncMap { "fromYaml": fromYAML, "toJson": toJSON, "fromJson": fromJSON, - "lookup" : lookup, + "lookup": lookup, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index fd3768292..75b822f86 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -16,7 +16,6 @@ import ( ) var config *rest.Config -var addLookupFunction = false func init() { // try the out-cluster config, this will default to the in-cluster config is not successful @@ -31,10 +30,8 @@ func init() { // use the current context in kubeconfig config1, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) if err == nil { - addLookupFunction = true config = config1 } - return } func homeDir() string { @@ -103,7 +100,7 @@ func getAPIReourceForGVK(gvk schema.GroupVersionKind) (metav1.APIResource, error } resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { - log.Printf("[ERROR] unable to retrieve resouce list for: %s , error: %s", gvk.GroupVersion().String(), err) + log.Printf("[ERROR] unable to retrieve resource list for: %s , error: %s", gvk.GroupVersion().String(), err) return res, err } for _, resource := range resList.APIResources { From b3495c73530af98d95783d371dc8a577d1c5c65f Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Wed, 23 Oct 2019 08:13:02 -0400 Subject: [PATCH 4/9] added license header Signed-off-by: raffaelespazzoli --- pkg/engine/lookup_func.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 75b822f86..f8a47ac38 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -1,3 +1,19 @@ +/* +Copyright The Helm 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 engine import ( From e98cd621f0c16cf1ee3a53e8324e18bb0f0cc297 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Mon, 4 Nov 2019 15:24:52 -0500 Subject: [PATCH 5/9] added rest client passed with action configuration Signed-off-by: raffaelespazzoli --- pkg/action/install.go | 7 +++- pkg/engine/engine.go | 15 +++++++ pkg/engine/funcs.go | 1 - pkg/engine/lookup_func.go | 86 +++++++++++++++------------------------ 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index dc5941810..8f865ddf9 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -436,7 +436,12 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values } } - files, err := engine.Render(ch, values) + rest, err := c.RESTClientGetter.ToRESTConfig() + if err != nil { + return hs, b, "", err + } + + files, err := engine.RenderWithClient(ch, values, rest) if err != nil { return hs, b, "", err } diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 5a7d54993..c4d5ee5a7 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -27,6 +27,7 @@ import ( "text/template" "github.com/pkg/errors" + "k8s.io/client-go/rest" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -39,6 +40,8 @@ type Engine struct { Strict bool // In LintMode, some 'required' template values may be missing, so don't fail LintMode bool + // the rest config to connect to te kubernetes api + config *rest.Config } // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. @@ -71,6 +74,15 @@ func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, erro return new(Engine).Render(chrt, values) } +// RenderWithClient takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. This engine is client aware and so can have template +// functions that interact with the client +func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) { + return Engine{ + config: config, + }.Render(chrt, values) +} + // renderable is an object that can be rendered. type renderable struct { // tpl is the current template. @@ -157,6 +169,9 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render } return val, nil } + if e.config != nil { + funcMap["lookup"] = NewLookupFunction(e.config) + } t.Funcs(funcMap) } diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 6ff10ea72..dac105e74 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -53,7 +53,6 @@ func funcMap() template.FuncMap { "fromYaml": fromYAML, "toJson": toJSON, "fromJson": fromJSON, - "lookup": lookup, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index f8a47ac38..7c5dc78f1 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -17,10 +17,7 @@ limitations under the License. package engine import ( - "flag" "log" - "os" - "path/filepath" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,67 +25,50 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) -var config *rest.Config +type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) -func init() { - // try the out-cluster config, this will default to the in-cluster config is not successful - var kubeconfig *string - if home := homeDir(); home != "" { - kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") - } else { - kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") - } - flag.Parse() - - // use the current context in kubeconfig - config1, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) - if err == nil { - config = config1 - } -} - -func homeDir() string { - if h := os.Getenv("HOME"); h != "" { - return h - } - return os.Getenv("USERPROFILE") // windows -} - -func lookup(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { - var client dynamic.ResourceInterface - c, namespaced, err := getDynamicClientOnKind(apiversion, resource) - if err != nil { - return map[string]interface{}{}, err - } - if namespaced && namespace != "" { - client = c.Namespace(namespace) - } else { - client = c - } - if name != "" { - //this will return a single object - obj, err := client.Get(name, metav1.GetOptions{}) +func NewLookupFunction(config *rest.Config) lookupFunc { + return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { + var client dynamic.ResourceInterface + c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config) + if err != nil { + return map[string]interface{}{}, err + } + if namespaced && namespace != "" { + client = c.Namespace(namespace) + } else { + client = c + } + if name != "" { + //this will return a single object + obj, err := client.Get(name, metav1.GetOptions{}) + if err != nil { + return map[string]interface{}{}, err + } + return obj.Object, nil + } + //this will return a list + obj, err := client.List(metav1.ListOptions{}) if err != nil { return map[string]interface{}{}, err } return obj.Object, nil } - //this will return a list - obj, err := client.List(metav1.ListOptions{}) - if err != nil { - return map[string]interface{}{}, err - } - return obj.Object, nil - } +// func homeDir() string { +// if h := os.Getenv("HOME"); h != "" { +// return h +// } +// return os.Getenv("USERPROFILE") // windows +// } + // GetDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. -func getDynamicClientOnKind(apiversion string, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { +func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { gvk := schema.FromAPIVersionAndKind(apiversion, kind) - apiRes, err := getAPIReourceForGVK(gvk) + apiRes, err := getAPIReourceForGVK(gvk, config) if err != nil { log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) return nil, false, err @@ -107,7 +87,7 @@ func getDynamicClientOnKind(apiversion string, kind string) (dynamic.Namespaceab return res, apiRes.Namespaced, nil } -func getAPIReourceForGVK(gvk schema.GroupVersionKind) (metav1.APIResource, error) { +func getAPIReourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (metav1.APIResource, error) { res := metav1.APIResource{} discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { From 8e088fc4a2a616354872b21d9ceb84e9d55a40de Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Mon, 4 Nov 2019 15:39:09 -0500 Subject: [PATCH 6/9] fixed test issue Signed-off-by: raffaelespazzoli --- pkg/action/install.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 8f865ddf9..1a1ebeeac 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -436,13 +436,21 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values } } - rest, err := c.RESTClientGetter.ToRESTConfig() - if err != nil { - return hs, b, "", err + var files map[string]string + var err2 error + + if c.RESTClientGetter != nil { + rest, err := c.RESTClientGetter.ToRESTConfig() + if err != nil { + files, err2 = engine.Render(ch, values) + } else { + files, err2 = engine.RenderWithClient(ch, values, rest) + } + } else { + files, err2 = engine.Render(ch, values) } - files, err := engine.RenderWithClient(ch, values, rest) - if err != nil { + if err2 != nil { return hs, b, "", err } From 0bb4eaace77687e8226113df6cafef5791940999 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Wed, 11 Dec 2019 20:53:29 -0500 Subject: [PATCH 7/9] addressing some feedback from @thomastaylor312 Signed-off-by: raffaelespazzoli --- pkg/engine/lookup_func.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 7c5dc78f1..747ebee0a 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -47,25 +47,18 @@ func NewLookupFunction(config *rest.Config) lookupFunc { if err != nil { return map[string]interface{}{}, err } - return obj.Object, nil + return obj.UnstructuredContent(), nil } //this will return a list obj, err := client.List(metav1.ListOptions{}) if err != nil { return map[string]interface{}{}, err } - return obj.Object, nil + return obj.UnstructuredContent(), nil } } -// func homeDir() string { -// if h := os.Getenv("HOME"); h != "" { -// return h -// } -// return os.Getenv("USERPROFILE") // windows -// } - -// GetDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. +// getDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { gvk := schema.FromAPIVersionAndKind(apiversion, kind) apiRes, err := getAPIReourceForGVK(gvk, config) @@ -100,6 +93,7 @@ func getAPIReourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (meta return res, err } for _, resource := range resList.APIResources { + //if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now. if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { res = resource res.Group = gvk.Group From a62ba049624d3212619005bc87fec410a248ca22 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Tue, 17 Dec 2019 08:31:02 -0500 Subject: [PATCH 8/9] additional fixes based on @thomastaylor312 comment Signed-off-by: raffaelespazzoli --- pkg/action/install.go | 5 ++--- pkg/engine/lookup_func.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 1a1ebeeac..e185e581b 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -442,10 +442,9 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values if c.RESTClientGetter != nil { rest, err := c.RESTClientGetter.ToRESTConfig() if err != nil { - files, err2 = engine.Render(ch, values) - } else { - files, err2 = engine.RenderWithClient(ch, values, rest) + return hs, b, "", err } + files, err2 = engine.RenderWithClient(ch, values, rest) } else { files, err2 = engine.Render(ch, values) } diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 747ebee0a..c70dda424 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -20,6 +20,7 @@ import ( "log" "strings" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" @@ -64,7 +65,7 @@ func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) apiRes, err := getAPIReourceForGVK(gvk, config) if err != nil { log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) - return nil, false, err + return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s , error %s", gvk.String()) } gvr := schema.GroupVersionResource{ Group: apiRes.Group, From fa643cfa31af72f06b6d041c310a29bed18f0ac4 Mon Sep 17 00:00:00 2001 From: raffaelespazzoli Date: Tue, 17 Dec 2019 08:37:17 -0500 Subject: [PATCH 9/9] fixed golint Signed-off-by: raffaelespazzoli --- pkg/engine/lookup_func.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index c70dda424..14f2351b4 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -65,7 +65,7 @@ func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) apiRes, err := getAPIReourceForGVK(gvk, config) if err != nil { log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) - return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s , error %s", gvk.String()) + return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s", gvk.String()) } gvr := schema.GroupVersionResource{ Group: apiRes.Group,