Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON stream progress writer #11478

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/compose/build.go
Expand Up @@ -67,10 +67,14 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
builderName = os.Getenv("BUILDX_BUILDER")
}

uiMode := ui.Mode
if uiMode == ui.ModeJSON {
uiMode = "rawjson"
}
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,
Expand Down
40 changes: 40 additions & 0 deletions cmd/compose/compose.go
Expand Up @@ -18,6 +18,7 @@ package compose

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -111,6 +112,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
}
}
Expand Down Expand Up @@ -166,6 +170,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")
Expand Down Expand Up @@ -445,6 +481,9 @@ 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
logrus.SetFormatter(&logrus.JSONFormatter{})
default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
}
Expand Down Expand Up @@ -620,6 +659,7 @@ var printerModes = []string{
ui.ModeAuto,
ui.ModeTTY,
ui.ModePlain,
ui.ModeJSON,
ui.ModeQuiet,
}

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/compose.md
Expand Up @@ -50,7 +50,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<br>(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/docker_compose.yaml
Expand Up @@ -293,7 +293,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
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/docker_compose_build.yaml
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions 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) {
}
9 changes: 9 additions & 0 deletions pkg/progress/writer.go
Expand Up @@ -110,6 +110,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
Expand All @@ -136,6 +138,13 @@ func NewWriter(ctx context.Context, out io.Writer, progressTitle string) (Writer
return newTTYWriter(f, dryRun, progressTitle)
}
}
if Mode == ModeJSON {
return &jsonWriter{
out: out,
done: make(chan bool),
dryRun: dryRun,
}, nil
}
return &plainWriter{
out: out,
done: make(chan bool),
Expand Down