diff --git a/command/daemon/config.go b/command/daemon/config.go index 753c2e2..acfdaa9 100644 --- a/command/daemon/config.go +++ b/command/daemon/config.go @@ -87,6 +87,7 @@ type Config struct { Docker struct { Config string `envconfig:"DRONE_DOCKER_CONFIG"` + Stream bool `envconfig:"DRONE_DOCKER_STREAM_PULL" default:"true"` } } diff --git a/command/daemon/daemon.go b/command/daemon/daemon.go index 26ef741..9ddca46 100644 --- a/command/daemon/daemon.go +++ b/command/daemon/daemon.go @@ -78,7 +78,10 @@ func (c *daemonCommand) run(*kingpin.ParseContext) error { ), ) - engine, err := engine.NewEnv() + opts := engine.Opts{ + HidePull: !config.Docker.Stream, + } + engine, err := engine.NewEnv(opts) if err != nil { logrus.WithError(err). Fatalln("cannot load the docker engine") diff --git a/command/exec.go b/command/exec.go index 3c22489..be7f33f 100644 --- a/command/exec.go +++ b/command/exec.go @@ -228,7 +228,7 @@ func (c *execCommand) run(*kingpin.ParseContext) error { ), ) - engine, err := engine.NewEnv() + engine, err := engine.NewEnv(engine.Opts{}) if err != nil { return err } diff --git a/engine/compiler/compiler.go b/engine/compiler/compiler.go index 50a2456..8d53f99 100644 --- a/engine/compiler/compiler.go +++ b/engine/compiler/compiler.go @@ -266,6 +266,7 @@ func (c *Compiler) Compile(ctx context.Context, args Args) *engine.Spec { step.Envs = environ.Combine(envs, step.Envs) step.WorkingDir = full step.Labels = labels + step.Pull = engine.PullIfNotExists step.Volumes = append(step.Volumes, mount) spec.Steps = append(spec.Steps, step) diff --git a/engine/engine_impl.go b/engine/engine_impl.go index a3643cd..8865779 100644 --- a/engine/engine_impl.go +++ b/engine/engine_impl.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "github.com/drone-runners/drone-runner-docker/internal/docker/image" + "github.com/drone-runners/drone-runner-docker/internal/docker/jsonmessage" "github.com/drone-runners/drone-runner-docker/internal/docker/stdcopy" "github.com/drone/runner-go/registry/auths" @@ -18,23 +19,32 @@ import ( "docker.io/go-docker/api/types/volume" ) +// Opts configures the Docker engine. +type Opts struct { + HidePull bool +} + // Docker implements a Docker pipeline engine. type Docker struct { - client docker.APIClient + client docker.APIClient + hidePull bool } // New returns a new engine. -func New(client docker.APIClient) *Docker { - return &Docker{client} +func New(client docker.APIClient, opts Opts) *Docker { + return &Docker{ + client: client, + hidePull: opts.HidePull, + } } // NewEnv returns a new Engine from the environment. -func NewEnv() (*Docker, error) { +func NewEnv(opts Opts) (*Docker, error) { cli, err := docker.NewEnvClient() if err != nil { return nil, err } - return New(cli), nil + return New(cli, opts), nil } // Ping pings the Docker daemon. @@ -157,7 +167,11 @@ func (e *Docker) create(ctx context.Context, spec *Spec, step *Step, output io.W (step.Pull == PullDefault && image.IsLatest(step.Image)) { rc, pullerr := e.client.ImagePull(ctx, step.Image, pullopts) if pullerr == nil { - io.Copy(ioutil.Discard, rc) + if e.hidePull { + io.Copy(ioutil.Discard, rc) + } else { + jsonmessage.Copy(rc, output) + } rc.Close() } if pullerr != nil { @@ -179,7 +193,12 @@ func (e *Docker) create(ctx context.Context, spec *Spec, step *Step, output io.W if pullerr != nil { return pullerr } - io.Copy(ioutil.Discard, rc) + + if e.hidePull { + io.Copy(ioutil.Discard, rc) + } else { + jsonmessage.Copy(rc, output) + } rc.Close() // once the image is successfully pulled we attempt to diff --git a/internal/docker/jsonmessage/jsonmessage.go b/internal/docker/jsonmessage/jsonmessage.go new file mode 100644 index 0000000..027d7a3 --- /dev/null +++ b/internal/docker/jsonmessage/jsonmessage.go @@ -0,0 +1,61 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package jsonmessage + +import ( + "encoding/json" + "fmt" + "io" +) + +type jsonError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *jsonError) Error() string { + return e.Message +} + +type jsonMessage struct { + ID string `json:"id"` + Status string `json:"status"` + Error *jsonError `json:"errorDetail"` + Progress *jsonProgress `json:"progressDetail"` +} + +type jsonProgress struct { +} + +// Copy copies a json message string to the io.Writer. +func Copy(in io.Reader, out io.Writer) error { + dec := json.NewDecoder(in) + for { + var jm jsonMessage + if err := dec.Decode(&jm); err != nil { + if err == io.EOF { + break + } + return err + } + + if jm.Error != nil { + if jm.Error.Code == 401 { + return fmt.Errorf("authentication is required") + } + return jm.Error + } + + if jm.Progress != nil { + continue + } + if jm.ID == "" { + fmt.Fprintf(out, "%s\n", jm.Status) + } else { + fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status) + } + } + return nil +} diff --git a/internal/docker/jsonmessage/testdata/alpine.json b/internal/docker/jsonmessage/testdata/alpine.json new file mode 100644 index 0000000..6969c42 --- /dev/null +++ b/internal/docker/jsonmessage/testdata/alpine.json @@ -0,0 +1,12 @@ +{"status":"Pulling from library/alpine","id":"3.6"} +{"status":"Pulling fs layer","progressDetail":{},"id":"5a3ea8efae5d"} +{"status":"Downloading","progressDetail":{"current":21019,"total":2017774},"progress":"[\u003e ] 21.02kB/2.018MB","id":"5a3ea8efae5d"} +{"status":"Downloading","progressDetail":{"current":1194721,"total":2017774},"progress":"[=============================\u003e ] 1.195MB/2.018MB","id":"5a3ea8efae5d"} +{"status":"Verifying Checksum","progressDetail":{},"id":"5a3ea8efae5d"} +{"status":"Download complete","progressDetail":{},"id":"5a3ea8efae5d"} +{"status":"Extracting","progressDetail":{"current":32768,"total":2017774},"progress":"[\u003e ] 32.77kB/2.018MB","id":"5a3ea8efae5d"} +{"status":"Extracting","progressDetail":{"current":786432,"total":2017774},"progress":"[===================\u003e ] 786.4kB/2.018MB","id":"5a3ea8efae5d"} +{"status":"Extracting","progressDetail":{"current":2017774,"total":2017774},"progress":"[==================================================\u003e] 2.018MB/2.018MB","id":"5a3ea8efae5d"} +{"status":"Pull complete","progressDetail":{},"id":"5a3ea8efae5d"} +{"status":"Digest: sha256:66790a2b79e1ea3e1dabac43990c54aca5d1ddf268d9a5a0285e4167c8b24475"} +{"status":"Status: Downloaded newer image for alpine:3.6"} \ No newline at end of file