Repository: score-spec/score-compose
Pull request #103 (2024-04-05 07:57:02)

fix: upgrade score-go and move special 2-part volume placeholder impl
There was previously a special case in score-go for converting 2 part placeholders like `${resources.foo}` into just "foo". This was a legacy behavior due to the way volumes are linked into containers. However this was a behavior only needed by score-compose and incompatible with other score implementations. So in 1.5.1 of score-go we removed the special casing and now throw the original error and move the handling of this field specifically into the volume field substitution in score-compose. This means we restrict this special casing into only the particular field and resource type combination that need it 👍 

Going forwards the `${resources.foo}` placeholder will throw an error in most cases because it expects an output lookup key. Certain resources implemented by output-lookup-functions may now handle the 2-part lookup specifically, but this is not a limited case rather than general behavior.




Patch:
diff --git a/go.mod b/go.mod
index 7894119..6686bfa 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/compose-spec/compose-go/v2 v2.0.0-rc.8
 	github.com/imdario/mergo v0.3.13
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/score-spec/score-go v1.5.0
+	github.com/score-spec/score-go v1.5.1
 	github.com/spf13/cobra v1.6.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index 1fcdd5d..5030082 100644
--- a/go.sum
+++ b/go.sum
@@ -49,8 +49,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
-github.com/score-spec/score-go v1.5.0 h1:5IybImptuFomQghh4Zbyux9ZU5uTBYvVYw2DVcdo70c=
-github.com/score-spec/score-go v1.5.0/go.mod h1:3QSPH5QVMIX7FdhktLLFtjLQTL1/ENqrWDe5lSdZGFc=
+github.com/score-spec/score-go v1.5.1 h1:0HuNihM/bDA6z5CPYQlVN0P4V0/AkfLT8DjDxnkZ03Q=
+github.com/score-spec/score-go v1.5.1/go.mod h1:3QSPH5QVMIX7FdhktLLFtjLQTL1/ENqrWDe5lSdZGFc=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
diff --git a/internal/compose/convert.go b/internal/compose/convert.go
index ba2e9aa..605350f 100644
--- a/internal/compose/convert.go
+++ b/internal/compose/convert.go
@@ -84,25 +84,25 @@ func ConvertSpec(state *project.State, spec *score.Workload) (*compose.Project,
 					return nil, fmt.Errorf("containers.%s.volumes[%d].path: can't mount named volume with sub path '%s': not supported", containerName, idx, *vol.Path)
 				}
 
-				resolvedVolumeSource, err := framework.SubstituteString(vol.Source, substitutionFunction)
+				// The way volumes are linked to a resource is a bit of a special case. The goal is to confirm that the
+				// resource exists and is a volume. We then extract the source property. This only applies to the pattern
+				// of ${resources.my-volume} which can also be written as ${resources.my-volume.source}.
+				resolvedVolumeSource, err := framework.SubstituteString(vol.Source, func(ref string) (string, error) {
+					if parts := framework.SplitRefParts(ref); len(parts) == 2 && parts[0] == "resources" {
+						resName := parts[1]
+						if res, ok := spec.Resources[resName]; !ok {
+							return "", fmt.Errorf("containers.%s.volumes[%d].source: resource '%s' does not exist", containerName, idx, resName)
+						} else if res.Type != "volume" {
+							return "", fmt.Errorf("containers.%s.volumes[%d].source: resource '%s' is not a volume", containerName, idx, resName)
+						}
+						ref += ".source"
+					}
+					return substitutionFunction(ref)
+				})
 				if err != nil {
 					return nil, fmt.Errorf("containers.%s.volumes[%d].source: %w", containerName, idx, err)
 				}
 
-				if res, ok := spec.Resources[resolvedVolumeSource]; !ok {
-					return nil, fmt.Errorf("containers.%s.volumes[%d].source: resource '%s' does not exist", containerName, idx, resolvedVolumeSource)
-				} else if res.Type != "volume" {
-					return nil, fmt.Errorf("containers.%s.volumes[%d].source: resource '%s' is not a volume", containerName, idx, resolvedVolumeSource)
-				}
-
-				if outputFunc, ok := resourceOutputs[resolvedVolumeSource]; ok {
-					if v, err := outputFunc("source"); err != nil {
-						slog.Warn(fmt.Sprintf("containers.%s.volumes[%d].source: failed to find 'source' key in volume resource '%s': %v", containerName, idx, resolvedVolumeSource, err))
-					} else if sv, ok := v.(string); ok {
-						resolvedVolumeSource = sv
-					}
-				}
-
 				volumes[idx] = compose.ServiceVolumeConfig{
 					Type:     "volume",
 					Source:   resolvedVolumeSource,


Test patch:
diff --git a/internal/compose/convert_test.go b/internal/compose/convert_test.go
index e91e128..d33ebcb 100644
--- a/internal/compose/convert_test.go
+++ b/internal/compose/convert_test.go
@@ -252,7 +252,7 @@ func TestScoreConvert(t *testing.T) {
 				Containers: score.WorkloadContainers{
 					"test": score.Container{
 						Image:   "busybox",
-						Volumes: []score.ContainerVolumesElem{{Source: "data", Target: "/mnt/data"}},
+						Volumes: []score.ContainerVolumesElem{{Source: "${resources.data}", Target: "/mnt/data"}},
 					},
 				},
 			},
@@ -266,7 +266,7 @@ func TestScoreConvert(t *testing.T) {
 				Containers: score.WorkloadContainers{
 					"test": score.Container{
 						Image:   "busybox",
-						Volumes: []score.ContainerVolumesElem{{Source: "data", Target: "/mnt/data"}},
+						Volumes: []score.ContainerVolumesElem{{Source: "${resources.data}", Target: "/mnt/data"}},
 					},
 				},
 				Resources: map[string]score.Resource{"data": {Type: "thing"}},