Repository: honeydipper/honeydipper (go)

Issue:
Conditional asset loading using go template
Honeydipper already uses go template for string interpolation. However, since it is **inline template**, it only affects string values. We can't use that to load assets conditionally. Being able to load assets conditionally can be very useful when we sharing config repos between environments. For example, imaging we added the global template feature with `<%` and `%>` marker, we can load a rule or a system based on the environment.

```yaml
---
rules:
<% if eq .env.environment "prod" %>
  - when:
      target:
        system: internal
        trigger: pubsub
      if_match:
        json:
          event: deploy_app_prod
    do:
      call_workflow: deploy_prod
<% end %>
... other rules...
```
Or something like
```yaml
system:
  github_app:
    extends:
      - github
    data:
      signingSecret: LOOKUP[gcloud-secret,<% .env.gcp_project %>/github_signing_secret
```

The normal context variables, event data and labels will not be available during the render of the file because it is rendered at file loading time. But, environment variables, ip addresses, CLI parameters should be available.

Pull request description:
feat: env var interpolation and load-time interpolation
#### Description

Support load-time interpolation of go template in addition to inline interpolation. Support using environment variable in interpolation.

#### This PR fixes the following issues
Fixes #496



Patch:
diff --git a/internal/config/repo.go b/internal/config/repo.go
index 83cb465..89c71eb 100644
--- a/internal/config/repo.go
+++ b/internal/config/repo.go
@@ -1,4 +1,4 @@
-// Copyright 2022 PayPal Inc.
+// Copyright 2023 PayPal Inc.
 
 // This Source Code Form is subject to the terms of the MIT License.
 // If a copy of the MIT License was not distributed with this file,
