tvl-depot/templater/templater.go
Vincent Ambo b33c353233 refactor(context): Implement more explicit merging of variables
The hierarchy for loading variables was previously not expressed
explicitly.

This commit refactors the logic for merging variables to explicitly
set the different layers of variables as values on the context object
and merge them for each resource set in `mergeContextValues`.
2018-06-09 19:16:23 +02:00

203 lines
5 KiB
Go

// Copyright (C) 2016-2017 Vincent Ambo <mail@tazj.in>
//
// This file is part of Kontemplate.
//
// Kontemplate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
package templater
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"text/template"
"github.com/Masterminds/sprig"
"github.com/tazjin/kontemplate/context"
"github.com/tazjin/kontemplate/util"
)
const failOnMissingKeys string = "missingkey=error"
type RenderedResource struct {
Filename string
Rendered string
}
type RenderedResourceSet struct {
Name string
Resources []RenderedResource
}
func LoadAndApplyTemplates(include *[]string, exclude *[]string, c *context.Context) ([]RenderedResourceSet, error) {
limitedResourceSets := applyLimits(&c.ResourceSets, include, exclude)
renderedResourceSets := make([]RenderedResourceSet, 0)
if len(*limitedResourceSets) == 0 {
return renderedResourceSets, fmt.Errorf("No valid resource sets included!")
}
for _, rs := range *limitedResourceSets {
set, err := processResourceSet(c, &rs)
if err != nil {
return nil, err
}
renderedResourceSets = append(renderedResourceSets, *set)
}
return renderedResourceSets, nil
}
func processResourceSet(c *context.Context, rs *context.ResourceSet) (*RenderedResourceSet, error) {
fmt.Fprintf(os.Stderr, "Loading resources for %s\n", rs.Name)
rp := path.Join(c.BaseDir, rs.Path)
// Explicitly discard this error, which will give us an empty list of files instead.
// This will end up printing a warning to the user, but it won't stop the rest of the process.
files, _ := ioutil.ReadDir(rp)
resources, err := processFiles(c, rs, rp, files)
if err != nil {
return nil, err
}
return &RenderedResourceSet{
Name: rs.Name,
Resources: resources,
}, nil
}
func processFiles(c *context.Context, rs *context.ResourceSet, rp string, files []os.FileInfo) ([]RenderedResource, error) {
resources := make([]RenderedResource, 0)
for _, file := range files {
if !file.IsDir() && isResourceFile(file) {
p := path.Join(rp, file.Name())
o, err := templateFile(c, rs, p)
if err != nil {
return resources, err
}
res := RenderedResource{
Filename: file.Name(),
Rendered: o,
}
resources = append(resources, res)
}
}
return resources, nil
}
func templateFile(c *context.Context, rs *context.ResourceSet, filename string) (string, error) {
tpl, err := template.New(path.Base(filename)).Funcs(templateFuncs(c, rs)).Option(failOnMissingKeys).ParseFiles(filename)
if err != nil {
return "", fmt.Errorf("Template %s not found: %v", filename, err)
}
var b bytes.Buffer
err = tpl.Execute(&b, rs.Values)
if err != nil {
return "", fmt.Errorf("Error while templating %s: %v", filename, err)
}
return b.String(), nil
}
// Applies the limits of explicitly included or excluded resources and returns the updated resource set.
// Exclude takes priority over include
func applyLimits(rs *[]context.ResourceSet, include *[]string, exclude *[]string) *[]context.ResourceSet {
if len(*include) == 0 && len(*exclude) == 0 {
return rs
}
// Exclude excluded resource sets
excluded := make([]context.ResourceSet, 0)
for _, r := range *rs {
if !matchesResourceSet(exclude, &r) {
excluded = append(excluded, r)
}
}
// Include included resource sets
if len(*include) == 0 {
return &excluded
}
included := make([]context.ResourceSet, 0)
for _, r := range excluded {
if matchesResourceSet(include, &r) {
included = append(included, r)
}
}
return &included
}
// Check whether an include/exclude string slice matches a resource set
func matchesResourceSet(s *[]string, rs *context.ResourceSet) bool {
for _, r := range *s {
r = strings.TrimSuffix(r, "/")
if r == rs.Name || r == rs.Parent {
return true
}
}
return false
}
func templateFuncs(c *context.Context, rs *context.ResourceSet) template.FuncMap {
m := sprig.TxtFuncMap()
m["json"] = func(data interface{}) string {
b, _ := json.Marshal(data)
return string(b)
}
m["passLookup"] = GetFromPass
m["gitHEAD"] = func() (string, error) {
out, err := exec.Command("git", "-C", c.BaseDir, "rev-parse", "HEAD").Output()
if err != nil {
return "", err
}
output := strings.TrimSpace(string(out))
return output, nil
}
m["lookupIPAddr"] = GetIPsFromDNS
m["insertFile"] = func(file string) (string, error) {
data, err := ioutil.ReadFile(path.Join(rs.Path, file))
if err != nil {
return "", err
}
return string(data), nil
}
return m
}
// Checks whether a file is a resource file (i.e. is YAML or JSON) and not a default values file.
func isResourceFile(f os.FileInfo) bool {
for _, defaultFile := range util.DefaultFilenames {
if f.Name() == defaultFile {
return false
}
}
return strings.HasSuffix(f.Name(), "yaml") ||
strings.HasSuffix(f.Name(), "yml") ||
strings.HasSuffix(f.Name(), "json")
}