Skip to content

Commit b2ee955

Browse files
authored
Merge pull request #4758 from hashicorp/f-driver-plugin-docker
docklog: add go-plugin for forwarding of docker logs
2 parents 668596e + 4ed995a commit b2ee955

File tree

7 files changed

+751
-0
lines changed

7 files changed

+751
-0
lines changed

drivers/docker/docklog/client.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package docklog
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/nomad/drivers/docker/docklog/proto"
7+
)
8+
9+
// dockerLoggerClient implements the dockerLogger interface for client side requests
10+
type dockerLoggerClient struct {
11+
client proto.DockerLoggerClient
12+
}
13+
14+
// Start proxies the Start client side func to the protobuf interface
15+
func (c *dockerLoggerClient) Start(opts *StartOpts) error {
16+
req := &proto.StartRequest{
17+
Endpoint: opts.Endpoint,
18+
ContainerId: opts.ContainerID,
19+
StdoutFifo: opts.Stdout,
20+
StderrFifo: opts.Stderr,
21+
22+
TlsCert: opts.TLSCert,
23+
TlsKey: opts.TLSKey,
24+
TlsCa: opts.TLSCA,
25+
}
26+
_, err := c.client.Start(context.Background(), req)
27+
return err
28+
}
29+
30+
// Stop proxies the Stop client side func to the protobuf interface
31+
func (c *dockerLoggerClient) Stop() error {
32+
req := &proto.StopRequest{}
33+
_, err := c.client.Stop(context.Background(), req)
34+
return err
35+
}
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package docklog
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
docker "github.com/fsouza/go-dockerclient"
8+
hclog "github.com/hashicorp/go-hclog"
9+
multierror "github.com/hashicorp/go-multierror"
10+
"github.com/hashicorp/nomad/client/lib/fifo"
11+
"golang.org/x/net/context"
12+
)
13+
14+
// DockerLogger is a small utility to forward logs from a docker container to a target
15+
// destination
16+
type DockerLogger interface {
17+
Start(*StartOpts) error
18+
Stop() error
19+
}
20+
21+
// StartOpts are the options needed to start docker log monitoring
22+
type StartOpts struct {
23+
// Endpoint sets the docker client endpoint, defaults to environment if not set
24+
Endpoint string
25+
26+
// ContainerID of the container to monitor logs for
27+
ContainerID string
28+
29+
// Stdout path to fifo
30+
Stdout string
31+
//Stderr path to fifo
32+
Stderr string
33+
34+
// TLS settings for docker client
35+
TLSCert string
36+
TLSKey string
37+
TLSCA string
38+
}
39+
40+
// NewDockerLogger returns an implementation of the DockerLogger interface
41+
func NewDockerLogger(logger hclog.Logger) DockerLogger {
42+
return &dockerLogger{logger: logger}
43+
}
44+
45+
// dockerLogger implements the DockerLogger interface
46+
type dockerLogger struct {
47+
logger hclog.Logger
48+
49+
stdout io.WriteCloser
50+
stderr io.WriteCloser
51+
cancelCtx context.CancelFunc
52+
}
53+
54+
// Start log monitoring
55+
func (d *dockerLogger) Start(opts *StartOpts) error {
56+
client, err := d.getDockerClient(opts)
57+
if err != nil {
58+
return fmt.Errorf("failed to open docker client: %v", err)
59+
}
60+
61+
if d.stdout == nil {
62+
stdout, err := fifo.Open(opts.Stdout)
63+
if err != nil {
64+
return fmt.Errorf("failed to open fifo for path %s: %v", opts.Stdout, err)
65+
}
66+
d.stdout = stdout
67+
}
68+
if d.stderr == nil {
69+
stderr, err := fifo.Open(opts.Stderr)
70+
if err != nil {
71+
return fmt.Errorf("failed to open fifo for path %s: %v", opts.Stdout, err)
72+
}
73+
d.stderr = stderr
74+
}
75+
ctx, cancel := context.WithCancel(context.Background())
76+
d.cancelCtx = cancel
77+
78+
logOpts := docker.LogsOptions{
79+
Context: ctx,
80+
Container: opts.ContainerID,
81+
OutputStream: d.stdout,
82+
ErrorStream: d.stderr,
83+
Since: 0,
84+
Follow: true,
85+
Stdout: true,
86+
Stderr: true,
87+
}
88+
89+
go func() { client.Logs(logOpts) }()
90+
return nil
91+
92+
}
93+
94+
// Stop log monitoring
95+
func (d *dockerLogger) Stop() error {
96+
if d.cancelCtx != nil {
97+
d.cancelCtx()
98+
}
99+
if d.stdout != nil {
100+
d.stdout.Close()
101+
}
102+
if d.stderr != nil {
103+
d.stderr.Close()
104+
}
105+
return nil
106+
}
107+
108+
func (d *dockerLogger) getDockerClient(opts *StartOpts) (*docker.Client, error) {
109+
var err error
110+
var merr multierror.Error
111+
var newClient *docker.Client
112+
113+
// Default to using whatever is configured in docker.endpoint. If this is
114+
// not specified we'll fall back on NewClientFromEnv which reads config from
115+
// the DOCKER_* environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and
116+
// DOCKER_CERT_PATH. This allows us to lock down the config in production
117+
// but also accept the standard ENV configs for dev and test.
118+
if opts.Endpoint != "" {
119+
if opts.TLSCert+opts.TLSKey+opts.TLSCA != "" {
120+
d.logger.Debug("using TLS client connection to docker", "endpoint", opts.Endpoint)
121+
newClient, err = docker.NewTLSClient(opts.Endpoint, opts.TLSCert, opts.TLSKey, opts.TLSCA)
122+
if err != nil {
123+
merr.Errors = append(merr.Errors, err)
124+
}
125+
} else {
126+
d.logger.Debug("using plaintext client connection to docker", "endpoint", opts.Endpoint)
127+
newClient, err = docker.NewClient(opts.Endpoint)
128+
if err != nil {
129+
merr.Errors = append(merr.Errors, err)
130+
}
131+
}
132+
} else {
133+
d.logger.Debug("using client connection initialized from environment")
134+
newClient, err = docker.NewClientFromEnv()
135+
if err != nil {
136+
merr.Errors = append(merr.Errors, err)
137+
}
138+
}
139+
140+
return newClient, merr.ErrorOrNil()
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package docklog
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"testing"
7+
8+
docker "github.com/fsouza/go-dockerclient"
9+
"github.com/hashicorp/nomad/helper/testlog"
10+
"github.com/hashicorp/nomad/testutil"
11+
"github.com/stretchr/testify/require"
12+
"golang.org/x/net/context"
13+
)
14+
15+
func TestDockerLogger(t *testing.T) {
16+
t.Parallel()
17+
require := require.New(t)
18+
19+
client, err := docker.NewClientFromEnv()
20+
if err != nil {
21+
t.Skip("docker unavailable:", err)
22+
}
23+
24+
containerConf := docker.CreateContainerOptions{
25+
Config: &docker.Config{
26+
Cmd: []string{
27+
"/bin/ash", "-c", "touch /tmp/docklog; tail -f /tmp/docklog",
28+
},
29+
Image: "alpine",
30+
},
31+
Context: context.Background(),
32+
}
33+
34+
container, err := client.CreateContainer(containerConf)
35+
require.NoError(err)
36+
37+
defer client.RemoveContainer(docker.RemoveContainerOptions{
38+
ID: container.ID,
39+
Force: true,
40+
})
41+
42+
err = client.StartContainer(container.ID, nil)
43+
require.NoError(err)
44+
45+
testutil.WaitForResult(func() (bool, error) {
46+
container, err = client.InspectContainer(container.ID)
47+
if err != nil {
48+
return false, err
49+
}
50+
if !container.State.Running {
51+
return false, fmt.Errorf("container not running")
52+
}
53+
return true, nil
54+
}, func(err error) {
55+
require.NoError(err)
56+
})
57+
58+
stdout := &noopCloser{bytes.NewBuffer(nil)}
59+
stderr := &noopCloser{bytes.NewBuffer(nil)}
60+
61+
dl := NewDockerLogger(testlog.HCLogger(t)).(*dockerLogger)
62+
dl.stdout = stdout
63+
dl.stderr = stderr
64+
require.NoError(dl.Start(&StartOpts{
65+
ContainerID: container.ID,
66+
}))
67+
68+
echoToContainer(t, client, container.ID, "abc")
69+
echoToContainer(t, client, container.ID, "123")
70+
71+
testutil.WaitForResult(func() (bool, error) {
72+
act := stdout.String()
73+
if "abc\n123\n" != act {
74+
return false, fmt.Errorf("expected abc\\n123\\n for stdout but got %s", act)
75+
}
76+
77+
return true, nil
78+
}, func(err error) {
79+
require.NoError(err)
80+
})
81+
}
82+
83+
func echoToContainer(t *testing.T, client *docker.Client, id string, line string) {
84+
op := docker.CreateExecOptions{
85+
Container: id,
86+
Cmd: []string{
87+
"/bin/ash", "-c",
88+
fmt.Sprintf("echo %s >>/tmp/docklog", line),
89+
},
90+
}
91+
92+
exec, err := client.CreateExec(op)
93+
require.NoError(t, err)
94+
require.NoError(t, client.StartExec(exec.ID, docker.StartExecOptions{Detach: true}))
95+
}
96+
97+
type noopCloser struct {
98+
*bytes.Buffer
99+
}
100+
101+
func (*noopCloser) Close() error {
102+
return nil
103+
}

