From 14e0bbd1491fb359263761392b9df0d767e38f81 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 8 Feb 2024 11:16:02 +0100 Subject: [PATCH 1/5] Add JSON stream progress writer Signed-off-by: Felix Fontein --- cmd/compose/compose.go | 3 + docs/reference/compose.md | 2 +- docs/reference/docker_compose.yaml | 2 +- docs/reference/docker_compose_build.yaml | 2 +- pkg/progress/json.go | 88 ++++++++++++++++++++++++ pkg/progress/writer.go | 9 +++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 pkg/progress/json.go diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 5332033d79..7162c76081 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -453,6 +453,8 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli ui.Mode = ui.ModePlain case ui.ModeQuiet, "none": ui.Mode = ui.ModeQuiet + case ui.ModeJSON: + ui.Mode = ui.ModeJSON default: return fmt.Errorf("unsupported --progress value %q", opts.Progress) } @@ -603,6 +605,7 @@ var printerModes = []string{ ui.ModeAuto, ui.ModeTTY, ui.ModePlain, + ui.ModeJSON, ui.ModeQuiet, } diff --git a/docs/reference/compose.md b/docs/reference/compose.md index 7770f785a0..b8f10ce8a2 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -51,7 +51,7 @@ Define and run multi-container applications with Docker | `-f`, `--file` | `stringArray` | | Compose configuration files | | `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited | | `--profile` | `stringArray` | | Specify a profile to enable | -| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) | +| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, json, quiet) | | `--project-directory` | `string` | | Specify an alternate working directory
(default: the path of the, first specified, Compose file) | | `-p`, `--project-name` | `string` | | Project name | diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml index 64e5b7a1d2..82c7cd1844 100644 --- a/docs/reference/docker_compose.yaml +++ b/docs/reference/docker_compose.yaml @@ -306,7 +306,7 @@ options: - option: progress value_type: string default_value: auto - description: Set type of progress output (auto, tty, plain, quiet) + description: Set type of progress output (auto, tty, plain, json, quiet) deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_build.yaml b/docs/reference/docker_compose_build.yaml index 1c7c2ecd9e..ade039dc4e 100644 --- a/docs/reference/docker_compose_build.yaml +++ b/docs/reference/docker_compose_build.yaml @@ -99,7 +99,7 @@ options: - option: progress value_type: string default_value: auto - description: Set type of ui output (auto, tty, plain, quiet) + description: Set type of ui output (auto, tty, plain, json, quiet) deprecated: false hidden: true experimental: false diff --git a/pkg/progress/json.go b/pkg/progress/json.go new file mode 100644 index 0000000000..25eaac177d --- /dev/null +++ b/pkg/progress/json.go @@ -0,0 +1,88 @@ +/* + Copyright 2024 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package progress + +import ( + "context" + "encoding/json" + "fmt" + "io" +) + +type jsonWriter struct { + out io.Writer + done chan bool + dryRun bool +} + +type jsonMessage struct { + DryRun bool `json:"dry-run,omitempty"` + Tail bool `json:"tail,omitempty"` + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Status string `json:"status,omitempty"` +} + +func (p *jsonWriter) Start(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-p.done: + return nil + } +} + +func (p *jsonWriter) Event(e Event) { + var message = &jsonMessage{ + DryRun: p.dryRun, + Tail: false, + ID: e.ID, + Text: e.Text, + Status: e.StatusText, + } + marshal, err := json.Marshal(message) + if err == nil { + fmt.Fprintln(p.out, string(marshal)) + } +} + +func (p *jsonWriter) Events(events []Event) { + for _, e := range events { + p.Event(e) + } +} + +func (p *jsonWriter) TailMsgf(msg string, args ...interface{}) { + var message = &jsonMessage{ + DryRun: p.dryRun, + Tail: true, + ID: "", + Text: fmt.Sprintf(msg, args...), + Status: "", + } + marshal, err := json.Marshal(message) + if err == nil { + fmt.Fprintln(p.out, string(marshal)) + } +} + +func (p *jsonWriter) Stop() { + p.done <- true +} + +func (p *jsonWriter) HasMore(bool) { +} diff --git a/pkg/progress/writer.go b/pkg/progress/writer.go index 0f1bd568c4..3f4a74f562 100644 --- a/pkg/progress/writer.go +++ b/pkg/progress/writer.go @@ -107,6 +107,8 @@ const ( ModePlain = "plain" // ModeQuiet don't display events ModeQuiet = "quiet" + // ModeJSON outputs a machine-readable JSON stream + ModeJSON = "json" ) // Mode define how progress should be rendered, either as ModePlain or ModeTTY @@ -130,6 +132,13 @@ func NewWriter(ctx context.Context, out *streams.Out, progressTitle string) (Wri if tty { return newTTYWriter(out, dryRun, progressTitle) } + if Mode == ModeJSON { + return &jsonWriter{ + out: out, + done: make(chan bool), + dryRun: dryRun, + }, nil + } return &plainWriter{ out: out, done: make(chan bool), From b8cdce5500ec517ac06da6c6585792f9f395c83e Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 8 Feb 2024 16:31:29 +0100 Subject: [PATCH 2/5] Pass 'plain' instead of 'json' to build backend Signed-off-by: Felix Fontein --- cmd/compose/build.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/compose/build.go b/cmd/compose/build.go index a204c37a00..b7ab0d01e2 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -67,10 +67,15 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, builderName = os.Getenv("BUILDX_BUILDER") } + // The build backends do not have a "json" progress; use "plain" instead + uiMode := ui.Mode + if uiMode == ui.ModeJSON { + uiMode = ui.ModePlain + } return api.BuildOptions{ Pull: opts.pull, Push: opts.push, - Progress: ui.Mode, + Progress: uiMode, Args: types.NewMappingWithEquals(opts.args), NoCache: opts.noCache, Quiet: opts.quiet, From 1af4987af6406d799e7209fba53d633a62b26b2f Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 8 Feb 2024 21:19:54 +0100 Subject: [PATCH 3/5] Format errors as JSON when in JSON progress mode. Signed-off-by: Felix Fontein --- cmd/compose/compose.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 7162c76081..663903df6b 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -18,6 +18,7 @@ package compose import ( "context" + "encoding/json" "errors" "fmt" "os" @@ -109,6 +110,9 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error { Status: err.Error(), } } + if ui.Mode == ui.ModeJSON { + err = makeJSONError(err) + } return err } } @@ -165,6 +169,38 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF }) } +type jsonErrorData struct { + Error bool `json:"error,omitempty"` + Message string `json:"message,omitempty"` +} + +func errorAsJSON(message string) string { + errorMessage := &jsonErrorData{ + Error: true, + Message: message, + } + marshal, err := json.Marshal(errorMessage) + if err == nil { + return string(marshal) + } else { + return message + } +} + +func makeJSONError(err error) error { + if err == nil { + return nil + } + var statusErr dockercli.StatusError + if errors.As(err, &statusErr) { + return dockercli.StatusError{ + StatusCode: statusErr.StatusCode, + Status: errorAsJSON(statusErr.Status), + } + } + return fmt.Errorf("%s", errorAsJSON(err.Error())) +} + func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) { f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable") f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name") From 0d2ed6be264c41b3699afd00f82e65bc19cc42a6 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 3 Mar 2024 09:21:15 +0100 Subject: [PATCH 4/5] Set logging format to JSON. Signed-off-by: Felix Fontein --- cmd/compose/compose.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 663903df6b..75aef36beb 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -491,6 +491,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli ui.Mode = ui.ModeQuiet case ui.ModeJSON: ui.Mode = ui.ModeJSON + logrus.SetFormatter(&logrus.JSONFormatter{}) default: return fmt.Errorf("unsupported --progress value %q", opts.Progress) } From 22459435f432a2aba12974e2012a74319ec9a5bf Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 16 Mar 2024 17:10:58 +0100 Subject: [PATCH 5/5] Use rawjson for the build backend. Signed-off-by: Felix Fontein --- cmd/compose/build.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/compose/build.go b/cmd/compose/build.go index b7ab0d01e2..1c914134e1 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -67,10 +67,9 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, builderName = os.Getenv("BUILDX_BUILDER") } - // The build backends do not have a "json" progress; use "plain" instead uiMode := ui.Mode if uiMode == ui.ModeJSON { - uiMode = ui.ModePlain + uiMode = "rawjson" } return api.BuildOptions{ Pull: opts.pull,