Skip to content

Commit

Permalink
fix: interpolation of mixed delimiters
Browse files Browse the repository at this point in the history
  • Loading branch information
JGiola committed Jul 25, 2024
1 parent e3a53df commit 2ab5001
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 56 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- complete rewrite of the cli
- changed interpolation implementation for future improvements
- changed interpolation implementation for better mantainability
- use configmap instead of secret as inventory storage
- update to go 1.22.5

Expand Down
2 changes: 1 addition & 1 deletion docs/50_interpolate.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ and substitute the first of this three variables that contains a value.

By default this interpolation will set the value on a single line putting the `\n` character explicitly for every
new line found in the value.
If the interpolation sequence is found sorrounded by the `"` or `'` character will also escape the content contained
If the interpolation sequence is found sorrounded by the `"` or `'` character we will also escape the content contained
in the environment for you so that the resulting string will be a valid double or single quoted string.
2 changes: 1 addition & 1 deletion pkg/cmd/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func (o *Options) readConfiguration(ctx context.Context, path string) (*v1.Gener
}

logger.V(8).Info("interpolating configuration file", "path", path)
interpolatedData, err := interpolate.Interpolate(data, o.prefixes, filepath.Base(path))
interpolatedData, err := interpolate.Interpolate(data, o.prefixes)
if err != nil {
return nil, err
}
Expand Down
105 changes: 54 additions & 51 deletions pkg/cmd/interpolate/interpolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package interpolate

