Skip to content
This repository has been archived by the owner on Jul 4, 2024. It is now read-only.

Fix: Do not substitute strings without brackets #45

Merged
merged 1 commit into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions internal/humanitec/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package humanitec
import (
"fmt"
"log"
"os"
"regexp"
"strings"

"github.com/mitchellh/mapstructure"
Expand All @@ -19,6 +19,10 @@ import (
extensions "github.com/score-spec/score-humanitec/internal/humanitec/extensions"
)

var (
placeholderRegEx = regexp.MustCompile(`\$(\$|{([a-zA-Z0-9.\-_\[\]"'#]+)})`)
)

// templatesContext ia an utility type that provides a context for '${...}' templates substitution
type templatesContext struct {
meta map[string]interface{}
Expand Down Expand Up @@ -65,22 +69,31 @@ func (ctx *templatesContext) SubstituteAll(src map[string]interface{}) map[strin

// Substitute replaces all matching '${...}' templates in a source string
func (ctx *templatesContext) Substitute(src string) string {
return os.Expand(src, ctx.mapVar)
return placeholderRegEx.ReplaceAllStringFunc(src, func(str string) string {
// WORKAROUND: ReplaceAllStringFunc(..) does not provide match details
// https://github.com/golang/go/issues/5690
var matches = placeholderRegEx.FindStringSubmatch(str)

// SANITY CHECK
if len(matches) != 3 {
log.Printf("Error: could not find a proper match in previously captured string fragment")
return src
}

// EDGE CASE: Captures "$$" sequences and empty templates "${}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Score placeholders follow the Humanitec syntax: https://developer.humanitec.com/platform-orchestrator/reference/placeholders/

This does not support $$ for escaping the placeholder, rather $\{.

There is a further problem here that if I escape $${shared.my-var} it will turn into ${shared.my-var} and so be interpreted as a valid placeholder by Humanitec which it should not. It should still be escaped for Humanitec to deal with in the delta.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$$ -> $ is supported right now in all our CLI tools. If we remove this functionality, it may break existing setups, where someone is already using $$. We are solving a concrete issue in this PR. Lets stick to it and do not add new breaking changes. We can address all other improvements in separate PRs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the second point still stands. You should not unescape yet. What you need to do is convert it into $\{ so that Humanitec does not read it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the case. Several placeholders in Humanitec does not have their counterparts in Score resources, and this construct is used exactly to pass through the placeholder as is, so Humanitec would handle it properly. For example, $${pod.status.hostIP}.

Note, that similar mechanism is used in score-compose to reference environment variables that should not be substituted by the CLI tool, but they are known to compose CLI tool.

We can change this behaviour, but again, this would be a breaking change and it does not relate to the issue we are addressing in this PR. A concrete issue is that we want to ignore $abc and only react to ${abc} (with brackets).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's open a second issue to straighten up escaping more generally.

if matches[2] == "" {
return matches[1]
}

return ctx.mapVar(matches[2])
})

}

// MapVar replaces objects and properties references with corresponding values
// Returns an empty string if the reference can't be resolved
func (ctx *templatesContext) mapVar(ref string) string {
if ref == "" {
return ""
}

// NOTE: os.Expand(..) would invoke a callback function with "$" as an argument for escaped sequences.
// "$${abc}" is treated as "$$" pattern and "{abc}" static text.
// The first segment (pattern) would trigger a callback function call.
// By returning "$" value we would ensure that escaped sequences would remain in the source text.
// For example "$${abc}" would result in "${abc}" after os.Expand(..) call.
if ref == "$" {
if ref == "" || ref == "$" {
return ref
}

Expand Down
1 change: 1 addition & 0 deletions internal/humanitec/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func TestSubstitute(t *testing.T) {
assert.Equal(t, "", ctx.Substitute(""))
assert.Equal(t, "abc", ctx.Substitute("abc"))
assert.Equal(t, "abc $ abc", ctx.Substitute("abc $$ abc"))
assert.Equal(t, "$abc", ctx.Substitute("$abc"))
assert.Equal(t, "${abc}", ctx.Substitute("$${abc}"))

assert.Equal(t, "The name is 'test-name'", ctx.Substitute("The name is '${metadata.name}'"))
Expand Down