Skip to content

Commit

Permalink
Interface out log writer for docker/podman formats
Browse files Browse the repository at this point in the history
  • Loading branch information
kleesc committed Nov 19, 2020
1 parent ee757ce commit 3a557a5
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 49 deletions.
2 changes: 1 addition & 1 deletion buildctx/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func New(client rpc.Client, args *rpc.BuildArgs, dockerHost, containerRuntime st

return &Context{
client: client,
writer: containerclient.NewRPCWriter(client),
writer: containerclient.NewRPCWriter(client, containerRuntime),
containerClient: containerClient,
args: args,
}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,6 @@ const (
pushingStatus = "Pushing"
)

// LogWriter represents anything that can stream Docker logs from the daemon
// and check if any error has occured.
type LogWriter interface {
// ErrResponse returns an error that occurred from Docker and resets the
// state of that internal error value to nil. If there is no error, returns
// false as part of the tuple.
ErrResponse() (error, bool)

// ResetError throws away any error state from previously streamed logs.
ResetError()

io.Writer
}

// partialBuffer represents a buffer of data that was unable to be previously
// serialized because it was not enough data was provided to form valid JSON.
type partialBuffer []byte
Expand All @@ -49,25 +35,17 @@ func (pb *partialBuffer) getAndEmpty(in []byte) (ret []byte) {
return
}

// RPCWriter implements a Writer that consumes encoded JSON data and buffers it
// DockerRPCWriter implements a RPCWriter that consumes encoded JSON data and buffers it
// until it has a valid JSON object and then logs it to an rpc.Client.
type RPCWriter struct {
type DockerRPCWriter struct {
client rpc.Client
errResponse *Response
partialBuffer *partialBuffer
hasPartialBuffer bool
}

// NewRPCWriter allocates a new Writer that streams logs via an RPC client.
func NewRPCWriter(client rpc.Client) LogWriter {
return &RPCWriter{
client: client,
partialBuffer: new(partialBuffer),
}
}

// Write implements the io.Writer interface for RPCWriter.
func (w *RPCWriter) Write(p []byte) (n int, err error) {
func (w *DockerRPCWriter) Write(p []byte) (n int, err error) {
originalLength := len(p)

// Note: Sometimes Docker returns to us only the beginning of a stream,
Expand Down Expand Up @@ -135,7 +113,7 @@ func (w *RPCWriter) Write(p []byte) (n int, err error) {

// ErrResponse returns an error that occurred from Docker and then calls
// ResetError().
func (w *RPCWriter) ErrResponse() (error, bool) {
func (w *DockerRPCWriter) ErrResponse() (error, bool) {
err := w.errResponse
w.ResetError()

Expand All @@ -147,25 +125,10 @@ func (w *RPCWriter) ErrResponse() (error, bool) {
}

// ResetError throws away any error state from previously streamed logs.
func (w *RPCWriter) ResetError() {
func (w *DockerRPCWriter) ResetError() {
w.errResponse = nil
}

// Response represents a response from a Docker™ daemon.
type Response struct {
Error string `json:"error,omitempty"`
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
ID string `json:"id,omitempty"`
ProgressDetail progressDetail `json:"progressDetail,omitempty"`
}

// progressDetail represents the progress made by a Docker™ command.
type progressDetail struct {
Current int `json:"current,omitempty"`
Total int `json:"total,omitempty"`
}

type filter struct {
lastSent *Response
}
Expand Down
66 changes: 60 additions & 6 deletions containerclient/interface.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package containerclient

import (
"fmt"
"io"
"strings"

log "github.com/sirupsen/logrus"

"github.com/quay/quay-builder/rpc"
)

type BuildImageOptions struct {
Expand Down Expand Up @@ -74,15 +77,66 @@ type Client interface {
PruneImages(PruneImagesOptions) (*PruneImagesResults, error)
}

func NewClient(host, runtime string) (Client, error) {
runtime = strings.ToLower(runtime)
if runtime != "docker" && runtime != "podman" {
return nil, fmt.Errorf("Invalid container runtime: %s", runtime)
func NewClient(host, containerRuntime string) (Client, error) {
containerRuntime = strings.ToLower(containerRuntime)
if containerRuntime != "docker" && containerRuntime != "podman" {
log.Fatal("Invalid container runtime:", containerRuntime)
}

if runtime == "docker" {
if containerRuntime == "docker" {
return NewDockerClient(host)
} else {
return NewPodmanClient(host)
}
}

// LogWriter represents anything that can stream Docker logs from the daemon
// and check if any error has occured.
type LogWriter interface {
// ErrResponse returns an error that occurred from Docker and resets the
// state of that internal error value to nil. If there is no error, returns
// false as part of the tuple.
ErrResponse() (error, bool)

// ResetError throws away any error state from previously streamed logs.
ResetError()

io.Writer
}

// NewRPCWriter allocates a new Writer that streams logs via an RPC client.
func NewRPCWriter(client rpc.Client, containerRuntime string) LogWriter {
containerRuntime = strings.ToLower(containerRuntime)
if containerRuntime != "docker" && containerRuntime != "podman" {
log.Fatal("Invalid container runtime:", containerRuntime)
}

if containerRuntime == "docker" {
return &DockerRPCWriter{
client: client,
partialBuffer: new(partialBuffer),
}
} else {
return &PodmanRPCWriter{
client: client,
partialBuffer: new(partialBuffer),
}
}

}


// Response represents a response from a Docker™ daemon or podman.
type Response struct {
Error string `json:"error,omitempty"`
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
ID string `json:"id,omitempty"`
ProgressDetail progressDetail `json:"progressDetail,omitempty"`
}

// progressDetail represents the progress made by a Docker™ command.
type progressDetail struct {
Current int `json:"current,omitempty"`
Total int `json:"total,omitempty"`
}
56 changes: 56 additions & 0 deletions containerclient/podman_log_write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package containerclient

import (
"errors"
"encoding/json"

log "github.com/sirupsen/logrus"

"github.com/quay/quay-builder/rpc"
)

// PodmanRPCWriter implements a RPCWriter.
// Unlike the Docker daemon, Podman's build call outputs plain string, and not JSON encoded data,
// so we need to serialize each line into a Response struct before logging it to an rpc.Client.
type PodmanRPCWriter struct {
client rpc.Client
errResponse *Response
partialBuffer *partialBuffer
hasPartialBuffer bool
}

// Write implements the io.Writer interface for RPCWriter.
func (w * PodmanRPCWriter) Write(p []byte) (n int, err error) {
// Unlike docker, libpod parses the JSON encoded data from stream before writing the output,
// without the option of returning the raw data instead.
// Instead of decoding the stream into a Response, we set the Response's "Stream" before
// marshaling it into JSON to be logged.
originalLength := len(p)

var m Response
m.Stream = string(p)

jsonData, err := json.Marshal(&m)
if err != nil {
log.Fatalf("Error when marshaling logs: %v", err)
}

err = w.client.PublishBuildLogEntry(string(jsonData))
if err != nil {
log.Fatalf("Failed to publish log entry: %v", err)
}

return originalLength, nil
}

func (w *PodmanRPCWriter) ErrResponse() (error, bool) {
// libpod already parses the JSON stream before writing to output.
// So the error would not be returned from the output stream,. but as
// the return value of the API call instead.
// See https://github.com/containers/podman/blob/master/pkg/bindings/images/build.go#L175
return nil, false
}

func (w *PodmanRPCWriter) ResetError() {
w.errResponse = nil
}

0 comments on commit 3a557a5

Please sign in to comment.