@@ -75,6 +75,7 @@ func (c *Repo) loadFile(filename string) {
 
 	var content DataSet
 	yamlFile := dipper.Must(ioutil.ReadFile(path.Join(c.root, filename[1:]))).([]byte)
+	yamlFile = []byte(dipper.InterpolateGoTemplate(true, "filename", string(yamlFile), map[string]interface{}{"env": dipper.Getenv()}))
 	dipper.Must(yaml.Unmarshal(yamlFile, &content))
 
 	if content.Repos != nil {
diff --git a/pkg/dipper/env.go b/pkg/dipper/env.go
new file mode 100644
index 0000000..d7a6d1a
--- /dev/null
+++ b/pkg/dipper/env.go
@@ -0,0 +1,39 @@
+// Copyright 2023 PayPal Inc.
+
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT License was not distributed with this file,
+// you can obtain one at https://mit-license.org/.
+
+// Package dipper is a library used for developing drivers for Honeydipper.
+package dipper
+
+import (
+	"os"
+	"strings"
+)
+
+var _envs map[string]interface{}
+
+const (
+	envPrefix    string = "HD_"
+	envPrefixLen int    = len(envPrefix)
+)
+
+// Getenv returns all HD_ prefixed environment variables in a map, with prefix removed.
+func Getenv() map[string]interface{} {
+	if _envs != nil {
+		return _envs
+	}
+
+	_envs = map[string]interface{}{}
+	for _, v := range os.Environ() {
+		if strings.HasPrefix(v, envPrefix) {
+			at := strings.IndexRune(v, '=')
+			name := v[envPrefixLen:at]
+			value := v[at+1:]
+			_envs[name] = value
+		}
+	}
+
+	return _envs
+}
diff --git a/pkg/dipper/interpolation.go b/pkg/dipper/interpolation.go
index e77faba..98beca1 100644
--- a/pkg/dipper/interpolation.go
+++ b/pkg/dipper/interpolation.go
@@ -1,4 +1,4 @@
-// Copyright 2022 PayPal Inc.
+// Copyright 2023 PayPal Inc.
 
 // This Source Code Form is subject to the terms of the MIT License.
 // If a copy of the MIT License was not distributed with this file,
@@ -48,9 +48,15 @@ func InterpolateStr(pattern string, data interface{}) string {
 }
 
 // InterpolateGoTemplate : parse the string as go template.
-func InterpolateGoTemplate(pattern string, data interface{}) string {
-	if strings.Contains(pattern, "{{") {
-		tmpl := template.Must(template.New("got").Funcs(FuncMap).Funcs(sprig.TxtFuncMap()).Parse(pattern))
+func InterpolateGoTemplate(isLoading bool, title string, pattern string, data interface{}) string {
+	ldelim := "{{"
+	rdelim := "}}"
+	if isLoading {
+		ldelim = "{%"
+		rdelim = "%}"
+	}
+	if strings.Contains(pattern, ldelim) {
+		tmpl := template.Must(template.New(title).Funcs(FuncMap).Funcs(sprig.TxtFuncMap()).Delims(ldelim, rdelim).Parse(pattern))
 		buf := new(bytes.Buffer)
 		if err := tmpl.Execute(buf, data); err != nil {
 			Logger.Warningf("interpolation pattern failed: %+v", pattern)
@@ -132,7 +138,7 @@ func Interpolate(source interface{}, data interface{}) interface{} {
 			return InterpolateDollarStr(v, data)
 		}
 
-		ret := InterpolateGoTemplate(v, data)
+		ret := InterpolateGoTemplate(false, "go", v, data)
 
 		if strings.HasPrefix(ret, ":yaml:") {
 			defer func() {


Test patch:
diff --git a/pkg/dipper/env_test.go b/pkg/dipper/env_test.go
new file mode 100644
index 0000000..9c191d7
--- /dev/null
+++ b/pkg/dipper/env_test.go
@@ -0,0 +1,27 @@
+// Copyright 2023 PayPal Inc.
+
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT License was not distributed with this file,
+// you can obtain one at https://mit-license.org/.
+
+//go:build !integration
+// +build !integration
+
+package dipper
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetenv(t *testing.T) {
+	assert.NotNil(t, Getenv(), "Getenv should not return nil")
+	assert.NotContains(t, Getenv(), "TEST_ENV", "TEST_ENV should not present before injection")
+
+	// reset
+	_envs = nil
+	t.Setenv("HD_TEST_ENV", "present")
+	assert.NotNil(t, Getenv(), "Getenv should not return nil the 2nd time")
+	assert.Contains(t, Getenv(), "TEST_ENV", "TEST_ENV should present after injection")
+}
diff --git a/pkg/dipper/interpolation_test.go b/pkg/dipper/interpolation_test.go
index 75334e8..c5c1779 100644
--- a/pkg/dipper/interpolation_test.go
+++ b/pkg/dipper/interpolation_test.go
@@ -1,4 +1,4 @@
-// Copyright 2022 PayPal Inc.
+// Copyright 2023 PayPal Inc.
 
 // This Source Code Form is subject to the terms of the MIT License.
 // If a copy of the MIT License was not distributed with this file,
@@ -72,3 +72,10 @@ test:
 		"interpolating a map of templates",
 	)
 }
+
+func TestInterpolateGoTemplate(t *testing.T) {
+	assert.Equal(t, "{% not interpolated %}", InterpolateGoTemplate(false, "go", "{% not interpolated %}", map[string]interface{}{}), "should not interpolate {%%} in non-loading time")
+	assert.Equal(t, "{{ not interpolated }}", InterpolateGoTemplate(true, "test.yml", "{{ not interpolated }}", map[string]interface{}{}), "should not interpolate {{}} in loading time")
+	assert.Equal(t, "test", InterpolateGoTemplate(false, "go", "{{ .env.TEST_ENV }}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}), "should interpolate {{}} in non-loading time")
+	assert.Equal(t, "test", InterpolateGoTemplate(true, "test.yml", "{% .env.TEST_ENV %}", map[string]interface{}{"env": map[string]interface{}{"TEST_ENV": "test"}}), "should interpolate {%%} in loading time")
+}