From 16a1041d2e45c76bee11c43eb5cfa761089d22a5 Mon Sep 17 00:00:00 2001
From: Chris Goller <goller@gmail.com>
Date: Tue, 19 Dec 2017 15:06:24 -0600
Subject: [PATCH] Add env var golang templates to filestore rendering

---
 filestore/dashboards.go     |  3 +-
 filestore/environ.go        | 24 ++++++++++++++
 filestore/environ_test.go   | 29 +++++++++++++++++
 filestore/templates.go      | 28 ++++++++++++++++
 filestore/templates_test.go | 64 +++++++++++++++++++++++++++++++++++++
 5 files changed, 147 insertions(+), 1 deletion(-)
 create mode 100644 filestore/environ.go
 create mode 100644 filestore/environ_test.go
 create mode 100644 filestore/templates.go
 create mode 100644 filestore/templates_test.go

diff --git a/filestore/dashboards.go b/filestore/dashboards.go
index 6a73a1d11..9b8fc6578 100644
--- a/filestore/dashboards.go
+++ b/filestore/dashboards.go
@@ -47,10 +47,11 @@ func dashboardFile(dir string, dashboard chronograf.Dashboard) string {
 }
 
 func load(name string, resource interface{}) error {
-	octets, err := ioutil.ReadFile(name)
+	octets, err := templatedFromEnv(name)
 	if err != nil {
 		return fmt.Errorf("resource %s not found", name)
 	}
+
 	return json.Unmarshal(octets, resource)
 }
 
diff --git a/filestore/environ.go b/filestore/environ.go
new file mode 100644
index 000000000..091e179e8
--- /dev/null
+++ b/filestore/environ.go
@@ -0,0 +1,24 @@
+package filestore
+
+import (
+	"os"
+	"strings"
+)
+
+var env map[string]string
+
+// environ returns a map of all environment variables in the running process
+func environ() map[string]string {
+	if env == nil {
+		env = make(map[string]string)
+		envVars := os.Environ()
+		for _, envVar := range envVars {
+			kv := strings.SplitN(envVar, "=", 2)
+			if len(kv) != 2 {
+				continue
+			}
+			env[kv[0]] = kv[1]
+		}
+	}
+	return env
+}
diff --git a/filestore/environ_test.go b/filestore/environ_test.go
new file mode 100644
index 000000000..689484806
--- /dev/null
+++ b/filestore/environ_test.go
@@ -0,0 +1,29 @@
+package filestore
+
+import (
+	"os"
+	"testing"
+)
+
+func Test_environ(t *testing.T) {
+	tests := []struct {
+		name  string
+		key   string
+		value string
+	}{
+		{
+			name:  "environment variable is returned",
+			key:   "CHRONOGRAF_TEST_ENVIRON",
+			value: "howdy",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			os.Setenv(tt.key, tt.value)
+			got := environ()
+			if v, ok := got[tt.key]; !ok || v != tt.value {
+				t.Errorf("environ() = %v, want %v", v, tt.value)
+			}
+		})
+	}
+}
diff --git a/filestore/templates.go b/filestore/templates.go
new file mode 100644
index 000000000..fc0e1ffc4
--- /dev/null
+++ b/filestore/templates.go
@@ -0,0 +1,28 @@
+package filestore
+
+import (
+	"bytes"
+	"html/template"
+)
+
+// templated returns all files templated using data
+func templated(data interface{}, filenames ...string) ([]byte, error) {
+	t, err := template.ParseFiles(filenames...)
+	if err != nil {
+		return nil, err
+	}
+	var b bytes.Buffer
+	// If a key in the file exists but is not in the data we
+	// immediately fail with a missing key error
+	err = t.Option("missingkey=error").Execute(&b, data)
+	if err != nil {
+		return nil, err
+	}
+
+	return b.Bytes(), nil
+}
+
+// templatedFromEnv returns all files templated against environment variables
+func templatedFromEnv(filenames ...string) ([]byte, error) {
+	return templated(environ(), filenames...)
+}
diff --git a/filestore/templates_test.go b/filestore/templates_test.go
new file mode 100644
index 000000000..5d5b82f5d
--- /dev/null
+++ b/filestore/templates_test.go
@@ -0,0 +1,64 @@
+package filestore
+
+import (
+	"io/ioutil"
+	"os"
+	"reflect"
+	"testing"
+)
+
+func Test_templated(t *testing.T) {
+	tests := []struct {
+		name    string
+		content []string
+		data    interface{}
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "files with templates are rendered correctly",
+			content: []string{
+				"{{ .MYVAR }}",
+			},
+			data: map[string]string{
+				"MYVAR": "howdy",
+			},
+			want: []byte("howdy"),
+		},
+		{
+			name: "missing key gives an error",
+			content: []string{
+				"{{ .MYVAR }}",
+			},
+			wantErr: true,
+		},
+		{
+			name:    "no files make me an error!",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			filenames := make([]string, len(tt.content))
+			for i, c := range tt.content {
+				f, err := ioutil.TempFile("", "")
+				if err != nil {
+					t.Fatal(err)
+				}
+				if _, err := f.Write([]byte(c)); err != nil {
+					t.Fatal(err)
+				}
+				filenames[i] = f.Name()
+				defer os.Remove(f.Name())
+			}
+			got, err := templated(tt.data, filenames...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("templated() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("templated() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}