Skip to content

Commit

Permalink
Addition of template functions allow Consul and Time lookups.
Browse files Browse the repository at this point in the history
This commit introduces template functions which allow lookups
against Consul as well as generic time lookups which are run over
the template during execution. A consul client has been added with
config flag in both the render and deploy commands as these may
need to connect to Consul for variable lookups.

Closes #58
  • Loading branch information
jrasell committed May 18, 2018
1 parent 14d10be commit 9502d0f
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 40 deletions.
21 changes: 21 additions & 0 deletions client/consul.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package client

import (
consul "github.com/hashicorp/consul/api"
)

// NewConsulClient is used to create a new client to interact with Consul.
func NewConsulClient(addr string) (*consul.Client, error) {
config := consul.DefaultConfig()

if addr != "" {
config.Address = addr
}

c, err := consul.NewClient(config)
if err != nil {
return nil, err
}

return c, nil
}
8 changes: 7 additions & 1 deletion command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ General Options:
-canary-auto-promote=<seconds>
The time in seconds, after which Levant will auto-promote a canary job
if all canaries within the deployment are healthy.
-consul-address=<addr>
The Consul host and port to use when making Consul KeyValue lookups for
template rendering.
-force-batch
Forces a new instance of the periodic job. A new instance will be created
Expand Down Expand Up @@ -73,13 +77,15 @@ func (c *DeployCommand) Synopsis() string {
func (c *DeployCommand) Run(args []string) int {

var err error
var addr string
config := &structs.Config{}

flags := c.Meta.FlagSet("deploy", FlagSetVars)
flags.Usage = func() { c.UI.Output(c.Help()) }

flags.StringVar(&config.Addr, "address", "", "")
flags.IntVar(&config.Canary, "canary-auto-promote", 0, "")
flags.StringVar(&addr, "consul-address", "", "")
flags.BoolVar(&config.ForceBatch, "force-batch", false, "")
flags.BoolVar(&config.ForceCount, "force-count", false, "")
flags.StringVar(&config.LogLevel, "log-level", "INFO", "")
Expand Down Expand Up @@ -110,7 +116,7 @@ func (c *DeployCommand) Run(args []string) int {
return 1
}

config.Job, err = template.RenderJob(config.TemplateFile, config.VaiableFile, &c.Meta.flagVars)
config.Job, err = template.RenderJob(config.TemplateFile, config.VaiableFile, addr, &c.Meta.flagVars)
if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
Expand Down
4 changes: 2 additions & 2 deletions command/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestDeploy_checkCanaryAutoPromote(t *testing.T) {
}

for i, c := range cases {
job, err := template.RenderJob(c.File, "", &fVars)
job, err := template.RenderJob(c.File, "", "", &fVars)
if err != nil {
t.Fatalf("case %d failed: %v", i, err)
}
Expand Down Expand Up @@ -61,7 +61,7 @@ func TestDeploy_checkForceBatch(t *testing.T) {
}

for i, c := range cases {
job, err := template.RenderJob(c.File, "", &fVars)
job, err := template.RenderJob(c.File, "", "", &fVars)
if err != nil {
t.Fatalf("case %d failed: %v", i, err)
}
Expand Down
9 changes: 7 additions & 2 deletions command/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Arguments:
If no argument is given we look for a single *.nomad file
General Options:
-consul-address=<addr>
The Consul host and port to use when making Consul KeyValue lookups for
template rendering.
-out=<file>
Specify the path to write the rendered template out to, if a file exists at
Expand All @@ -49,13 +53,14 @@ func (c *RenderCommand) Synopsis() string {
// Run triggers a run of the Levant template functions.
func (c *RenderCommand) Run(args []string) int {

var variables, outPath, templateFile string
var addr, variables, outPath, templateFile string
var err error
var tpl *bytes.Buffer

flags := c.Meta.FlagSet("render", FlagSetVars)
flags.Usage = func() { c.UI.Output(c.Help()) }

flags.StringVar(&addr, "consul-address", "", "")
flags.StringVar(&variables, "var-file", "", "")
flags.StringVar(&outPath, "out", "", "")

Expand All @@ -78,7 +83,7 @@ func (c *RenderCommand) Run(args []string) int {
return 1
}

tpl, err = template.RenderTemplate(templateFile, variables, &c.Meta.flagVars)
tpl, err = template.RenderTemplate(templateFile, variables, addr, &c.Meta.flagVars)
if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
Expand Down
119 changes: 119 additions & 0 deletions template/funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package template

import (
"errors"
"text/template"
"time"

consul "github.com/hashicorp/consul/api"
"github.com/rs/zerolog/log"
)

// funcMap builds the template functions and passes the consulClient where this
// is required.
func funcMap(consulClient *consul.Client) template.FuncMap {
return template.FuncMap{
"consulKey": consulKeyFunc(consulClient),
"consulKeyExists": consulKeyExistsFunc(consulClient),
"consulKeyOrDefault": consulKeyOrDefaultFunc(consulClient),
"timeNow": timeNowFunc,
"timeNowUTC": timeNowUTCFunc,
"timeNowTimezone": timeNowTimezoneFunc(),
}
}

func consulKeyFunc(consulClient *consul.Client) func(string) (string, error) {
return func(s string) (string, error) {

if len(s) == 0 {
return "", nil
}

kv, _, err := consulClient.KV().Get(s, nil)
if err != nil {
return "", err
}

if kv == nil {
return "", errors.New("Consul KV not found")
}

v := string(kv.Value[:])
log.Info().Msgf("template/funcs: using Consul KV variable with key %s and value %s",
s, v)

return v, nil
}
}

func consulKeyExistsFunc(consulClient *consul.Client) func(string) (bool, error) {
return func(s string) (bool, error) {

if len(s) == 0 {
return false, nil
}

kv, _, err := consulClient.KV().Get(s, nil)
if err != nil {
return false, err
}

if kv == nil {
return false, nil
}

log.Info().Msgf("template/funcs: found Consul KV variable with key %s", s)

return true, nil
}
}

func consulKeyOrDefaultFunc(consulClient *consul.Client) func(string, string) (string, error) {
return func(s, d string) (string, error) {

if len(s) == 0 {
log.Info().Msgf("template/funcs: using default Consul KV variable with value %s", d)
return d, nil
}

kv, _, err := consulClient.KV().Get(s, nil)
if err != nil {
return "", err
}

if kv == nil {
log.Info().Msgf("template/funcs: using default Consul KV variable with value %s", d)
return d, nil
}

v := string(kv.Value[:])
log.Info().Msgf("template/funcs: using Consul KV variable with key %s and value %s",
s, v)

return v, nil
}
}

func timeNowFunc() string {
return time.Now().Format("2006-01-02T15:04:05Z07:00")
}

func timeNowUTCFunc() string {
return time.Now().UTC().Format("2006-01-02T15:04:05Z07:00")
}

func timeNowTimezoneFunc() func(string) (string, error) {
return func(t string) (string, error) {

if t == "" {
return "", nil
}

loc, err := time.LoadLocation(t)
if err != nil {
return "", err
}

return time.Now().In(loc).Format("2006-01-02T15:04:05Z07:00"), nil
}
}
Loading

0 comments on commit 9502d0f

Please sign in to comment.