2017-11-21 11:24:04 +01:00
|
|
|
// 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.
|
|
|
|
|
2017-02-08 11:50:49 +01:00
|
|
|
package templater
|
|
|
|
|
|
|
|
import (
|
2017-02-08 11:55:59 +01:00
|
|
|
"bytes"
|
2017-02-08 14:37:36 +01:00
|
|
|
"encoding/json"
|
2017-02-08 11:50:49 +01:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path"
|
2017-02-08 11:55:59 +01:00
|
|
|
"strings"
|
2017-02-08 11:50:49 +01:00
|
|
|
"text/template"
|
|
|
|
|
2017-02-08 14:37:36 +01:00
|
|
|
"github.com/Masterminds/sprig"
|
2017-02-08 11:50:49 +01:00
|
|
|
"github.com/polydawn/meep"
|
2017-02-08 11:55:59 +01:00
|
|
|
"github.com/tazjin/kontemplate/context"
|
2017-02-10 20:53:59 +01:00
|
|
|
"github.com/tazjin/kontemplate/util"
|
2017-02-08 11:50:49 +01:00
|
|
|
)
|
|
|
|
|
2017-04-04 11:02:34 +02:00
|
|
|
const failOnMissingKeys string = "missingkey=error"
|
|
|
|
|
2017-02-08 11:50:49 +01:00
|
|
|
// Error that is caused by non-existent template files being specified
|
|
|
|
type TemplateNotFoundError struct {
|
|
|
|
meep.AllTraits
|
|
|
|
Name string
|
2017-07-13 16:00:21 +02:00
|
|
|
Path string
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Error that is caused during templating, e.g. required value being absent or invalid template format
|
|
|
|
type TemplatingError struct {
|
2017-02-09 15:32:14 +01:00
|
|
|
meep.TraitAutodescribing
|
|
|
|
meep.TraitCausable
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
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) {
|
2017-02-08 16:44:55 +01:00
|
|
|
limitedResourceSets := applyLimits(&c.ResourceSets, include, exclude)
|
2017-06-11 21:43:02 +02:00
|
|
|
renderedResourceSets := make([]RenderedResourceSet, 0)
|
2017-02-08 12:58:53 +01:00
|
|
|
|
2017-05-04 17:24:36 +02:00
|
|
|
if len(*limitedResourceSets) == 0 {
|
2017-06-11 21:25:56 +02:00
|
|
|
return renderedResourceSets, fmt.Errorf("No valid resource sets included!")
|
2017-05-04 17:24:36 +02:00
|
|
|
}
|
|
|
|
|
2017-02-08 16:44:55 +01:00
|
|
|
for _, rs := range *limitedResourceSets {
|
2017-06-11 21:25:56 +02:00
|
|
|
set, err := processResourceSet(c, &rs)
|
2017-02-08 11:50:49 +01:00
|
|
|
|
2017-02-08 16:44:55 +01:00
|
|
|
if err != nil {
|
2017-06-11 21:25:56 +02:00
|
|
|
return nil, err
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
2017-06-11 21:25:56 +02:00
|
|
|
|
|
|
|
renderedResourceSets = append(renderedResourceSets, *set)
|
2017-02-08 12:58:53 +01:00
|
|
|
}
|
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
return renderedResourceSets, nil
|
2017-02-08 12:58:53 +01:00
|
|
|
}
|
2017-02-08 11:50:49 +01:00
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
func processResourceSet(c *context.Context, rs *context.ResourceSet) (*RenderedResourceSet, error) {
|
2017-02-08 12:58:53 +01:00
|
|
|
fmt.Fprintf(os.Stderr, "Loading resources for %s\n", rs.Name)
|
2017-02-08 11:50:49 +01:00
|
|
|
|
2017-07-13 16:00:21 +02:00
|
|
|
rp := path.Join(c.BaseDir, rs.Path)
|
2017-10-27 02:37:28 +02:00
|
|
|
|
|
|
|
// 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)
|
2017-02-08 11:50:49 +01:00
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
resources, err := processFiles(c, rs, rp, files)
|
2017-02-08 12:58:53 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2017-06-11 21:25:56 +02:00
|
|
|
return nil, meep.New(
|
2017-07-13 16:00:21 +02:00
|
|
|
&TemplateNotFoundError{Name: rs.Name, Path: rs.Path},
|
2017-02-08 12:58:53 +01:00
|
|
|
meep.Cause(err),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
return &RenderedResourceSet{
|
|
|
|
Name: rs.Name,
|
|
|
|
Resources: resources,
|
|
|
|
}, nil
|
2017-02-08 12:58:53 +01:00
|
|
|
}
|
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
func processFiles(c *context.Context, rs *context.ResourceSet, rp string, files []os.FileInfo) ([]RenderedResource, error) {
|
2017-06-11 21:43:02 +02:00
|
|
|
resources := make([]RenderedResource, 0)
|
2017-06-11 21:25:56 +02:00
|
|
|
|
2017-02-08 12:58:53 +01:00
|
|
|
for _, file := range files {
|
|
|
|
if !file.IsDir() && isResourceFile(file) {
|
|
|
|
p := path.Join(rp, file.Name())
|
|
|
|
o, err := templateFile(c, rs, p)
|
|
|
|
|
|
|
|
if err != nil {
|
2017-06-11 21:25:56 +02:00
|
|
|
return resources, err
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
2017-02-08 12:58:53 +01:00
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
res := RenderedResource{
|
|
|
|
Filename: file.Name(),
|
|
|
|
Rendered: o,
|
|
|
|
}
|
|
|
|
resources = append(resources, res)
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-11 21:25:56 +02:00
|
|
|
return resources, nil
|
2017-02-08 11:50:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func templateFile(c *context.Context, rs *context.ResourceSet, filename string) (string, error) {
|
2018-03-09 14:49:33 +01:00
|
|
|
tpl, err := template.New(path.Base(filename)).Funcs(templateFuncs(rs)).Option(failOnMissingKeys).ParseFiles(filename)
|
2017-02-08 11:50:49 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", meep.New(
|
|
|
|
&TemplateNotFoundError{Name: filename},
|
|
|
|
meep.Cause(err),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
|
2017-02-10 20:53:59 +01:00
|
|
|
rs.Values = *util.Merge(&c.Global, &rs.Values)
|
2017-02-08 11:50:49 +01:00
|
|
|
|
|
|
|
err = tpl.Execute(&b, rs.Values)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", meep.New(
|
|
|
|
&TemplatingError{},
|
|
|
|
meep.Cause(err),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return b.String(), nil
|
|
|
|
}
|
|
|
|
|
2017-02-08 16:44:55 +01:00
|
|
|
// 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 {
|
2017-02-08 17:14:40 +01:00
|
|
|
if !matchesResourceSet(exclude, &r) {
|
2017-02-08 16:44:55 +01:00
|
|
|
excluded = append(excluded, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Include included resource sets
|
|
|
|
if len(*include) == 0 {
|
|
|
|
return &excluded
|
|
|
|
}
|
|
|
|
included := make([]context.ResourceSet, 0)
|
|
|
|
for _, r := range excluded {
|
2017-02-08 17:14:40 +01:00
|
|
|
if matchesResourceSet(include, &r) {
|
2017-02-08 16:44:55 +01:00
|
|
|
included = append(included, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &included
|
|
|
|
}
|
|
|
|
|
2017-02-08 17:14:40 +01:00
|
|
|
// Check whether an include/exclude string slice matches a resource set
|
|
|
|
func matchesResourceSet(s *[]string, rs *context.ResourceSet) bool {
|
2017-02-08 16:44:55 +01:00
|
|
|
for _, r := range *s {
|
2017-06-11 22:27:20 +02:00
|
|
|
r = strings.TrimSuffix(r, "/")
|
2017-02-08 17:32:19 +01:00
|
|
|
if r == rs.Name || r == rs.Parent {
|
2017-02-08 16:44:55 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-03-09 14:49:33 +01:00
|
|
|
func templateFuncs(rs *context.ResourceSet) template.FuncMap {
|
2017-02-08 14:37:36 +01:00
|
|
|
m := sprig.TxtFuncMap()
|
|
|
|
m["json"] = func(data interface{}) string {
|
|
|
|
b, _ := json.Marshal(data)
|
|
|
|
return string(b)
|
|
|
|
}
|
2017-02-09 15:33:03 +01:00
|
|
|
m["passLookup"] = GetFromPass
|
2017-10-17 22:49:42 +02:00
|
|
|
m["lookupIPAddr"] = GetIPsFromDNS
|
2018-03-09 14:49:33 +01:00
|
|
|
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
|
|
|
|
}
|
2017-02-08 14:37:36 +01:00
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
2017-04-04 14:36:27 +02:00
|
|
|
// Checks whether a file is a resource file (i.e. is YAML or JSON) and not a default values file.
|
2017-02-08 11:50:49 +01:00
|
|
|
func isResourceFile(f os.FileInfo) bool {
|
2017-08-31 18:37:57 +02:00
|
|
|
for _, defaultFile := range util.DefaultFilenames {
|
|
|
|
if f.Name() == defaultFile {
|
|
|
|
return false
|
|
|
|
}
|
2017-04-04 14:36:27 +02:00
|
|
|
}
|
|
|
|
|
2017-02-08 11:50:49 +01:00
|
|
|
return strings.HasSuffix(f.Name(), "yaml") ||
|
|
|
|
strings.HasSuffix(f.Name(), "yml") ||
|
|
|
|
strings.HasSuffix(f.Name(), "json")
|
|
|
|
}
|