Repository: score-spec/score-compose
Pull request #131 (2024-05-03 09:42:21)

fix(generate): correct parsing of --build json
See #130. This corrects the parsing of the `--build` json by using the compose yaml/json loader which pulls in all the extra unmarshalling magic for dynamic types that the json reader cannot cope with.

A test is added to check that this works as expected.

As a side effect, this also supports inline yaml.



Patch:
diff --git a/README.md b/README.md
index 2ec98ae..5c82e2c 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,7 @@ Examples:
   score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value
 
 Flags:
-      --build stringArray               An optional build context to use for the given container --build=container=./dir or --build=container={'"context":"./dir"}
+      --build stringArray               An optional build context to use for the given container --build=container=./dir or --build=container={"context":"./dir"}
       --env-file string                 Location to store a skeleton .env file for compose - this will override existing content
   -h, --help                            help for generate
       --image string                    An optional container image to use for any container with image == '.'
diff --git a/internal/command/generate.go b/internal/command/generate.go
index bd4d955..eec505b 100644
--- a/internal/command/generate.go
+++ b/internal/command/generate.go
@@ -16,13 +16,13 @@ package command
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"log/slog"
 	"os"
 	"slices"
 	"strings"
 
+	composeloader "github.com/compose-spec/compose-go/v2/loader"
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/imdario/mergo"
 	"github.com/score-spec/score-go/framework"
@@ -158,10 +158,12 @@ arguments.
 						return fmt.Errorf("invalid --%s '%s': unknown container '%s'", generateCmdBuildFlag, buildFlag, parts[0])
 					}
 					if strings.HasPrefix(parts[1], "{") {
+						var intermediate interface{}
+						if err := yaml.Unmarshal([]byte(parts[1]), &intermediate); err != nil {
+							return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err)
+						}
 						var out types.BuildConfig
-						dec := json.NewDecoder(strings.NewReader(parts[1]))
-						dec.DisallowUnknownFields()
-						if err := dec.Decode(&out); err != nil {
+						if err := composeloader.Transform(intermediate, &out); err != nil {
 							return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err)
 						}
 						containerBuildContexts[parts[0]] = out
@@ -327,7 +329,7 @@ func init() {
 	generateCommand.Flags().String(generateCmdOverridesFileFlag, "", "An optional file of Score overrides to merge in")
 	generateCommand.Flags().StringArray(generateCmdOverridePropertyFlag, []string{}, "An optional set of path=key overrides to set or remove")
 	generateCommand.Flags().String(generateCmdImageFlag, "", "An optional container image to use for any container with image == '.'")
-	generateCommand.Flags().StringArray(generateCmdBuildFlag, []string{}, "An optional build context to use for the given container --build=container=./dir or --build=container={'\"context\":\"./dir\"}")
+	generateCommand.Flags().StringArray(generateCmdBuildFlag, []string{}, "An optional build context to use for the given container --build=container=./dir or --build=container={\"context\":\"./dir\"}")
 	generateCommand.Flags().String(generateCmdEnvFileFlag, "", "Location to store a skeleton .env file for compose - this will override existing content")
 	rootCmd.AddCommand(generateCommand)
 }


Test patch:
diff --git a/internal/command/generate_test.go b/internal/command/generate_test.go
index 1b3e8d2..4bdb7b3 100644
--- a/internal/command/generate_test.go
+++ b/internal/command/generate_test.go
@@ -53,7 +53,7 @@ Examples:
   score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value
 
 Flags:
-      --build stringArray               An optional build context to use for the given container --build=container=./dir or --build=container={'"context":"./dir"}
+      --build stringArray               An optional build context to use for the given container --build=container=./dir or --build=container={"context":"./dir"}
       --env-file string                 Location to store a skeleton .env file for compose - this will override existing content
   -h, --help                            help for generate
       --image string                    An optional container image to use for any container with image == '.'
@@ -270,6 +270,44 @@ services:
 		assert.Equal(t, expectedOutput, string(raw))
 	})
 
+	t.Run("generate with json build context and array args", func(t *testing.T) {
+		stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{
+			"generate", "-o", "compose-output.yaml", "--build", `example={"context":"./dir","args":["DEBUG"]}`, "--", "score.yaml",
+		})
+		assert.NoError(t, err)
+		assert.Equal(t, "", stdout)
+		raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml"))
+		assert.NoError(t, err)
+		expectedOutput := `name: "001"
+services:
+    example-example:
+        build:
+            context: ./dir
+            args:
+                DEBUG: null
+        hostname: example
+`
+		assert.Equal(t, expectedOutput, string(raw))
+	})
+
+	t.Run("generate with yaml build context", func(t *testing.T) {
+		stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{
+			"generate", "-o", "compose-output.yaml", "--build", `example={context: "./dir"}`, "--", "score.yaml",
+		})
+		assert.NoError(t, err)
+		assert.Equal(t, "", stdout)
+		raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml"))
+		assert.NoError(t, err)
+		expectedOutput := `name: "001"
+services:
+    example-example:
+        build:
+            context: ./dir
+        hostname: example
+`
+		assert.Equal(t, expectedOutput, string(raw))
+	})
+
 }
 
 func TestInitAndGenerate_with_files(t *testing.T) {