Repository: score-spec/score-compose
Pull request #34 (2023-06-05 14:30:41)

removed properties from score spec
<!--- Provide a general summary of your changes in the Title above -->

#### Description
We are planning to deprecate resource properties in the Score spec: https://github.com/score-spec/spec/issues/61

#### What does this PR do?
It removes resource properties from the example score.yaml files we store in this repo

#### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] New chore (expected functionality to be implemented)

#### Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My change requires a change to the documentation.
  - [x] I have updated the documentation accordingly.
- [x] I've signed off with an email address that matches the commit author.



Patch:
diff --git a/README.md b/README.md
index 94ca7b1..d9c11b2 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ score-compose run -f /tmp/score.yaml -o /tmp/compose.yaml
 
 - `run` tells the CLI to translate the Score file to a Docker Compose file.
 - `-f` is the path to the Score file.
-- `--env` specifies the path to the output file.
+- `-o` specifies the path to the output file.
 
 If you're just getting started, follow [this guide](https://docs.score.dev/docs/get-started/score-compose-hello-world/) to run your first Hello World program with `score-compose`.
 
diff --git a/examples/02-environment/README.md b/examples/02-environment/README.md
index 8d0a05a..7f2ddfc 100644
--- a/examples/02-environment/README.md
+++ b/examples/02-environment/README.md
@@ -21,10 +21,6 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
 ```
 
 To convert `score.yaml` file into runnable `compose.yaml` use a `score-compose` CLI tool:
@@ -44,7 +40,7 @@ services:
     entrypoint:
       - /bin/sh
     environment:
-      FRIEND: ${NAME-World}
+      FRIEND: ${NAME}
     image: busybox
 ```
 
@@ -74,7 +70,7 @@ $ score-compose run -f ./score.yaml -o ./compose.yaml --env-file ./.env
 For the example above the `.env` file would include only one variable:
 
 ```yaml
-NAME=World
+NAME=
 ```
 
 Once the `.env` is populated with all the values (usually such file is generated automatically by the configuration managements system or with the CI/CD automation scripts), it can be fed to `docker-compose`:
diff --git a/examples/02-environment/score.yaml b/examples/02-environment/score.yaml
index 3a1efd8..5785c77 100644
--- a/examples/02-environment/score.yaml
+++ b/examples/02-environment/score.yaml
@@ -11,7 +11,3 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
diff --git a/examples/03-dependencies/README.md b/examples/03-dependencies/README.md
index 22c22b1..0ef56af 100644
--- a/examples/03-dependencies/README.md
+++ b/examples/03-dependencies/README.md
@@ -22,23 +22,8 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
   db:
     type: postgres
-    properties:
-      host:
-        default: localhost
-      port:
-        default: 5432
-      name:
-        default: postgres
-      user:
-        secret: true
-      password:
-        secret: true
   service-b:
     type: service
 ```
@@ -62,10 +47,6 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
 ```
 
 To prepare executable `docker-compose` configuration files, convert both score files with `score-compose` CLI tool:
diff --git a/examples/03-dependencies/service-a.yaml b/examples/03-dependencies/service-a.yaml
index bfef595..ef2b182 100644
--- a/examples/03-dependencies/service-a.yaml
+++ b/examples/03-dependencies/service-a.yaml
@@ -15,22 +15,7 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
   db:
     type: postgres
-    properties:
-      host:
-        default: localhost
-      port:
-        default: 5432
-      name:
-        default: postgres
-      user:
-        secret: true
-      password:
-        secret: true
   service-b:
     type: service
diff --git a/examples/03-dependencies/service-b.yaml b/examples/03-dependencies/service-b.yaml
index 638fade..15de397 100644
--- a/examples/03-dependencies/service-b.yaml
+++ b/examples/03-dependencies/service-b.yaml
@@ -14,7 +14,3 @@ containers:
 resources:
   env:
     type: environment
