about summary refs log tree commit diff
path: root/ops/kontemplate/templater/templater.go
diff options
context:
space:
mode:
Diffstat (limited to 'ops/kontemplate/templater/templater.go')
-rw-r--r--ops/kontemplate/templater/templater.go236
1 files changed, 236 insertions, 0 deletions
diff --git a/ops/kontemplate/templater/templater.go b/ops/kontemplate/templater/templater.go
new file mode 100644
index 000000000000..a8f0c670a603
--- /dev/null
+++ b/ops/kontemplate/templater/templater.go
@@ -0,0 +1,236 @@
+// Copyright (C) 2016-2019  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
+	Args      []string
+}
+
+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(ctx *context.Context, rs *context.ResourceSet) (*RenderedResourceSet, error) {
+	fmt.Fprintf(os.Stderr, "Loading resources for %s\n", rs.Name)
+
+	fileInfo, err := os.Stat(rs.Path)
+	if err != nil {
+		return nil, err
+	}
+
+	var files []os.FileInfo
+	var resources []RenderedResource
+
+	// Treat single-file resource paths separately from resource
+	// sets containing multiple templates
+	if fileInfo.IsDir() {
+		// 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(rs.Path)
+		resources, err = processFiles(ctx, rs, files)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		resource, err := templateFile(ctx, rs, rs.Path)
+		if err != nil {
+			return nil, err
+		}
+
+		resources = []RenderedResource{resource}
+	}
+
+	return &RenderedResourceSet{
+		Name:      rs.Name,
+		Resources: resources,
+		Args:      rs.Args,
+	}, nil
+}
+
+func processFiles(ctx *context.Context, rs *context.ResourceSet, files []os.FileInfo) ([]RenderedResource, error) {
+	resources := make([]RenderedResource, 0)
+
+	for _, file := range files {
+		if !file.IsDir() && isResourceFile(file) {
+			path := path.Join(rs.Path, file.Name())
+			res, err := templateFile(ctx, rs, path)
+
+			if err != nil {
+				return resources, err
+			}
+
+			resources = append(resources, res)
+		}
+	}
+
+	return resources, nil
+}
+
+func templateFile(ctx *context.Context, rs *context.ResourceSet, filepath string) (RenderedResource, error) {
+	var resource RenderedResource
+
+	tpl, err := template.New(path.Base(filepath)).Funcs(templateFuncs(ctx, rs)).Option(failOnMissingKeys).ParseFiles(filepath)
+	if err != nil {
+		return resource, fmt.Errorf("Could not load template %s: %v", filepath, err)
+	}
+
+	var b bytes.Buffer
+	err = tpl.Execute(&b, rs.Values)
+	if err != nil {
+		return resource, fmt.Errorf("Error while templating %s: %v", filepath, err)
+	}
+
+	resource = RenderedResource{
+		Filename: path.Base(filepath),
+		Rendered: b.String(),
+	}
+
+	return resource, 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
+	}
+	m["insertTemplate"] = func(file string) (string, error) {
+		data, err := templateFile(c, rs, path.Join(rs.Path, file))
+		if err != nil {
+			return "", err
+		}
+
+		return data.Rendered, nil
+	}
+	m["default"] = func(defaultVal interface{}, varName string) interface{} {
+		if val, ok := rs.Values[varName]; ok {
+			return val
+		}
+
+		return defaultVal
+	}
+	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")
+}