Skip to content

Commit

Permalink
Merge pull request #12617 from porridge/dynamic-client
Browse files Browse the repository at this point in the history
feature(pkg/engine): introduce RenderWithClientProvider
  • Loading branch information
mattfarina committed Jan 9, 2024
2 parents 1ccde5a + a997de1 commit 7fd0804
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 10 deletions.
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,
}
}

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 @@ -220,8 +232,8 @@ func (e Engine) initFunMap(t *template.Template) {

// 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

0 comments on commit 7fd0804

Please sign in to comment.