import (
"bytes"
"context"
"fmt"
"io"
Expand All @@ -27,7 +26,6 @@ import (
"slices"
"strconv"
"strings"
"text/template"

"github.com/MakeNowJust/heredoc/v2"
"github.com/go-logr/logr"
Expand Down Expand Up @@ -79,7 +77,13 @@ const (
stdinToken = "-"
outputFileNameForStdin = "output.yaml"

envirnonmentRegex = `[{]{2}([A-Z0-9_]+)[}]{2}`
envirnonmentRegex = `[{]{2}([A-Z0-9_]+)[}]{2}`
unqutedLeftDelim = `{{`
unqutedRightDelim = `}}`
doubleQoutedLeftDelim = `"` + unqutedLeftDelim
doubleQoutedRightDelim = unqutedRightDelim + `"`
singleQoutedLeftDelim = `'` + unqutedLeftDelim
singleQoutedRightDelim = unqutedRightDelim + `'`
)

// Flags contains all the flags for the `interpolate` command. They will be converted to Options
Expand Down Expand Up @@ -175,7 +179,7 @@ func (o *Options) Run(ctx context.Context) error {
}

logger.V(5).Info("intepolating file", "path", path)
interpolatedData, err := Interpolate(data, o.prefixes, name)
interpolatedData, err := Interpolate(data, o.prefixes)
if err != nil {
return err
}
Expand Down Expand Up @@ -249,44 +253,16 @@ func (o *Options) readFile(path string) ([]byte, string, error) {
}

// Interpolate will interpolate the data content with values from env values
func Interpolate(data []byte, envPrefixes []string, name string) ([]byte, error) {
noQuoteTemplateFuncs := make(template.FuncMap)
singleQuotedTemplateFuncs := make(template.FuncMap)
doubleQuotedTemplateFuncs := make(template.FuncMap)

func Interpolate(data []byte, envPrefixes []string) ([]byte, error) {
for _, env := range envNamesToInterpolate(data) {
noQuoteTemplateFuncs[env] = func() (string, error) {
return substituteEnv(env, envPrefixes, func(str string) string {
return strings.ReplaceAll(str, "\n", "\\n") // keep multiline string on one line
})
}
singleQuotedTemplateFuncs[env] = func() (string, error) {
return substituteEnv(env, envPrefixes, func(str string) string {
str = strconv.Quote(str)
str = strings.ReplaceAll(str, `\\`, `\`)
str = strings.ReplaceAll(str, `\"`, `"`)
return "'" + str[1:len(str)-1] + "'"
})
}
doubleQuotedTemplateFuncs[env] = func() (string, error) {
return substituteEnv(env, envPrefixes, func(str string) string {
str = strconv.Quote(str)
return strings.ReplaceAll(str, `\\`, `\`)
})
parsedData, err := substituteEnv(string(data), env, envPrefixes)
if err != nil {
return nil, err
}
data = []byte(parsedData)
}

var parsedData []byte
var err error
if parsedData, err = templating(name, `'{{`, `}}'`, singleQuotedTemplateFuncs, data); err != nil {
return nil, err
}

if parsedData, err = templating(name, `"{{`, `}}"`, doubleQuotedTemplateFuncs, parsedData); err != nil {
return nil, err
}

return templating(name, "", "", noQuoteTemplateFuncs, parsedData)
return data, nil
}

func envNamesToInterpolate(data []byte) []string {
Expand All @@ -302,7 +278,45 @@ func envNamesToInterpolate(data []byte) []string {
return envNames
}

func substituteEnv(envName string, prefixes []string, quotingStringFun func(string) string) (string, error) {
// substituteEnv substitute envName in data when encased in a set of delimiters appling transformations on the
// value contained in it.
func substituteEnv(data, envName string, prefixes []string) (string, error) {
doubleQouted := doubleQoutedLeftDelim + envName + doubleQoutedRightDelim
substitution, err := valueForEnv(envName, prefixes, func(str string) string {
str = strconv.Quote(str)
return strings.ReplaceAll(str, `\\`, `\`)
})
if err != nil {
return "", err
}

data = strings.ReplaceAll(data, doubleQouted, substitution)

singleQouted := singleQoutedLeftDelim + envName + singleQoutedRightDelim
substitution, err = valueForEnv(envName, prefixes, func(str string) string {
str = strconv.Quote(str)
str = strings.ReplaceAll(str, `\\`, `\`)
str = strings.ReplaceAll(str, `\"`, `"`)
return "'" + str[1:len(str)-1] + "'"
})
if err != nil {
return "", err
}

data = strings.ReplaceAll(data, singleQouted, substitution)

unquoted := unqutedLeftDelim + envName + unqutedRightDelim
substitution, err = valueForEnv(envName, prefixes, func(str string) string {
return strings.ReplaceAll(str, "\n", "\\n") // keep multiline string on one line
})
if err != nil {
return "", err
}

return strings.ReplaceAll(data, unquoted, substitution), nil
}

func valueForEnv(envName string, prefixes []string, fn func(string) string) (string, error) {
envsToCheck := make([]string, 0, len(prefixes)+1)
for _, prefix := range prefixes {
envsToCheck = append(envsToCheck, prefix+envName)
Expand All @@ -311,20 +325,9 @@ func substituteEnv(envName string, prefixes []string, quotingStringFun func(stri

for _, envName := range envsToCheck {
if val, exists := os.LookupEnv(envName); exists {
return quotingStringFun(val), nil
return fn(val), nil
}
}

return "", fmt.Errorf("environment variable %q not found", envName)
}

func templating(name, leftDelim, rightDelim string, funcs template.FuncMap, data []byte) ([]byte, error) {
tmpl := template.New(name).Delims(leftDelim, rightDelim).Funcs(funcs)
if _, err := tmpl.Parse(string(data)); err != nil {
return nil, err
}

buffer := new(bytes.Buffer)
err := tmpl.Execute(buffer, data)
return buffer.Bytes(), err
}
4 changes: 3 additions & 1 deletion pkg/cmd/interpolate/testdata/folder/other-file.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ data:
key: "{{JSON_SINGLELINE_ENV}}"
key2: {{MULTILINE_STRING}}
key3: {{STRING_ESCAPED_ENV}}
key4: {{DOLLAR_ENV}}
key4: "{{DOLLAR_ENV}}otherstring"
key5: "otherstring{{MLP_SIMPLE_ENV}}"
key6: {{DOLLAR_ENV}}
4 changes: 3 additions & 1 deletion pkg/cmd/interpolate/testdata/results/other-file.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ data:
key: "{\"type\":\"type\",\"project_id\":\"id\",\"private_key_id\":\"key\",\"private_key\":\"-----BEGIN CERTIFICATE-----\nXXXXXXXXXXXXXXXXXXXXXXXXX\nYYYYYYYYYYYYYYY/4YYYYYYYYY\n-----END CERTIFICATE-----\n\",\"client_email\":\"[email protected]\",\"client_id\":\"client-id\",\"auth_uri\":\"https://example.com/auth\",\"token_uri\":\"https://example.com/token\",\"auth_provider_x509_cert_url\":\"https://example.com/certs\",\"client_x509_cert_url\":\"https://example.com/certs/fooo%40bar\"}"
key2: -----BEGIN CERTIFICATE-----\nXXXXXXXXXXXXXXXXXXXXXXXXX\nYYYYYYYYYYYYYYY/4YYYYYYYYY\nZZZZZZZZZZZZZZZZZZZZZZZZZZ\n-----END CERTIFICATE-----
key3: env\\first\line
key4: $contains$dollars$
key4: "$contains$dollars$otherstring"
key5: "otherstringtest"
key6: $contains$dollars$

0 comments on commit 2ab5001

Please sign in to comment.