lukasjarosch / skipper

Inventory based templated configuration library inspired by the kapitan project
https://lukasjarosch.github.io/skipper/
MIT License
11 stars 3 forks source link

Partial Template Support #77

Closed lukasjarosch closed 6 months ago

lukasjarosch commented 6 months ago

Partial Templates allow the user to deduplicate template code by defining a template at a single place and reuse it within different templates.

lukasjarosch commented 6 months ago
andaryjo commented 6 months ago

Great feature which will solve a lot of problems for us! 🚀

What was a bit tricky for me to understand at first is that there is a slight difference between supplying . and .Inventory as context to a template definition. . is the whole context which includes the inventory (and .TargetName) and .Inventory is only the inventory. Depending on what you choose, in your partial template definition you either need to add a .Inventory in before your references or you don't.

But that comes with a catch. In case I would invoke a template definition like this:

{{ template "dummy" .Inventory.commander }}

... I could no longer use the .TargetName reference within that partial template. If you need the target name, you could only provide the whole context to the partial template, which might limit their use cases. For example, in some scenarios a user might want to invoke the same partial template with different contexts so that it ultimately renders different data. Is there maybe a way to keep the .TargetName accessible for partial templates even though it is not in the inventory context?

lukasjarosch commented 6 months ago

Glad that this feature will solve lots of problems for you 😃. And thank you for testing this out ❤️

This is indeed an issue. But unfortunately Go templates only accept one context parameter. I've come up with a solution which might be sufficient.

I have introduced a new template function context which essentially allows you to build a map[string]interface{} on the fly. This allows you to pass in as many arguments as you like, as long as there is an even amount of arguments and the keys of the map are strings.

This helps you in solving the TargetName issue.

{{ template "my_template" context "TargetName" .TargetName "Something" .Inventory.something }}

This will create a map with keys TargetName and Something with the respective values. So in your partial you can then use {{ .TargetName }} again.

Unfortunately I'm not able to automatically inject the targetname as the value is not known when the template functions are defined.

Is this a solution which will work for you?

andaryjo commented 6 months ago

Oh that is a nice solution. That would even allow us to provide multiple parts of the Inventory to a template instead of the whole Inventory (even though I'm not sure yet why one would that haha).

Tested and it works. Thanks for the quick fix.

andaryjo commented 6 months ago

I also tested supplying variables into the template, both "primitives" (like strings you define on the fly) as well as declaring parts of the Inventory as variable and supplying them.

{{ $commander := "cody" }}
{{ $firstElement := (index .Inventory.elements 0 ) }
{{ template "dummy" context "Element $firstElement "Commander" $commander }}

With a template like this:

{{ define "dumy" }}
{{ . }}
{{ end }}

... you are then able to see all of those contexts getting supplied correctly (even though they get rearranged alphabetically which threw me off at first).

However, you are not actually able to access .Element or .Commander. When supplying contexts to a template like this, you seemingly can only access conecxts where their name is the name of a field that is also available to the template that invokes the partial template:

executing "dummy" at <.Commander>: can't evaluate field Commander in type struct { Inventory interface {}; TargetName string }
lukasjarosch commented 6 months ago

Thanks for repeatedly challenging the implementation 👍🏼 Unfortunately I'm not able to reproduce your error.

Given this target

---
target:
  skipper:
    use:
      - network # not used
  elements:
    - this:
        is:
          a:
            nested: map
    - welcome: home

This template:

# main template

{{- $commander := "cody" }}
{{- $firstElement := (index .Inventory.elements 0 ) }}
{{ template "with_data" context "TargetName" .TargetName "Network" .Inventory.network "Commander" $commander "Elements" $firstElement }}

And this partial

{{ define "with_data" }}

# {{ .TargetName }} partial

{{ . }}

{{ .Elements.this.is.a.nested }}

{{ .Commander }}

{{ end }}

Yields the following rendered main.md file:

# main template

# example partial

map[Commander:cody Elements:this:
    is:
        a:
            nested: map
 Network:foo: bar
 TargetName:example]

map

cody

Did I miss anything?

andaryjo commented 6 months ago

I can't reproduce it either. It's possible I just messed up the local skipper vendoring. Everything works now. 🚀