Repository: score-spec/score-compose (go)

Issue:
[bug] score-compose causes double $$ escapes to break
When a variable contains `$${foo}` we current unescape it to `${foo}` in the ~Dockerfile~ compose.yaml, which then causes the compose execution to break if the variable foo is not available, or if the format is bad like `${foo.bar.baz}`.

This is generally not what we expect, and we'd rather users use an `environment` type resource if they need to access environment variables. Thus we want `${foo}` to be escaped again, back to `$${foo}`, OR to add some other mechanism for resolving these.

A big usecase is for something like a workload that has `$${pod.metadata.name}` in an environment variable because the end goal is to deploy to Humanitec. Docker will never resolve `${pod.metadata.name}`. And overrides are not useful here, because they must be applied to the whole usage of the variable, not the actual placeholder itself.

Solution 1: Do nothing, customers must find and replace the unsatisified placeholders before running `compose up`.

Solution 2: Replace `$${foo.bar}` with `${FOO_BAR}` so that an environment variable can be used.

Solution 3: Don't unescape `$${foo.bar}` in variables, so that it just stays as `${foo.bar}` at runtime.

Solution 4: Provide a `--override-placeholder foo.bar=new-value option` to supply a value to replace.

Solution 5. A mix of 3, and 4. 

So far I think solution 5 is the answer: default to no escape, but allow override.

Pull request description:
fix: don't unescape dollars in environment variable placeholders
This finishes off the last bit of work and fixes #148.

We need to use the custom unescaper to not escape the variables here. This allows it to match the expected runtime behavior.


Patch:
diff --git a/examples/02-environment/README.md b/examples/02-environment/README.md
index 2c171ad..10b7182 100644
--- a/examples/02-environment/README.md
+++ b/examples/02-environment/README.md
@@ -21,6 +21,7 @@ containers:
       GREETING: Hello
       NAME: ${resources.env.NAME}
       WORKLOAD_NAME: ${metadata.name}
+      ESCAPED: $$_$${fizzbuzz}
 
 resources:
   env:
@@ -48,6 +49,7 @@ services:
         entrypoint:
             - /bin/sh
         environment:
+            ESCAPED: $$_$${fizzbuzz}
             GREETING: Hello
             NAME: ${NAME}
             WORKLOAD_NAME: hello-world
diff --git a/examples/02-environment/score.yaml b/examples/02-environment/score.yaml
index f4de005..cba2687 100644
--- a/examples/02-environment/score.yaml
+++ b/examples/02-environment/score.yaml
@@ -12,6 +12,7 @@ containers:
       GREETING: Hello
       NAME: ${resources.env.NAME}
       WORKLOAD_NAME: ${metadata.name}
+      ESCAPED: $$_$${fizzbuzz}
 
 resources:
   env:
diff --git a/internal/compose/convert.go b/internal/compose/convert.go
index 9748043..fae363e 100644
--- a/internal/compose/convert.go
+++ b/internal/compose/convert.go
@@ -68,13 +68,20 @@ func ConvertSpec(state *project.State, spec *score.Workload) (*compose.Project,
 	}
 	sort.Strings(containerNames)
 
+	variablesSubstitutor := framework.Substituter{
+		Replacer: deferredSubstitutionFunction,
+		UnEscaper: func(s string) (string, error) {
+			return s, nil
+		},
+	}
+
 	var firstService string
 	for _, containerName := range containerNames {
 		cSpec := spec.Containers[containerName]
 
 		var env = make(compose.MappingWithEquals, len(cSpec.Variables))
 		for key, val := range cSpec.Variables {
-			resolved, err := framework.SubstituteString(val, deferredSubstitutionFunction)
+			resolved, err := variablesSubstitutor.SubstituteString(val)
 			if err != nil {
 				return nil, fmt.Errorf("containers.%s.variables.%s: %w", containerName, key, err)
 			}


Test patch:
diff --git a/internal/command/generate_examples_test.go b/internal/command/generate_examples_test.go
index e36298a..2325daa 100644
--- a/internal/command/generate_examples_test.go
+++ b/internal/command/generate_examples_test.go
@@ -68,6 +68,7 @@ services:
         entrypoint:
             - /bin/sh
         environment:
+            ESCAPED: $$_$${fizzbuzz}
             GREETING: Hello
             NAME: ${NAME}
             WORKLOAD_NAME: hello-world
diff --git a/internal/compose/convert_test.go b/internal/compose/convert_test.go
index 5739927..f08967a 100644
--- a/internal/compose/convert_test.go
+++ b/internal/compose/convert_test.go
@@ -156,7 +156,7 @@ func TestScoreConvert(t *testing.T) {
 						Image:    "busybox",
 						Environment: compose.MappingWithEquals{
 							"DEBUG":             stringPtr("${DEBUG}"),
-							"LOGS_LEVEL":        stringPtr("${LOGS_LEVEL}"),
+							"LOGS_LEVEL":        stringPtr("$${LOGS_LEVEL}"),
 							"DOMAIN_NAME":       stringPtr("${SOME_DNS_DOMAIN_NAME?required}"),
 							"CONNECTION_STRING": stringPtr("postgresql://${APP_DB_HOST?required}:${APP_DB_PORT?required}/${APP_DB_NAME?required}"),
 						},
@@ -229,7 +229,7 @@ func TestScoreConvert(t *testing.T) {
 						Image:    "busybox",
 						Environment: compose.MappingWithEquals{
 							"DEBUG":             stringPtr("${DEBUG}"),
-							"LOGS_LEVEL":        stringPtr("${LOGS_LEVEL}"),
+							"LOGS_LEVEL":        stringPtr("$${LOGS_LEVEL}"),
 							"DOMAIN_NAME":       stringPtr("${SOME_DNS_DOMAIN_NAME?required}"),
 							"CONNECTION_STRING": stringPtr("mysql://${APP_DB_HOST?required}:${APP_DB_PORT?required}/${APP_DB_NAME?required}"),
 						},