Skip to content

Commit

Permalink
[feature] Allow external FuncMaps (#1708)
Browse files Browse the repository at this point in the history
  • Loading branch information
angrycub committed Apr 4, 2023
1 parent 3d59f1b commit b26991a
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 13 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@

---
# Consul Template

[![build](https://github.com/hashicorp/consul-template/actions/workflows/build.yml/badge.svg)](https://github.com/hashicorp/consul-template/actions/workflows/build.yml)
Expand Down
44 changes: 32 additions & 12 deletions config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"regexp"
"strconv"
"strings"
"text/template"
"time"

"golang.org/x/exp/maps"
)

const (
Expand Down Expand Up @@ -104,6 +107,12 @@ type TemplateConfig struct {
LeftDelim *string `mapstructure:"left_delimiter"`
RightDelim *string `mapstructure:"right_delimiter"`

// ExtFuncMap is a map of external functions that this template is
// permitted to run. Allows users to add functions to the library
// and selectively opaque existing ones. Omitted from json output
// to prevent errors when the configuration is marshalled for printing.
ExtFuncMap template.FuncMap `json:"-"`

// FunctionDenylist is a list of functions that this template is not
// permitted to run.
FunctionDenylist []string `mapstructure:"function_denylist"`
Expand All @@ -124,8 +133,9 @@ type TemplateConfig struct {
// default values.
func DefaultTemplateConfig() *TemplateConfig {
return &TemplateConfig{
Exec: DefaultExecConfig(),
Wait: DefaultWaitConfig(),
Exec: DefaultExecConfig(),
Wait: DefaultWaitConfig(),
ExtFuncMap: make(template.FuncMap),
}
}

Expand Down Expand Up @@ -174,13 +184,13 @@ func (c *TemplateConfig) Copy() *TemplateConfig {
o.LeftDelim = c.LeftDelim
o.RightDelim = c.RightDelim

for _, fun := range c.FunctionDenylist {
o.FunctionDenylist = append(o.FunctionDenylist, fun)
if c.ExtFuncMap != nil {
o.ExtFuncMap = make(template.FuncMap, len(c.ExtFuncMap))
maps.Copy(o.ExtFuncMap, c.ExtFuncMap)
}

for _, fun := range c.FunctionDenylistDeprecated {
o.FunctionDenylistDeprecated = append(o.FunctionDenylistDeprecated, fun)
}
o.FunctionDenylist = append(o.FunctionDenylist, c.FunctionDenylist...)
o.FunctionDenylistDeprecated = append(o.FunctionDenylistDeprecated, c.FunctionDenylistDeprecated...)

o.SandboxPath = c.SandboxPath

Expand Down Expand Up @@ -276,13 +286,17 @@ func (c *TemplateConfig) Merge(o *TemplateConfig) *TemplateConfig {
r.RightDelim = o.RightDelim
}

for _, fun := range o.FunctionDenylist {
r.FunctionDenylist = append(r.FunctionDenylist, fun)
if o.ExtFuncMap != nil {
if r.ExtFuncMap == nil {
r.ExtFuncMap = make(template.FuncMap, len(o.ExtFuncMap))
}
for key, fun := range o.ExtFuncMap {
r.ExtFuncMap[key] = fun
}
}

for _, fun := range o.FunctionDenylistDeprecated {
r.FunctionDenylistDeprecated = append(r.FunctionDenylistDeprecated, fun)
}
r.FunctionDenylist = append(r.FunctionDenylist, o.FunctionDenylist...)
r.FunctionDenylistDeprecated = append(r.FunctionDenylistDeprecated, o.FunctionDenylistDeprecated...)

if o.SandboxPath != nil {
r.SandboxPath = o.SandboxPath
Expand Down Expand Up @@ -380,6 +394,10 @@ func (c *TemplateConfig) Finalize() {
c.SandboxPath = String("")
}

if c.ExtFuncMap == nil {
c.ExtFuncMap = make(template.FuncMap, 0)
}

if c.FunctionDenylist == nil && c.FunctionDenylistDeprecated == nil {
c.FunctionDenylist = []string{}
c.FunctionDenylistDeprecated = []string{}
Expand Down Expand Up @@ -409,6 +427,7 @@ func (c *TemplateConfig) GoString() string {
"Wait:%#v, "+
"LeftDelim:%s, "+
"RightDelim:%s, "+
"ExtFuncMap:%s, "+
"FunctionDenylist:%s, "+
"SandboxPath:%s"+
"}",
Expand All @@ -426,6 +445,7 @@ func (c *TemplateConfig) GoString() string {
c.Wait,
StringGoString(c.LeftDelim),
StringGoString(c.RightDelim),
maps.Keys(c.ExtFuncMap),
combineLists(c.FunctionDenylist, c.FunctionDenylistDeprecated),
StringGoString(c.SandboxPath),
)
Expand Down
2 changes: 2 additions & 0 deletions config/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"testing"
"text/template"
"time"
)

Expand Down Expand Up @@ -451,6 +452,7 @@ func TestTemplateConfig_Finalize(t *testing.T) {
},
LeftDelim: String(""),
RightDelim: String(""),
ExtFuncMap: template.FuncMap{},
FunctionDenylist: []string{},
FunctionDenylistDeprecated: []string{},
SandboxPath: String(""),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
require (
github.com/Masterminds/sprig/v3 v3.2.3
github.com/hashicorp/vault/api/auth/kubernetes v0.3.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/text v0.7.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
87 changes: 87 additions & 0 deletions manager/example_extfuncmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package manager_test

import (
"fmt"
"io"
"log"
"os"
"path"
"text/template"

"github.com/hashicorp/consul-template/config"
"github.com/hashicorp/consul-template/manager"
)

// ExampleCustomFuncMap demonstrates a minimum [consul-template/manager]
// configuration and supply custom templates to consul-template's internal
// [text/template] based renderer.
//
// It is not comprehensive and does not demonstrate the dependencies, polling,
// and rerendering features available in the manager
func Example_customFuncMap() {

// Consul-template uses the standard logger, which needs to be silenced
// in this example
log.SetOutput(io.Discard)

// Create a simple function to add to CT
greet := func(n string) string { return "Hello, " + n + "!" }
fm := template.FuncMap{"greet": greet}

// Define a template that uses the new function
tmpl := `{{greet "World"}}`

// Make a destination path to write the rendered template to
outPath := path.Join(os.TempDir(), "tmpl.out") // Use the temp dir
defer os.RemoveAll(outPath) // Defer the file cleanup

// Create a TemplateConfig
tc1 := config.DefaultTemplateConfig() // Start with the default configuration
tc1.ExtFuncMap = fm // Use the ExtFuncMap to add greet
tc1.Contents = &tmpl // Add the template to the configuration
tc1.Destination = &outPath // Set the output destination
tc1.Finalize() // Finalize the template config

// Create the (consul-template) Config
cfg := config.DefaultConfig() // Start with default configuration
cfg.Once = true // Perform a one-shot render
cfg.Templates = &config.TemplateConfigs{tc1} // Add the template created earlier
cfg.Finalize() // Finalize the consul-template configuration

// Instantiate a runner with the config and with `dry` == false
runner, err := manager.NewRunner(cfg, false)
if err != nil {
fmt.Printf("[ERROR] %s\n", err.Error())
return
}

go runner.Start() // The runner blocks, so must be started in a goroutine
defer runner.Stop()

select {

// When the runner is successfully done, it will emit a message on DoneCh
case <-runner.DoneCh:
break

// When the runner encounters an error, it will emit an error on ErrCh and
// then return.
case err := <-runner.ErrCh:
fmt.Printf("[ERROR] %s\n", err.Error())
return
}

// Read the rendered template from disk
if b, e := os.ReadFile(outPath); e == nil {
fmt.Println(string(b))
} else {
fmt.Printf("[ERROR] %s\n", err.Error())
return
}

// Output:
// Hello, World!
}
80 changes: 80 additions & 0 deletions manager/example_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package manager_test

import (
"fmt"
"io"
"log"
"os"
"path"

"github.com/hashicorp/consul-template/config"
"github.com/hashicorp/consul-template/manager"
)

// This example demonstrates a minimum configuration to create, configure, and
// use a consul-template/manager from code. It is not comprehensive and does not
// demonstrate the dependencies, polling, and rerendering features available in
// the manager
func Example() {

// Consul-template uses the standard logger, which needs to be silenced
// in this example
log.SetOutput(io.Discard)

// Define a template
tmpl := `{{ "foo\nbar\n" | split "\n" | toJSONPretty }}`

// Make a destination path to write the rendered template to
outPath := path.Join(os.TempDir(), "tmpl.out") // Use the temp dir
defer os.RemoveAll(outPath) // Defer the file cleanup

// Create a TemplateConfig
tCfg := config.DefaultTemplateConfig() // Start with the default configuration
tCfg.Contents = &tmpl // Add the template to the configuration
tCfg.Destination = &outPath // Set the output destination
tCfg.Finalize() // Finalize the template config

// Create the (consul-template) Config
cfg := config.DefaultConfig() // Start with default configuration
cfg.Once = true // Perform a one-shot render
cfg.Templates = &config.TemplateConfigs{tCfg} // Add the template created earlier
cfg.Finalize() // Finalize the consul-template configuration

// Instantiate a runner with the config and with `dry` == false
runner, err := manager.NewRunner(cfg, false)
if err != nil {
fmt.Printf("[ERROR] %s\n", err.Error())
return
}

go runner.Start() // The runner blocks, so must be started in a goroutine
defer runner.Stop()

select {
// When the runner is successfully done, it will emit a message on DoneCh
case <-runner.DoneCh:
break

// When the runner encounters an error, it will emit an error on ErrCh and
// then return.
case err := <-runner.ErrCh:
fmt.Printf("[ERROR] %s\n", err.Error())
return
}

// Read the rendered template from disk
if b, e := os.ReadFile(outPath); e == nil {
fmt.Println(string(b))
} else {
fmt.Printf("[ERROR] %s\n", err.Error())
return
}
// Output:
// [
// "foo",
// "bar"
// ]
}
1 change: 1 addition & 0 deletions manager/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@ func (r *Runner) init(clients *dep.ClientSet) error {
ErrFatal: config.BoolVal(ctmpl.ErrFatal),
LeftDelim: leftDelim,
RightDelim: rightDelim,
ExtFuncMap: ctmpl.ExtFuncMap,
FunctionDenylist: ctmpl.FunctionDenylist,
SandboxPath: config.StringVal(ctmpl.SandboxPath),
Destination: config.StringVal(ctmpl.Destination),
Expand Down

0 comments on commit b26991a

Please sign in to comment.