Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(pkg/engine): introduce RenderWithClientProvider #12617

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 19 additions & 7 deletions pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ 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 the kubernetes api
config *rest.Config
// optional provider of clients to talk to the Kubernetes API
clientProvider *ClientProvider
// EnableDNS tells the engine to allow DNS lookups when rendering templates
EnableDNS bool
}

// New creates a new instance of Engine using the passed in rest config.
func New(config *rest.Config) Engine {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
Copy link
Contributor

@gjenkins8 gjenkins8 Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! IMHO it is great to be able to re-write the existing implementation in terms of this new, more generic, interface

}
}

Expand Down Expand Up @@ -85,10 +86,21 @@ func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, erro

// 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
// functions that interact with the client.
func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
}.Render(chrt, values)
}

// RenderWithClientProvider 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.
// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) {
return Engine{
clientProvider: &clientProvider,
}.Render(chrt, values)
}

Expand Down Expand Up @@ -194,8 +206,8 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render

// If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation.
if !e.LintMode && e.config != nil {
funcMap["lookup"] = NewLookupFunction(e.config)
if !e.LintMode && e.clientProvider != nil {
funcMap["lookup"] = newLookupFunction(*e.clientProvider)
}

// When DNS lookups are not enabled override the sprig function and return
Expand Down
180 changes: 179 additions & 1 deletion pkg/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import (
"testing"
"text/template"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"

"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
Expand Down Expand Up @@ -204,7 +210,7 @@ func TestRenderInternals(t *testing.T) {
}
}

func TestRenderWIthDNS(t *testing.T) {
func TestRenderWithDNS(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Expand Down Expand Up @@ -240,6 +246,178 @@ func TestRenderWIthDNS(t *testing.T) {
}
}

type kindProps struct {
shouldErr error
gvr schema.GroupVersionResource
namespaced bool
}

type testClientProvider struct {
t *testing.T
scheme map[string]kindProps
objects []runtime.Object
}

func (p *testClientProvider) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
props := p.scheme[path.Join(apiVersion, kind)]
if props.shouldErr != nil {
return nil, false, props.shouldErr
}
return fake.NewSimpleDynamicClient(runtime.NewScheme(), p.objects...).Resource(props.gvr), props.namespaced, nil
}

var _ ClientProvider = &testClientProvider{}

// makeUnstructured is a convenience function for single-line creation of Unstructured objects.
func makeUnstructured(apiVersion, kind, name, namespace string) *unstructured.Unstructured {
ret := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"name": name,
},
}}
if namespace != "" {
ret.Object["metadata"].(map[string]interface{})["namespace"] = namespace
}
return ret
}

func TestRenderWithClientProvider(t *testing.T) {
provider := &testClientProvider{
t: t,
scheme: map[string]kindProps{
"v1/Namespace": {
gvr: schema.GroupVersionResource{
Version: "v1",
Resource: "namespaces",
},
},
"v1/Pod": {
gvr: schema.GroupVersionResource{
Version: "v1",
Resource: "pods",
},
namespaced: true,
},
},
objects: []runtime.Object{
makeUnstructured("v1", "Namespace", "default", ""),
makeUnstructured("v1", "Pod", "pod1", "default"),
makeUnstructured("v1", "Pod", "pod2", "ns1"),
makeUnstructured("v1", "Pod", "pod3", "ns1"),
},
}

type testCase struct {
template string
output string
}
cases := map[string]testCase{
"ns-single": {
template: `{{ (lookup "v1" "Namespace" "" "default").metadata.name }}`,
output: "default",
},
"ns-list": {
template: `{{ (lookup "v1" "Namespace" "" "").items | len }}`,
output: "1",
},
"ns-missing": {
template: `{{ (lookup "v1" "Namespace" "" "absent") }}`,
output: "map[]",
},
"pod-single": {
template: `{{ (lookup "v1" "Pod" "default" "pod1").metadata.name }}`,
output: "pod1",
},
"pod-list": {
template: `{{ (lookup "v1" "Pod" "ns1" "").items | len }}`,
output: "2",
},
"pod-all": {
template: `{{ (lookup "v1" "Pod" "" "").items | len }}`,
output: "3",
},
"pod-missing": {
template: `{{ (lookup "v1" "Pod" "" "ns2") }}`,
output: "map[]",
},
}

c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Values: map[string]interface{}{},
}

for name, exp := range cases {
c.Templates = append(c.Templates, &chart.File{
Name: path.Join("templates", name),
Data: []byte(exp.template),
})
}

vals := map[string]interface{}{
"Values": map[string]interface{}{},
}

v, err := chartutil.CoalesceValues(c, vals)
if err != nil {
t.Fatalf("Failed to coalesce values: %s", err)
}

out, err := RenderWithClientProvider(c, v, provider)
if err != nil {
t.Errorf("Failed to render templates: %s", err)
}

for name, want := range cases {
t.Run(name, func(t *testing.T) {
key := path.Join("moby/templates", name)
if out[key] != want.output {
t.Errorf("Expected %q, got %q", want, out[key])
}
})
}
}

func TestRenderWithClientProvider_error(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*chart.File{
{Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)},
},
Values: map[string]interface{}{},
}

vals := map[string]interface{}{
"Values": map[string]interface{}{},
}

v, err := chartutil.CoalesceValues(c, vals)
if err != nil {
t.Fatalf("Failed to coalesce values: %s", err)
}

provider := &testClientProvider{
t: t,
scheme: map[string]kindProps{
"v1/Error": {
shouldErr: fmt.Errorf("kaboom"),
},
},
}
_, err = RenderWithClientProvider(c, v, provider)
if err == nil || !strings.Contains(err.Error(), "kaboom") {
t.Errorf("Expected error from client provider when rendering, got %q", err)
}
}

func TestParallelRenderInternals(t *testing.T) {
// Make sure that we can use one Engine to run parallel template renders.
e := new(Engine)
Expand Down
23 changes: 21 additions & 2 deletions pkg/engine/lookup_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,28 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam
// This function is considered deprecated, and will be renamed in Helm 4. It will no
// longer be a public function.
func NewLookupFunction(config *rest.Config) lookupFunc {
return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) {
return newLookupFunction(clientProviderFromConfig{config: config})
}

type ClientProvider interface {
// GetClientFor returns a dynamic.NamespaceableResourceInterface suitable for interacting with resources
// corresponding to the provided apiVersion and kind, as well as a boolean indicating whether the resources
// are namespaced.
GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error)
}

type clientProviderFromConfig struct {
config *rest.Config
}

func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
return getDynamicClientOnKind(apiVersion, kind, c.config)
}

func newLookupFunction(clientProvider ClientProvider) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]interface{}, error) {
var client dynamic.ResourceInterface
c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config)
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
if err != nil {
return map[string]interface{}{}, err
}
Expand Down