-    properties:
-      NAME:
-        type: string
-        default: World
diff --git a/go.mod b/go.mod
index dfc446d..b2dff71 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,10 @@ require (
 	github.com/compose-spec/compose-go v1.6.0
 	github.com/imdario/mergo v0.3.13
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/score-spec/score-go v0.0.0-20230601114155-58fa99cb56f8
+	github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1
 	github.com/spf13/cobra v1.6.0
 	github.com/stretchr/testify v1.8.0
+	github.com/tidwall/sjson v1.2.5
 	github.com/xeipuuv/gojsonschema v1.2.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -21,6 +22,9 @@ require (
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/tidwall/gjson v1.14.4 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.1 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
diff --git a/go.sum b/go.sum
index 64a417f..00f30e0 100644
--- a/go.sum
+++ b/go.sum
@@ -22,8 +22,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/score-spec/score-go v0.0.0-20230601114155-58fa99cb56f8 h1:gIOGix8DrqtGbEEBPimUON83Bk+AVnZRpanCKsWXa3s=
-github.com/score-spec/score-go v0.0.0-20230601114155-58fa99cb56f8/go.mod h1:kqDzGrkDasa4D1A9MWgHPVPoRVa+zZgFijYOZNDLSpM=
+github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1 h1:i/6Z1cPwKOEJp0jNK3oyh1RCWnGR1Cn4cAorRdCIe2M=
+github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1/go.mod h1:kqDzGrkDasa4D1A9MWgHPVPoRVa+zZgFijYOZNDLSpM=
 github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
 github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -34,6 +34,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
diff --git a/internal/command/run.go b/internal/command/run.go
index e8f9edb..4d7a652 100644
--- a/internal/command/run.go
+++ b/internal/command/run.go
@@ -8,17 +8,21 @@ The Apache Software Foundation (http://www.apache.org/).
 package command
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
 	"log"
 	"os"
 	"sort"
+	"strings"
 
 	"github.com/compose-spec/compose-go/types"
 	"github.com/imdario/mergo"
 	"github.com/spf13/cobra"
+	"github.com/tidwall/sjson"
 	"github.com/xeipuuv/gojsonschema"
+	"gopkg.in/yaml.v3"
 
 	"github.com/score-spec/score-compose/internal/compose"
 
@@ -39,6 +43,8 @@ var (
 	envFile       string
 	buildCtx      string
 
+	overrideParams []string
+
 	skipValidation bool
 	verbose        bool
 )
@@ -50,6 +56,8 @@ func init() {
 	runCmd.Flags().StringVar(&envFile, "env-file", "", "Location to store sample .env file")
 	runCmd.Flags().StringVar(&buildCtx, "build", "", "Replaces 'image' name with compose 'build' instruction")
 
+	runCmd.Flags().StringArrayVarP(&overrideParams, "property", "p", nil, "Overrides selected property value")
+
 	runCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "DEPRECATED: Disables Score file schema validation")
 	runCmd.Flags().BoolVar(&verbose, "verbose", false, "Enable diagnostic messages (written to STDERR)")
 
@@ -85,7 +93,7 @@ func run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Apply overrides (optional)
+	// Apply overrides from file (optional)
 	//
 	if overridesFile != "" {
 		log.Printf("Checking '%s'...\n", overridesFile)
@@ -105,6 +113,41 @@ func run(cmd *cobra.Command, args []string) error {
 		}
 	}
 
+	// Apply overrides from command line (optional)
+	//
+	for _, pstr := range overrideParams {
+		log.Print("Applying SCORE properties overrides...\n")
+
+		jsonBytes, err := json.Marshal(srcMap)
+		if err != nil {
+			return fmt.Errorf("marshalling score spec: %w", err)
+		}
+
+		pmap := strings.SplitN(pstr, "=", 2)
+		if len(pmap) <= 1 {
+			var path = pmap[0]
+			log.Printf("removing '%s'", path)
+			if jsonBytes, err = sjson.DeleteBytes(jsonBytes, path); err != nil {
+				return fmt.Errorf("removing '%s': %w", path, err)
+			}
+		} else {
+			var path = pmap[0]
+			var val interface{}
+			if err := yaml.Unmarshal([]byte(pmap[1]), &val); err != nil {
+				val = pmap[1]
+			}
+
+			log.Printf("overriding '%s' = '%s' (%T)", path, val, val)
+			if jsonBytes, err = sjson.SetBytes(jsonBytes, path, val); err != nil {
+				return fmt.Errorf("overriding '%s': %w", path, err)
+			}
+		}
+
+		if err = json.Unmarshal(jsonBytes, &srcMap); err != nil {
+			return fmt.Errorf("unmarshalling score spec: %w", err)
+		}
+	}
+
 	// Validate SCORE spec
 	//
 	if !skipValidation {
diff --git a/internal/compose/convert.go b/internal/compose/convert.go
index 483fb78..1663fa8 100644
--- a/internal/compose/convert.go
+++ b/internal/compose/convert.go
@@ -18,16 +18,15 @@ import (
 
 // ConvertSpec converts SCORE specification into docker-compose configuration.
 func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables, error) {
-	context, err := buildContext(spec.Metadata, spec.Resources)
+	ctx, err := buildContext(spec.Metadata, spec.Resources)
 	if err != nil {
 		return nil, nil, fmt.Errorf("preparing context: %w", err)
 	}
 
 	for _, cSpec := range spec.Containers {
-		var externalVars = ExternalVariables(context.ListEnvVars())
 		var env = make(compose.MappingWithEquals, len(cSpec.Variables))
 		for key, val := range cSpec.Variables {
-			var envVarVal = context.Substitute(val)
+			var envVarVal = ctx.Substitute(val)
 			env[key] = &envVarVal
 		}
 
@@ -62,7 +61,7 @@ func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables,
 				}
 				volumes[idx] = compose.ServiceVolumeConfig{
 					Type:     "volume",
-					Source:   context.Substitute(vol.Source),
+					Source:   ctx.Substitute(vol.Source),
 					Target:   vol.Target,
 					ReadOnly: vol.ReadOnly,
 				}
@@ -90,6 +89,8 @@ func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables,
 			},
 		}
 
+		var externalVars = ExternalVariables(ctx.ListEnvVars())
+
 		// NOTE: Only one container per workload can be defined for compose.
 		//       All other containers will be ignored by this tool.
 		return &proj, externalVars, nil
diff --git a/internal/compose/templates.go b/internal/compose/templates.go
index 88aa09a..7f222bd 100644
--- a/internal/compose/templates.go
+++ b/internal/compose/templates.go
@@ -11,7 +11,6 @@ import (
 	"fmt"
 	"log"
 	"os"
-	"regexp"
 	"strings"
 
 	"github.com/mitchellh/mapstructure"
@@ -20,12 +19,16 @@ import (
 )
 
 // templatesContext ia an utility type that provides a context for '${...}' templates substitution
-type templatesContext map[string]string
+type templatesContext struct {
+	meta      map[string]interface{}
+	resources score.ResourcesSpecs
 
-// buildContext initializes a new templatesContext instance
-func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (templatesContext, error) {
-	var ctx = make(map[string]string)
+	// env map is populated dynamically with any refenced variable used by Substitute
+	env map[string]interface{}
+}
 
+// buildContext initializes a new templatesContext instance
+func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (*templatesContext, error) {
 	var metadataMap = make(map[string]interface{})
 	if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
 		TagName: "json",
@@ -34,53 +37,24 @@ func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (
 		return nil, err
 	} else {
 		decoder.Decode(metadata)
-		for key, val := range metadataMap {
-			var ref = fmt.Sprintf("metadata.%s", key)
-			if _, exists := ctx[ref]; exists {
-				return nil, fmt.Errorf("ambiguous property reference '%s'", ref)
-			}
-			ctx[ref] = fmt.Sprintf("%v", val)
-		}
 	}
 
-	for resName, res := range resources {
-		ctx[fmt.Sprintf("resources.%s", resName)] = resName
-
-		for propName, prop := range res.Properties {
-			var ref = fmt.Sprintf("resources.%s.%s", resName, propName)
-
-			var envVar string
-			switch res.Type {
-			case "environment":
-				envVar = strings.ToUpper(propName)
-			default:
-				envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
-			}
-
-			envVar = strings.Replace(envVar, "-", "_", -1)
-			envVar = strings.Replace(envVar, ".", "_", -1)
-
-			if prop.Default != nil {
-				envVar += fmt.Sprintf("-%v", prop.Default)
-			} else if prop.Required {
-				envVar += "?err"
-			}
-
-			ctx[ref] = fmt.Sprintf("${%s}", envVar)
-		}
-	}
+	return &templatesContext{
+		meta:      metadataMap,
+		resources: resources,
 
-	return ctx, nil
+		env: make(map[string]interface{}),
+	}, nil
 }
 
 // Substitute replaces all matching '${...}' templates in a source string
-func (context templatesContext) Substitute(src string) string {
-	return os.Expand(src, context.mapVar)
+func (ctx *templatesContext) Substitute(src string) string {
+	return os.Expand(src, ctx.mapVar)
 }
 
 // MapVar replaces objects and properties references with corresponding values
 // Returns an empty string if the reference can't be resolved
-func (context templatesContext) mapVar(ref string) string {
+func (ctx *templatesContext) mapVar(ref string) string {
 	if ref == "" {
 		return ""
 	}
@@ -94,28 +68,47 @@ func (context templatesContext) mapVar(ref string) string {
 		return ref
 	}
 
-	if res, ok := context[ref]; ok {
-		return res
+	var segments = strings.SplitN(ref, ".", 2)
+	switch segments[0] {
+	case "metadata":
+		if len(segments) == 2 {
+			if val, exists := ctx.meta[segments[1]]; exists {
+				return fmt.Sprintf("%v", val)
+			}
+		}
+
+	case "resources":
+		if len(segments) == 2 {
+			segments = strings.SplitN(segments[1], ".", 2)
+			var resName = segments[0]
+			if res, exists := ctx.resources[resName]; exists {
+				if len(segments) == 1 {
+					return resName
+				} else {
+					var propName = segments[1]
+
+					var envVar string
+					switch res.Type {
+					case "environment":
+						envVar = strings.ToUpper(propName)
+					default:
+						envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
+					}
+					envVar = strings.Replace(envVar, "-", "_", -1)
+					envVar = strings.Replace(envVar, ".", "_", -1)
+
+					ctx.env[envVar] = ""
+					return fmt.Sprintf("${%s}", envVar)
+				}
+			}
+		}
 	}
 
-	log.Printf("Warning: Can not resolve '%s'. Resource or property is not declared.", ref)
+	log.Printf("Warning: Can not resolve '%s' reference.", ref)
 	return ""
 }
 
-// composeEnvVarReferencePattern defines the rule for compose environment variable references
-// Possible documented references for compose v3.5:
-//   - ${ENV_VAR}
-//   - ${ENV_VAR?err}
-//   - ${ENV_VAR-default}
-var envVarPattern = regexp.MustCompile(`\$\{(\w+)(?:\-(.+?)|\?.+)?\}$`)
-
 // ListEnvVars reports all environment variables used by templatesContext
-func (context templatesContext) ListEnvVars() map[string]interface{} {
-	var vars = make(map[string]interface{})
-	for _, ref := range context {
-		if matches := envVarPattern.FindStringSubmatch(ref); len(matches) == 3 {
-			vars[matches[1]] = matches[2]
-		}
-	}
-	return vars
+func (ctx *templatesContext) ListEnvVars() map[string]interface{} {
+	return ctx.env
 }


Test patch:
diff --git a/e2e-tests/resources/outputs/example-02-environment-output.txt b/e2e-tests/resources/outputs/example-02-environment-output.txt
index 89aa5f4..ea4e3d9 100644
--- a/e2e-tests/resources/outputs/example-02-environment-output.txt
+++ b/e2e-tests/resources/outputs/example-02-environment-output.txt
@@ -6,5 +6,5 @@ services:
     entrypoint:
       - /bin/sh
     environment:
-      FRIEND: ${NAME-World}
+      FRIEND: ${NAME}
     image: busybox
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/example-03-dependencies-service-a-output.txt b/e2e-tests/resources/outputs/example-03-dependencies-service-a-output.txt
index 1428683..8425103 100644
--- a/e2e-tests/resources/outputs/example-03-dependencies-service-a-output.txt
+++ b/e2e-tests/resources/outputs/example-03-dependencies-service-a-output.txt
@@ -6,6 +6,6 @@ services:
     entrypoint:
       - /bin/sh
     environment:
-      CONNECTION_STRING: postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST-localhost}:${DB_PORT-5432}/${DB_NAME-postgres}
-      FRIEND: ${NAME-World}
+      CONNECTION_STRING: postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
+      FRIEND: ${NAME}
     image: busybox
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/example-03-dependencies-service-b-output.txt b/e2e-tests/resources/outputs/example-03-dependencies-service-b-output.txt
index 251d679..168b5f7 100644
--- a/e2e-tests/resources/outputs/example-03-dependencies-service-b-output.txt
+++ b/e2e-tests/resources/outputs/example-03-dependencies-service-b-output.txt
@@ -6,5 +6,5 @@ services:
     entrypoint:
       - /bin/sh
     environment:
-      FRIEND: ${NAME-World}
+      FRIEND: ${NAME}
     image: busybox
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/run --help-output.txt b/e2e-tests/resources/outputs/run --help-output.txt
index d8bfb8b..7c1a526 100644
--- a/e2e-tests/resources/outputs/run --help-output.txt
+++ b/e2e-tests/resources/outputs/run --help-output.txt
@@ -4,11 +4,12 @@ Usage:
   score-compose run [flags]
 
 Flags:
-      --build string       Replaces 'image' name with compose 'build' instruction
-      --env-file string    Location to store sample .env file
-  -f, --file string        Source SCORE file (default "./score.yaml")
-  -h, --help               help for run
-  -o, --output string      Output file
-      --overrides string   Overrides SCORE file (default "./overrides.score.yaml")
-      --skip-validation    DEPRECATED: Disables Score file schema validation
-      --verbose            Enable diagnostic messages (written to STDERR)
\ No newline at end of file
+      --build string           Replaces 'image' name with compose 'build' instruction
+      --env-file string        Location to store sample .env file
+  -f, --file string            Source SCORE file (default "./score.yaml")
+  -h, --help                   help for run
+  -o, --output string          Output file
+      --overrides string       Overrides SCORE file (default "./overrides.score.yaml")
+  -p, --property stringArray   Overrides selected property value
+      --skip-validation        DEPRECATED: Disables Score file schema validation
+      --verbose                Enable diagnostic messages (written to STDERR)
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/run --verbose-output.txt b/e2e-tests/resources/outputs/run --verbose-output.txt
index 72bfa90..ed7891e 100644
--- a/e2e-tests/resources/outputs/run --verbose-output.txt
+++ b/e2e-tests/resources/outputs/run --verbose-output.txt
@@ -4,13 +4,14 @@ Usage:
   score-compose run [flags]
 
 Flags:
-      --build string       Replaces 'image' name with compose 'build' instruction
-      --env-file string    Location to store sample .env file
-  -f, --file string        Source SCORE file (default "./score.yaml")
-  -h, --help               help for run
-  -o, --output string      Output file
-      --overrides string   Overrides SCORE file (default "./overrides.score.yaml")
-      --skip-validation    DEPRECATED: Disables Score file schema validation
-      --verbose            Enable diagnostic messages (written to STDERR)
+      --build string           Replaces 'image' name with compose 'build' instruction
+      --env-file string        Location to store sample .env file
+  -f, --file string            Source SCORE file (default "./score.yaml")
+  -h, --help                   help for run
+  -o, --output string          Output file
+      --overrides string       Overrides SCORE file (default "./overrides.score.yaml")
+  -p, --property stringArray   Overrides selected property value
+      --skip-validation        DEPRECATED: Disables Score file schema validation
+      --verbose                Enable diagnostic messages (written to STDERR)
 
 open ./score.yaml: no such file or directory
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/run -f example-score.yaml -p containers.hello.image=hello:1.1-output.txt b/e2e-tests/resources/outputs/run -f example-score.yaml -p containers.hello.image=hello:1.1-output.txt
new file mode 100644
index 0000000..0b4df55
--- /dev/null
+++ b/e2e-tests/resources/outputs/run -f example-score.yaml -p containers.hello.image=hello:1.1-output.txt
@@ -0,0 +1,8 @@
+services:
+  hello-world:
+    command:
+      - -c
+      - while true; do echo Hello World!; sleep 5; done
+    entrypoint:
+      - /bin/sh
+    image: hello:1.1
\ No newline at end of file
diff --git a/e2e-tests/resources/outputs/run-output.txt b/e2e-tests/resources/outputs/run-output.txt
index e18189f..eb7a0e2 100644
--- a/e2e-tests/resources/outputs/run-output.txt
+++ b/e2e-tests/resources/outputs/run-output.txt
@@ -3,13 +3,14 @@ Usage:
   score-compose run [flags]
 
 Flags:
-      --build string       Replaces 'image' name with compose 'build' instruction
-      --env-file string    Location to store sample .env file
-  -f, --file string        Source SCORE file (default "./score.yaml")
-  -h, --help               help for run
-  -o, --output string      Output file
-      --overrides string   Overrides SCORE file (default "./overrides.score.yaml")
-      --skip-validation    DEPRECATED: Disables Score file schema validation
-      --verbose            Enable diagnostic messages (written to STDERR)
+      --build string           Replaces 'image' name with compose 'build' instruction
+      --env-file string        Location to store sample .env file
+  -f, --file string            Source SCORE file (default "./score.yaml")
+  -h, --help                   help for run
+  -o, --output string          Output file
+      --overrides string       Overrides SCORE file (default "./overrides.score.yaml")
+  -p, --property stringArray   Overrides selected property value
+      --skip-validation        DEPRECATED: Disables Score file schema validation
+      --verbose                Enable diagnostic messages (written to STDERR)
 
 open ./score.yaml: no such file or directory
\ No newline at end of file
diff --git a/e2e-tests/resources/score-compose-shared.resource b/e2e-tests/resources/score-compose-shared.resource
index ebd1d02..201d9d8 100644
--- a/e2e-tests/resources/score-compose-shared.resource
+++ b/e2e-tests/resources/score-compose-shared.resource
@@ -64,7 +64,7 @@ Execute score-compose run for 04-extras example for web-app
 
 Verify output of ${example} via docker compose convert
     ${docker_compose_output}    Run
-    ...    docker compose -f ${EXAMPLES_DIR}${example}/compose.yaml convert -q
+    ...    docker compose -f ${EXAMPLES_DIR}${example}/compose.yaml convert -q --no-interpolate
     Should Be Empty    ${docker_compose_output}
     Remove File    ${EXAMPLES_DIR}${example}/compose.yaml
 
diff --git a/e2e-tests/score-compose-cli.robot b/e2e-tests/score-compose-cli.robot
index d9f9633..26c4390 100644
--- a/e2e-tests/score-compose-cli.robot
+++ b/e2e-tests/score-compose-cli.robot
@@ -95,6 +95,9 @@ Verify score-compose run
     Execute score-compose with run -f ${RESOURCES_DIR}example-score.yaml --overrides ${RESOURCES_DIR}overrides.yaml
     Exit code is 0
     Vaildate output
+    Execute score-compose with run -f ${RESOURCES_DIR}example-score.yaml -p containers.hello.image=hello:1.1
+    Exit code is 0
+    Vaildate output
 
 Verify score-compose run (error cases)
     Execute score-compose with run
diff --git a/e2e-tests/utility/generate-expected-outputs.py b/e2e-tests/utility/generate-expected-outputs.py
index 878057f..bd5438a 100644
--- a/e2e-tests/utility/generate-expected-outputs.py
+++ b/e2e-tests/utility/generate-expected-outputs.py
@@ -29,6 +29,7 @@ arglist = [
     "run -f example-score.yaml",
     "run -f example-score.yaml --build test",
     "run -f example-score.yaml --overrides overrides.yaml",
+    "run -f example-score.yaml -p containers.hello.image=hello:1.1",
     "run --verbose",
 ]
 
diff --git a/internal/compose/convert_test.go b/internal/compose/convert_test.go
index 2d2ec2e..5122074 100644
--- a/internal/compose/convert_test.go
+++ b/internal/compose/convert_test.go
@@ -107,7 +107,7 @@ func TestScoreConvert(t *testing.T) {
 						Variables: map[string]string{
 							"DEBUG":             "${resources.env.DEBUG}",
 							"LOGS_LEVEL":        "$${LOGS_LEVEL}",
-							"DOMAIN_NAME":       "${resources.dns.domain_name}",
+							"DOMAIN_NAME":       "${resources.some-dns.domain_name}",
 							"CONNECTION_STRING": "postgresql://${resources.app-db.host}:${resources.app-db.port}/${resources.app-db.name}",
 						},
 						Volumes: []score.VolumeMountSpec{
@@ -122,25 +122,12 @@ func TestScoreConvert(t *testing.T) {
 				Resources: map[string]score.ResourceSpec{
 					"env": {
 						Type: "environment",
-						Properties: map[string]score.ResourcePropertySpec{
-							"DEBUG":      {Default: false, Required: false},
-							"LOGS_LEVEL": {Default: "WARN", Required: false},
-						},
 					},
 					"app-db": {
-						Properties: map[string]score.ResourcePropertySpec{
-							"host":      {Default: "localhost", Required: true},
-							"port":      {Default: 5432, Required: false},
-							"name":      {Required: true},
-							"user.name": {Required: true, Secret: true},
-							"password":  {Required: true, Secret: true},
-						},
+						Type: "postgress",
 					},
 					"dns": {
 						Type: "dns",
-						Properties: map[string]score.ResourcePropertySpec{
-							"domain": {Required: false},
-						},
 					},
 					"data": {
 						Type: "volume",
@@ -153,10 +140,10 @@ func TestScoreConvert(t *testing.T) {
 						Name:  "test",
 						Image: "busybox",
 						Environment: compose.MappingWithEquals{
-							"DEBUG":             stringPtr("${DEBUG-false}"),
+							"DEBUG":             stringPtr("${DEBUG}"),
 							"LOGS_LEVEL":        stringPtr("${LOGS_LEVEL}"),
 							"DOMAIN_NAME":       stringPtr(""),
-							"CONNECTION_STRING": stringPtr("postgresql://${APP_DB_HOST-localhost}:${APP_DB_PORT-5432}/${APP_DB_NAME?err}"),
+							"CONNECTION_STRING": stringPtr("postgresql://${APP_DB_HOST}:${APP_DB_PORT}/${APP_DB_NAME}"),
 						},
 						Volumes: []compose.ServiceVolumeConfig{
 							{
@@ -170,14 +157,10 @@ func TestScoreConvert(t *testing.T) {
 				},
 			},
 			Vars: ExternalVariables{
-				"DEBUG":            "false",
-				"LOGS_LEVEL":       "WARN",
-				"APP_DB_HOST":      "localhost",
-				"APP_DB_PORT":      "5432",
-				"APP_DB_NAME":      "",
-				"APP_DB_USER_NAME": "",
-				"APP_DB_PASSWORD":  "",
-				"DNS_DOMAIN":       "",
+				"DEBUG":       "",
+				"APP_DB_HOST": "",
+				"APP_DB_PORT": "",
+				"APP_DB_NAME": "",
 			},
 		},
 
diff --git a/internal/compose/templates_test.go b/internal/compose/templates_test.go
index d07cce8..84ab2c0 100644
--- a/internal/compose/templates_test.go
+++ b/internal/compose/templates_test.go
@@ -14,7 +14,7 @@ import (
 	assert "github.com/stretchr/testify/assert"
 )
 
-func TestBuildContext(t *testing.T) {
+func TestMapVar(t *testing.T) {
 	var meta = score.WorkloadMeta{
 		Name: "test-name",
 	}
@@ -22,124 +22,73 @@ func TestBuildContext(t *testing.T) {
 	var resources = score.ResourcesSpecs{
 		"env": score.ResourceSpec{
 			Type: "environment",
-			Properties: map[string]score.ResourcePropertySpec{
-				"DEBUG": {Required: false, Default: true},
-			},
 		},
 		"db": score.ResourceSpec{
 			Type: "postgres",
-			Properties: map[string]score.ResourcePropertySpec{
-				"host": {Required: true, Default: "."},
-				"port": {Required: true, Default: "5342"},
-				"name": {Required: true},
-			},
 		},
 	}
 
-	context, err := buildContext(meta, resources)
+	ctx, err := buildContext(meta, resources)
 	assert.NoError(t, err)
 
-	assert.Equal(t, templatesContext{
-		"metadata.name": "test-name",
-
-		"resources.env":       "env",
-		"resources.env.DEBUG": "${DEBUG-true}",
-
-		"resources.db":      "db",
-		"resources.db.host": "${DB_HOST-.}",
-		"resources.db.port": "${DB_PORT-5342}",
-		"resources.db.name": "${DB_NAME?err}",
-	}, context)
-}
-
-func TestMapVar(t *testing.T) {
-	var context = templatesContext{
-		"metadata.name": "test-name",
-
-		"resources.env":       "env",
-		"resources.env.DEBUG": "${DEBUG-true}",
+	assert.Equal(t, "", ctx.mapVar(""))
+	assert.Equal(t, "$", ctx.mapVar("$"))
 
-		"resources.db":      "db",
-		"resources.db.host": "${DB_HOST-.}",
-		"resources.db.port": "${DB_PORT-5342}",
-		"resources.db.name": "${DB_NAME?err}",
-	}
+	assert.Equal(t, "test-name", ctx.mapVar("metadata.name"))
+	assert.Equal(t, "", ctx.mapVar("metadata.name.nil"))
+	assert.Equal(t, "", ctx.mapVar("metadata.nil"))
 
-	assert.Equal(t, "", context.mapVar(""))
-	assert.Equal(t, "$", context.mapVar("$"))
+	assert.Equal(t, "${DEBUG}", ctx.mapVar("resources.env.DEBUG"))
 
-	assert.Equal(t, "test-name", context.mapVar("metadata.name"))
-	assert.Equal(t, "", context.mapVar("metadata.name.nil"))
-	assert.Equal(t, "", context.mapVar("metadata.nil"))
+	assert.Equal(t, "db", ctx.mapVar("resources.db"))
+	assert.Equal(t, "${DB_HOST}", ctx.mapVar("resources.db.host"))
+	assert.Equal(t, "${DB_PORT}", ctx.mapVar("resources.db.port"))
+	assert.Equal(t, "${DB_NAME}", ctx.mapVar("resources.db.name"))
+	assert.Equal(t, "${DB_NAME_USER}", ctx.mapVar("resources.db.name.user"))
 
-	assert.Equal(t, "${DEBUG-true}", context.mapVar("resources.env.DEBUG"))
-
-	assert.Equal(t, "db", context.mapVar("resources.db"))
-	assert.Equal(t, "${DB_HOST-.}", context.mapVar("resources.db.host"))
-	assert.Equal(t, "${DB_PORT-5342}", context.mapVar("resources.db.port"))
-	assert.Equal(t, "${DB_NAME?err}", context.mapVar("resources.db.name"))
-	assert.Equal(t, "", context.mapVar("resources.db.name.nil"))
-	assert.Equal(t, "", context.mapVar("resources.db.nil"))
-	assert.Equal(t, "", context.mapVar("resources.nil"))
-	assert.Equal(t, "", context.mapVar("nil.db.name"))
+	assert.Equal(t, "", ctx.mapVar("resources.nil"))
+	assert.Equal(t, "", ctx.mapVar("nil.db.name"))
 }
 
 func TestSubstitute(t *testing.T) {
-	var context = templatesContext{
-		"metadata.name": "test-name",
-
-		"resources.env":       "env",
-		"resources.env.DEBUG": "${DEBUG-true}",
-
-		"resources.db":      "db",
-		"resources.db.host": "${DB_HOST-.}",
-		"resources.db.port": "${DB_PORT-5342}",
-		"resources.db.name": "${DB_NAME?err}",
+	var meta = score.WorkloadMeta{
+		Name: "test-name",
 	}
 
-	assert.Equal(t, "", context.Substitute(""))
-	assert.Equal(t, "abc", context.Substitute("abc"))
-	assert.Equal(t, "abc $ abc", context.Substitute("abc $$ abc"))
-	assert.Equal(t, "${abc}", context.Substitute("$${abc}"))
-
-	assert.Equal(t, "The name is 'test-name'", context.Substitute("The name is '${metadata.name}'"))
-	assert.Equal(t, "The name is ''", context.Substitute("The name is '${metadata.nil}'"))
-
-	assert.Equal(t, "resources.env.DEBUG", context.Substitute("resources.env.DEBUG"))
+	var resources = score.ResourcesSpecs{
+		"env": score.ResourceSpec{
+			Type: "environment",
+		},
+		"db": score.ResourceSpec{
+			Type: "postgres",
+		},
+	}
 
-	assert.Equal(t, "db", context.Substitute("${resources.db}"))
-	assert.Equal(t,
-		"postgresql://:@${DB_HOST-.}:${DB_PORT-5342}/${DB_NAME?err}",
-		context.Substitute("postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}"))
-}
+	ctx, err := buildContext(meta, resources)
+	assert.NoError(t, err)
 
-func TestEnvVarPattern(t *testing.T) {
-	assert.Equal(t, []string(nil), envVarPattern.FindStringSubmatch(""))
-	assert.Equal(t, []string(nil), envVarPattern.FindStringSubmatch("ENV_VAR"))
-	assert.Equal(t, []string(nil), envVarPattern.FindStringSubmatch("${}"))
+	assert.Empty(t, ctx.ListEnvVars())
 
-	assert.Equal(t, []string{"${ENV_VAR}", "ENV_VAR", ""}, envVarPattern.FindStringSubmatch("${ENV_VAR}"))
-	assert.Equal(t, []string{"${ENV_VAR?err}", "ENV_VAR", ""}, envVarPattern.FindStringSubmatch("${ENV_VAR?err}"))
-	assert.Equal(t, []string{"${ENV_VAR-default}", "ENV_VAR", "default"}, envVarPattern.FindStringSubmatch("${ENV_VAR-default}"))
-}
+	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}"))
 
-func TestListEnvVars(t *testing.T) {
-	var context = templatesContext{
-		"metadata.name": "test-name",
+	assert.Equal(t, "The name is 'test-name'", ctx.Substitute("The name is '${metadata.name}'"))
+	assert.Equal(t, "The name is ''", ctx.Substitute("The name is '${metadata.nil}'"))
 
-		"resources.env":       "env",
-		"resources.env.DEBUG": "${DEBUG-true}",
+	assert.Equal(t, "resources.badref.DEBUG", ctx.Substitute("resources.badref.DEBUG"))
 
-		"resources.db":      "db",
-		"resources.db.host": "${DB_HOST-.}",
-		"resources.db.port": "${DB_PORT-5342}",
-		"resources.db.name": "${DB_NAME?err}",
-	}
+	assert.Equal(t, "db", ctx.Substitute("${resources.db}"))
+	assert.Equal(t,
+		"postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}",
+		ctx.Substitute("postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}"))
 
 	assert.Equal(t, map[string]interface{}{
-		"DEBUG":   "true",
-		"DB_HOST": ".",
-		"DB_PORT": "5342",
-		"DB_NAME": "",
-	}, context.ListEnvVars())
+		"DB_USER":     "",
+		"DB_PASSWORD": "",
+		"DB_HOST":     "",
+		"DB_PORT":     "",
+		"DB_NAME":     "",
+	}, ctx.ListEnvVars())
 }