From 36f72619b589ce9f3abaeed24a44a1e3951f0bbb Mon Sep 17 00:00:00 2001 From: Ben Meier Date: Tue, 27 Feb 2024 09:38:54 +0000 Subject: [PATCH] fix: remove double printing of errors and supress usage text when errors are present after parsing Signed-off-by: Ben Meier --- README.md | 20 ++- cmd/score-compose/main.go | 2 +- .../resources/outputs/run --help-output.txt | 2 +- .../outputs/run --verbose-output.txt | 18 +-- e2e-tests/resources/outputs/run-output.txt | 17 +- .../resources/outputs/unknown-output.txt | 4 +- internal/command/root.go | 2 + internal/command/run.go | 9 +- internal/command/run_test.go | 148 ++++++++++++++++++ 9 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 internal/command/run_test.go diff --git a/README.md b/README.md index c76f13c..fa65bc8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,25 @@ score-compose run -f /tmp/score.yaml -o /tmp/compose.yaml - `-f` is the path to the Score 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`. +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`. The full usage of the `run` command is: + +``` +Translate the SCORE file to docker-compose configuration + +Usage: + score-compose run [--file=score.yaml] [--output=compose.yaml] [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") + -p, --property stringArray Overrides selected property value + --skip-validation DEPRECATED: Disables Score file schema validation + --verbose Enable diagnostic messages (written to STDERR) +``` ## ![Get involved](docs/images/get-involved.svg) Get involved diff --git a/cmd/score-compose/main.go b/cmd/score-compose/main.go index a07048a..99408d1 100644 --- a/cmd/score-compose/main.go +++ b/cmd/score-compose/main.go @@ -16,7 +16,7 @@ import ( func main() { if err := command.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, "Error: "+err.Error()) os.Exit(1) } } diff --git a/e2e-tests/resources/outputs/run --help-output.txt b/e2e-tests/resources/outputs/run --help-output.txt index 7c1a526..f168c89 100644 --- a/e2e-tests/resources/outputs/run --help-output.txt +++ b/e2e-tests/resources/outputs/run --help-output.txt @@ -1,7 +1,7 @@ Translate the SCORE file to docker-compose configuration Usage: - score-compose run [flags] + score-compose run [--file=score.yaml] [--output=compose.yaml] [flags] Flags: --build string Replaces 'image' name with compose 'build' instruction diff --git a/e2e-tests/resources/outputs/run --verbose-output.txt b/e2e-tests/resources/outputs/run --verbose-output.txt index ed7891e..45c6c4e 100644 --- a/e2e-tests/resources/outputs/run --verbose-output.txt +++ b/e2e-tests/resources/outputs/run --verbose-output.txt @@ -1,17 +1 @@ -'./score.yaml'... -Error: open ./score.yaml: no such file or directory -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") - -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 +Error: open ./score.yaml: no such file or directory \ 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 eb7a0e2..45c6c4e 100644 --- a/e2e-tests/resources/outputs/run-output.txt +++ b/e2e-tests/resources/outputs/run-output.txt @@ -1,16 +1 @@ -Error: open ./score.yaml: no such file or directory -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") - -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 +Error: open ./score.yaml: no such file or directory \ No newline at end of file diff --git a/e2e-tests/resources/outputs/unknown-output.txt b/e2e-tests/resources/outputs/unknown-output.txt index 04a2e28..006fe2c 100644 --- a/e2e-tests/resources/outputs/unknown-output.txt +++ b/e2e-tests/resources/outputs/unknown-output.txt @@ -1,3 +1 @@ -Error: unknown command "unknown" for "score-compose" -Run 'score-compose --help' for usage. -unknown command "unknown" for "score-compose" \ No newline at end of file +Error: unknown command "unknown" for "score-compose" \ No newline at end of file diff --git a/internal/command/root.go b/internal/command/root.go index 21e9059..de2c746 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -23,6 +23,8 @@ var ( This tool produces a docker-compose configuration file from the SCORE specification. Complete documentation is available at https://score.dev`, Version: fmt.Sprintf("%s (build: %s; sha: %s)", version.Version, version.BuildTime, version.GitSHA), + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, } ) diff --git a/internal/command/run.go b/internal/command/run.go index 2c412cb..4b872a7 100644 --- a/internal/command/run.go +++ b/internal/command/run.go @@ -63,12 +63,17 @@ func init() { } var runCmd = &cobra.Command{ - Use: "run", + Use: "run [--file=score.yaml] [--output=compose.yaml]", Short: "Translate the SCORE file to docker-compose configuration", RunE: run, + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, } func run(cmd *cobra.Command, args []string) error { + // Silence usage message if args are parsed correctly + cmd.SilenceUsage = true + if !verbose { log.SetOutput(io.Discard) } @@ -185,7 +190,7 @@ func run(cmd *cobra.Command, args []string) error { // Open output file (optional) // - var dest = io.Writer(os.Stdout) + var dest = cmd.OutOrStdout() if outFile != "" { log.Printf("Creating '%s'...\n", outFile) destFile, err := os.Create(outFile) diff --git a/internal/command/run_test.go b/internal/command/run_test.go new file mode 100644 index 0000000..f996cde --- /dev/null +++ b/internal/command/run_test.go @@ -0,0 +1,148 @@ +package command + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// executeAndResetCommand is a test helper that runs and then resets a command for executing in another test. +func executeAndResetCommand(ctx context.Context, cmd *cobra.Command, args []string) (string, string, error) { + beforeOut, beforeErr := cmd.OutOrStderr(), cmd.ErrOrStderr() + defer func() { + cmd.SetOut(beforeOut) + cmd.SetErr(beforeErr) + }() + + nowOut, nowErr := new(bytes.Buffer), new(bytes.Buffer) + cmd.SetOut(nowOut) + cmd.SetErr(nowErr) + cmd.SetArgs(args) + subCmd, err := cmd.ExecuteContextC(ctx) + if subCmd != nil { + subCmd.SetContext(nil) + subCmd.SilenceUsage = false + subCmd.SilenceErrors = false + subCmd.Flags().VisitAll(func(f *pflag.Flag) { + _ = f.Value.Set(f.DefValue) + }) + } + return nowOut.String(), nowErr.String(), err +} + +func TestExample(t *testing.T) { + td := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-workload-name123 + extra-key: extra-value +service: + ports: + port-one: + port: 1000 + protocol: TCP + targetPort: 10000 + port-two2: + port: 8000 +containers: + container-one1: + image: localhost:4000/repo/my-image:tag + command: ["/bin/sh", "-c"] + args: ["hello", "world"] + resources: + requests: + cpu: 1000m + memory: 10Gi + limits: + cpu: "0.24" + memory: 128M + variables: + SOME_VAR: some content here + volumes: + - source: volume-name + target: /mnt/something + readOnly: false + - source: volume-two + target: /mnt/something-else + livenessProbe: + httpGet: + port: 8080 + path: /livez + readinessProbe: + httpGet: + host: 127.0.0.1 + port: 80 + scheme: HTTP + path: /readyz + httpHeaders: + - name: SOME_HEADER + value: some-value-here + container-two2: + image: localhost:4000/repo/my-image:tag +resources: + resource-one1: + metadata: + annotations: + Default-Annotation: this is my annotation + prefix.com/Another-Key_Annotation.2: something else + extra-key: extra-value + type: Resource-One + class: default + params: + extra: + data: here + resource-two2: + type: Resource-Two +`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) + assert.NoError(t, err) + assert.NotEqual(t, "", stdout) + assert.Equal(t, "", stderr) + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + var actualComposeContent map[string]interface{} + assert.NoError(t, yaml.Unmarshal(rawComposeContent, &actualComposeContent)) + assert.Equal(t, map[string]interface{}{ + "services": map[string]interface{}{ + "example-workload-name123-container-one1": map[string]interface{}{ + "image": "localhost:4000/repo/my-image:tag", + "entrypoint": []interface{}{"/bin/sh", "-c"}, + "command": []interface{}{"hello", "world"}, + "environment": map[string]interface{}{ + "SOME_VAR": "some content here", + }, + "ports": []interface{}{ + map[string]interface{}{"target": 10000, "published": "1000", "protocol": "TCP"}, + map[string]interface{}{"target": 8000, "published": "8000"}, + }, + "volumes": []interface{}{ + map[string]interface{}{"type": "volume", "source": "volume-name", "target": "/mnt/something"}, + map[string]interface{}{"type": "volume", "source": "volume-two", "target": "/mnt/something-else"}, + }, + }, + "example-workload-name123-container-two2": map[string]interface{}{ + "image": "localhost:4000/repo/my-image:tag", + "network_mode": "service:example-workload-name123-container-one1", + }, + }, + }, actualComposeContent) +} + +func TestExample_invalid_spec(t *testing.T) { + td := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +{}`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) + assert.EqualError(t, err, "validating workload spec: jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'") + assert.Equal(t, "", stdout) + assert.Equal(t, "", stderr) +}