drivers/docker/docklog/plugin.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package docklog
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
7+
hclog "github.com/hashicorp/go-hclog"
8+
plugin "github.com/hashicorp/go-plugin"
9+
"github.com/hashicorp/nomad/drivers/docker/docklog/proto"
10+
"github.com/hashicorp/nomad/helper/discover"
11+
"github.com/hashicorp/nomad/plugins/base"
12+
"google.golang.org/grpc"
13+
)
14+
15+
const pluginName = "docker_logger"
16+
17+
// LaunchDockerLogger launches an instance of DockerLogger
18+
// TODO: Integrate with base plugin loader
19+
func LaunchDockerLogger(logger hclog.Logger) (DockerLogger, *plugin.Client, error) {
20+
logger = logger.Named(pluginName)
21+
bin, err := discover.NomadExecutable()
22+
if err != nil {
23+
return nil, nil, err
24+
}
25+
26+
client := plugin.NewClient(&plugin.ClientConfig{
27+
HandshakeConfig: base.Handshake,
28+
Plugins: map[string]plugin.Plugin{
29+
pluginName: &Plugin{impl: NewDockerLogger(hclog.L().Named(pluginName))},
30+
},
31+
Cmd: exec.Command(bin, pluginName),
32+
AllowedProtocols: []plugin.Protocol{
33+
plugin.ProtocolGRPC,
34+
},
35+
})
36+
37+
rpcClient, err := client.Client()
38+
if err != nil {
39+
return nil, nil, err
40+
}
41+
42+
raw, err := rpcClient.Dispense(pluginName)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
47+
l := raw.(DockerLogger)
48+
return l, client, nil
49+
50+
}
51+
52+
// Plugin is the go-plugin implementation
53+
type Plugin struct {
54+
plugin.NetRPCUnsupportedPlugin
55+
impl DockerLogger
56+
}
57+
58+
// GRPCServer registered the server side implementation with the grpc server
59+
func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
60+
proto.RegisterDockerLoggerServer(s, &dockerLoggerServer{
61+
impl: p.impl,
62+
broker: broker,
63+
})
64+
return nil
65+
}
66+
67+
// GRPCClient returns a client side implementation of the plugin
68+
func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
69+
return &dockerLoggerClient{client: proto.NewDockerLoggerClient(c)}, nil
70+
}

0 commit comments

Comments
 (0)