From e31d405e04715b4e666a23aeee11e424eb343640 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 12 Jul 2017 07:01:42 -0700 Subject: [PATCH] agent: replace docker check This patch replaces the Docker client which is used for health checks with a simplified version tailored for that purpose. See #3254 See #3257 Fixes #3270 --- agent/check.go | 142 +++++------ agent/check_test.go | 516 ++++++++++++++++++++-------------------- agent/docker.go | 135 +++++++++++ agent/docker_unix.go | 3 + agent/docker_windows.go | 3 + 5 files changed, 455 insertions(+), 344 deletions(-) create mode 100644 agent/docker.go create mode 100644 agent/docker_unix.go create mode 100644 agent/docker_windows.go diff --git a/agent/check.go b/agent/check.go index d577a901ad9a..1410b1a0a31a 100644 --- a/agent/check.go +++ b/agent/check.go @@ -14,7 +14,6 @@ import ( "time" "github.com/armon/circbuf" - docker "github.com/fsouza/go-dockerclient" "github.com/hashicorp/consul/agent/consul/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" @@ -516,14 +515,6 @@ func (c *CheckTCP) check() { c.Notify.UpdateCheck(c.CheckID, api.HealthPassing, fmt.Sprintf("TCP connect %s: Success", c.TCP)) } -// DockerClient defines an interface for a docker client -// which is used for injecting a fake client during tests. -type DockerClient interface { - CreateExec(docker.CreateExecOptions) (*docker.Exec, error) - StartExec(string, docker.StartExecOptions) error - InspectExec(string) (*docker.ExecInspect, error) -} - // CheckDocker is used to periodically invoke a script to // determine the health of an application running inside a // Docker Container. We assume that the script is compatible @@ -537,21 +528,33 @@ type CheckDocker struct { Interval time.Duration Logger *log.Logger - dockerClient DockerClient - cmd []string - stop bool - stopCh chan struct{} - stopLock sync.Mutex + client *DockerClient + cmd []string + stop bool + stopCh chan struct{} + stopLock sync.Mutex } // Init initializes the Docker Client func (c *CheckDocker) Init() error { - var err error - c.dockerClient, err = docker.NewClientFromEnv() + // todo(fs): does this call need to be idempotent? + + dc, err := NewDockerClient(os.Getenv("DOCKER_HOST"), CheckBufSize) if err != nil { - c.Logger.Printf("[DEBUG] Error creating the Docker client: %s", err.Error()) + c.Logger.Printf("[ERR] agent: error creating docker client: %s", err) return err } + c.client = dc + + // todo(fs): we should not patch c.Shell here but use a local var + if c.Shell == "" { + c.Shell = os.Getenv("SHELL") + if c.Shell == "" { + c.Shell = "/bin/sh" + } + } + c.cmd = []string{c.Shell, "-c", c.Script} + return nil } @@ -560,14 +563,8 @@ func (c *CheckDocker) Init() error { func (c *CheckDocker) Start() { c.stopLock.Lock() defer c.stopLock.Unlock() - - //figure out the shell - if c.Shell == "" { - c.Shell = shell() - } - - c.cmd = []string{c.Shell, "-c", c.Script} - + // todo(fs): we don't need stop since stopCh != nil is enough + // todo(fs): we should check if we're already running and then stop first c.stop = false c.stopCh = make(chan struct{}) go c.run() @@ -601,73 +598,50 @@ func (c *CheckDocker) run() { } func (c *CheckDocker) check() { - //Set up the Exec since - execOpts := docker.CreateExecOptions{ - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - Tty: false, - Cmd: c.cmd, - Container: c.DockerContainerID, - } - var ( - exec *docker.Exec - err error - ) - if exec, err = c.dockerClient.CreateExec(execOpts); err != nil { - c.Logger.Printf("[DEBUG] agent: Error while creating Exec: %s", err.Error()) - c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, fmt.Sprintf("Unable to create Exec, error: %s", err.Error())) - return - } - - // Collect the output - output, _ := circbuf.NewBuffer(CheckBufSize) - - err = c.dockerClient.StartExec(exec.ID, docker.StartExecOptions{Detach: false, Tty: false, OutputStream: output, ErrorStream: output}) + var out string + status, b, err := c.doCheck(c.client) if err != nil { - c.Logger.Printf("[DEBUG] Error in executing health checks: %s", err.Error()) - c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, fmt.Sprintf("Unable to start Exec: %s", err.Error())) - return + c.Logger.Printf("[DEBUG] agent: Check '%s': %s", c.CheckID, err) + out = err.Error() + } else { + // out is already limited to CheckBufSize since we're getting a + // limited buffer. So we don't need to truncate it just report + // that it was truncated. + out = string(b.Bytes()) + if int(b.TotalWritten()) > len(out) { + out = fmt.Sprintf("Captured %d of %d bytes\n...\n%s", len(out), b.TotalWritten(), out) + } + c.Logger.Printf("[DEBUG] agent: Check '%s' script '%s' output: %s", c.CheckID, c.Script, out) } - // Get the output, add a message about truncation - outputStr := string(output.Bytes()) - if output.TotalWritten() > output.Size() { - outputStr = fmt.Sprintf("Captured %d of %d bytes\n...\n%s", - output.Size(), output.TotalWritten(), outputStr) + if status == api.HealthCritical { + c.Logger.Printf("[WARN] agent: Check '%v' is now critical", c.CheckID) } - c.Logger.Printf("[DEBUG] agent: Check '%s' script '%s' output: %s", - c.CheckID, c.Script, outputStr) + c.Notify.UpdateCheck(c.CheckID, status, out) +} - execInfo, err := c.dockerClient.InspectExec(exec.ID) +func (c *CheckDocker) doCheck(dc *DockerClient) (string, *circbuf.Buffer, error) { + execID, err := dc.CreateExec(c.DockerContainerID, c.cmd) if err != nil { - c.Logger.Printf("[DEBUG] Error in inspecting check result : %s", err.Error()) - c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, fmt.Sprintf("Unable to inspect Exec: %s", err.Error())) - return + return api.HealthCritical, nil, err } - - // Sets the status of the check to healthy if exit code is 0 - if execInfo.ExitCode == 0 { - c.Notify.UpdateCheck(c.CheckID, api.HealthPassing, outputStr) - return - } - - // Set the status of the check to Warning if exit code is 1 - if execInfo.ExitCode == 1 { - c.Logger.Printf("[DEBUG] Check failed with exit code: %d", execInfo.ExitCode) - c.Notify.UpdateCheck(c.CheckID, api.HealthWarning, outputStr) - return + buf, err := dc.StartExec(execID) + if err != nil { + return api.HealthCritical, nil, err } - - // Set the health as critical - c.Logger.Printf("[WARN] agent: Check '%v' is now critical", c.CheckID) - c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, outputStr) -} - -func shell() string { - if sh := os.Getenv("SHELL"); sh != "" { - return sh + exitCode, err := dc.InspectExec(execID) + if err != nil { + return api.HealthCritical, nil, err + } + switch exitCode { + case 0: + return api.HealthPassing, buf, nil + case 1: + c.Logger.Printf("[DEBUG] Check failed with exit code: %d", exitCode) + return api.HealthWarning, buf, nil + default: + c.Logger.Printf("[DEBUG] Check failed with exit code: %d", exitCode) + return api.HealthCritical, buf, nil } - return "/bin/sh" } diff --git a/agent/check_test.go b/agent/check_test.go index f18795e3738c..2e25dc0d90d1 100644 --- a/agent/check_test.go +++ b/agent/check_test.go @@ -2,21 +2,17 @@ package agent import ( "bytes" - "errors" "fmt" "io/ioutil" "log" "net" "net/http" "net/http/httptest" - "os" - "os/exec" "reflect" "strings" "testing" "time" - docker "github.com/fsouza/go-dockerclient" "github.com/hashicorp/consul/agent/mock" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/testutil/retry" @@ -513,259 +509,259 @@ func TestCheckTCPPassing(t *testing.T) { } // A fake docker client to test happy path scenario -type fakeDockerClientWithNoErrors struct { -} - -func (d *fakeDockerClientWithNoErrors) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithNoErrors) StartExec(id string, opts docker.StartExecOptions) error { - fmt.Fprint(opts.OutputStream, "output") - return nil -} - -func (d *fakeDockerClientWithNoErrors) InspectExec(id string) (*docker.ExecInspect, error) { - return &docker.ExecInspect{ - ID: "123", - ExitCode: 0, - }, nil -} - -// A fake docker client to test truncation of output -type fakeDockerClientWithLongOutput struct { -} - -func (d *fakeDockerClientWithLongOutput) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithLongOutput) StartExec(id string, opts docker.StartExecOptions) error { - b, _ := exec.Command("od", "-N", "81920", "/dev/urandom").Output() - fmt.Fprint(opts.OutputStream, string(b)) - return nil -} - -func (d *fakeDockerClientWithLongOutput) InspectExec(id string) (*docker.ExecInspect, error) { - return &docker.ExecInspect{ - ID: "123", - ExitCode: 0, - }, nil -} - -// A fake docker client to test non-zero exit codes from exec invocation -type fakeDockerClientWithExecNonZeroExitCode struct { -} - -func (d *fakeDockerClientWithExecNonZeroExitCode) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithExecNonZeroExitCode) StartExec(id string, opts docker.StartExecOptions) error { - return nil -} - -func (d *fakeDockerClientWithExecNonZeroExitCode) InspectExec(id string) (*docker.ExecInspect, error) { - return &docker.ExecInspect{ - ID: "123", - ExitCode: 127, - }, nil -} - -// A fake docker client to test exit code which result into Warning -type fakeDockerClientWithExecExitCodeOne struct { -} - -func (d *fakeDockerClientWithExecExitCodeOne) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithExecExitCodeOne) StartExec(id string, opts docker.StartExecOptions) error { - fmt.Fprint(opts.OutputStream, "output") - return nil -} - -func (d *fakeDockerClientWithExecExitCodeOne) InspectExec(id string) (*docker.ExecInspect, error) { - return &docker.ExecInspect{ - ID: "123", - ExitCode: 1, - }, nil -} - -// A fake docker client to simulate create exec failing -type fakeDockerClientWithCreateExecFailure struct { -} - -func (d *fakeDockerClientWithCreateExecFailure) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return nil, errors.New("Exec Creation Failed") -} - -func (d *fakeDockerClientWithCreateExecFailure) StartExec(id string, opts docker.StartExecOptions) error { - return errors.New("Exec doesn't exist") -} - -func (d *fakeDockerClientWithCreateExecFailure) InspectExec(id string) (*docker.ExecInspect, error) { - return nil, errors.New("Exec doesn't exist") -} - -// A fake docker client to simulate start exec failing -type fakeDockerClientWithStartExecFailure struct { -} - -func (d *fakeDockerClientWithStartExecFailure) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithStartExecFailure) StartExec(id string, opts docker.StartExecOptions) error { - return errors.New("Couldn't Start Exec") -} - -func (d *fakeDockerClientWithStartExecFailure) InspectExec(id string) (*docker.ExecInspect, error) { - return nil, errors.New("Exec doesn't exist") -} - -// A fake docker client to test exec info query failures -type fakeDockerClientWithExecInfoErrors struct { -} - -func (d *fakeDockerClientWithExecInfoErrors) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { - return &docker.Exec{ID: "123"}, nil -} - -func (d *fakeDockerClientWithExecInfoErrors) StartExec(id string, opts docker.StartExecOptions) error { - return nil -} - -func (d *fakeDockerClientWithExecInfoErrors) InspectExec(id string) (*docker.ExecInspect, error) { - return nil, errors.New("Unable to query exec info") -} - -func expectDockerCheckStatus(t *testing.T, dockerClient DockerClient, status string, output string) { - notif := mock.NewNotify() - check := &CheckDocker{ - Notify: notif, - CheckID: types.CheckID("foo"), - Script: "/health.sh", - DockerContainerID: "54432bad1fc7", - Shell: "/bin/sh", - Interval: 25 * time.Millisecond, - Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), - dockerClient: dockerClient, - } - check.Start() - defer check.Stop() - - time.Sleep(250 * time.Millisecond) - - // Should have at least 2 updates - if notif.Updates("foo") < 2 { - t.Fatalf("should have 2 updates %v", notif.UpdatesMap()) - } - - if notif.State("foo") != status { - t.Fatalf("should be %v %v", status, notif.StateMap()) - } - - if notif.Output("foo") != output { - t.Fatalf("should be %v %v", output, notif.OutputMap()) - } -} - -func TestDockerCheckWhenExecReturnsSuccessExitCode(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithNoErrors{}, api.HealthPassing, "output") -} - -func TestDockerCheckWhenExecCreationFails(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithCreateExecFailure{}, api.HealthCritical, "Unable to create Exec, error: Exec Creation Failed") -} - -func TestDockerCheckWhenExitCodeIsNonZero(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithExecNonZeroExitCode{}, api.HealthCritical, "") -} - -func TestDockerCheckWhenExitCodeIsone(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithExecExitCodeOne{}, api.HealthWarning, "output") -} - -func TestDockerCheckWhenExecStartFails(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithStartExecFailure{}, api.HealthCritical, "Unable to start Exec: Couldn't Start Exec") -} - -func TestDockerCheckWhenExecInfoFails(t *testing.T) { - t.Parallel() - expectDockerCheckStatus(t, &fakeDockerClientWithExecInfoErrors{}, api.HealthCritical, "Unable to inspect Exec: Unable to query exec info") -} - -func TestDockerCheckDefaultToSh(t *testing.T) { - t.Parallel() - os.Setenv("SHELL", "") - notif := mock.NewNotify() - check := &CheckDocker{ - Notify: notif, - CheckID: types.CheckID("foo"), - Script: "/health.sh", - DockerContainerID: "54432bad1fc7", - Interval: 10 * time.Millisecond, - Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), - dockerClient: &fakeDockerClientWithNoErrors{}, - } - check.Start() - defer check.Stop() - - time.Sleep(50 * time.Millisecond) - if check.Shell != "/bin/sh" { - t.Fatalf("Shell should be: %v , actual: %v", "/bin/sh", check.Shell) - } -} - -func TestDockerCheckUseShellFromEnv(t *testing.T) { - t.Parallel() - notif := mock.NewNotify() - os.Setenv("SHELL", "/bin/bash") - check := &CheckDocker{ - Notify: notif, - CheckID: types.CheckID("foo"), - Script: "/health.sh", - DockerContainerID: "54432bad1fc7", - Interval: 10 * time.Millisecond, - Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), - dockerClient: &fakeDockerClientWithNoErrors{}, - } - check.Start() - defer check.Stop() - - time.Sleep(50 * time.Millisecond) - if check.Shell != "/bin/bash" { - t.Fatalf("Shell should be: %v , actual: %v", "/bin/bash", check.Shell) - } - os.Setenv("SHELL", "") -} - -func TestDockerCheckTruncateOutput(t *testing.T) { - t.Parallel() - notif := mock.NewNotify() - check := &CheckDocker{ - Notify: notif, - CheckID: types.CheckID("foo"), - Script: "/health.sh", - DockerContainerID: "54432bad1fc7", - Shell: "/bin/sh", - Interval: 10 * time.Millisecond, - Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), - dockerClient: &fakeDockerClientWithLongOutput{}, - } - check.Start() - defer check.Stop() - - time.Sleep(50 * time.Millisecond) - - // Allow for extra bytes for the truncation message - if len(notif.Output("foo")) > CheckBufSize+100 { - t.Fatalf("output size is too long") - } -} +//type fakeDockerClientWithNoErrors struct { +//} +// +//func (d *fakeDockerClientWithNoErrors) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithNoErrors) StartExec(id string, opts docker.StartExecOptions) error { +// fmt.Fprint(opts.OutputStream, "output") +// return nil +//} +// +//func (d *fakeDockerClientWithNoErrors) InspectExec(id string) (*docker.ExecInspect, error) { +// return &docker.ExecInspect{ +// ID: "123", +// ExitCode: 0, +// }, nil +//} +// +//// A fake docker client to test truncation of output +//type fakeDockerClientWithLongOutput struct { +//} +// +//func (d *fakeDockerClientWithLongOutput) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithLongOutput) StartExec(id string, opts docker.StartExecOptions) error { +// b, _ := exec.Command("od", "-N", "81920", "/dev/urandom").Output() +// fmt.Fprint(opts.OutputStream, string(b)) +// return nil +//} +// +//func (d *fakeDockerClientWithLongOutput) InspectExec(id string) (*docker.ExecInspect, error) { +// return &docker.ExecInspect{ +// ID: "123", +// ExitCode: 0, +// }, nil +//} +// +//// A fake docker client to test non-zero exit codes from exec invocation +//type fakeDockerClientWithExecNonZeroExitCode struct { +//} +// +//func (d *fakeDockerClientWithExecNonZeroExitCode) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithExecNonZeroExitCode) StartExec(id string, opts docker.StartExecOptions) error { +// return nil +//} +// +//func (d *fakeDockerClientWithExecNonZeroExitCode) InspectExec(id string) (*docker.ExecInspect, error) { +// return &docker.ExecInspect{ +// ID: "123", +// ExitCode: 127, +// }, nil +//} +// +//// A fake docker client to test exit code which result into Warning +//type fakeDockerClientWithExecExitCodeOne struct { +//} +// +//func (d *fakeDockerClientWithExecExitCodeOne) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithExecExitCodeOne) StartExec(id string, opts docker.StartExecOptions) error { +// fmt.Fprint(opts.OutputStream, "output") +// return nil +//} +// +//func (d *fakeDockerClientWithExecExitCodeOne) InspectExec(id string) (*docker.ExecInspect, error) { +// return &docker.ExecInspect{ +// ID: "123", +// ExitCode: 1, +// }, nil +//} +// +//// A fake docker client to simulate create exec failing +//type fakeDockerClientWithCreateExecFailure struct { +//} +// +//func (d *fakeDockerClientWithCreateExecFailure) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return nil, errors.New("Exec Creation Failed") +//} +// +//func (d *fakeDockerClientWithCreateExecFailure) StartExec(id string, opts docker.StartExecOptions) error { +// return errors.New("Exec doesn't exist") +//} +// +//func (d *fakeDockerClientWithCreateExecFailure) InspectExec(id string) (*docker.ExecInspect, error) { +// return nil, errors.New("Exec doesn't exist") +//} +// +//// A fake docker client to simulate start exec failing +//type fakeDockerClientWithStartExecFailure struct { +//} +// +//func (d *fakeDockerClientWithStartExecFailure) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithStartExecFailure) StartExec(id string, opts docker.StartExecOptions) error { +// return errors.New("Couldn't Start Exec") +//} +// +//func (d *fakeDockerClientWithStartExecFailure) InspectExec(id string) (*docker.ExecInspect, error) { +// return nil, errors.New("Exec doesn't exist") +//} +// +//// A fake docker client to test exec info query failures +//type fakeDockerClientWithExecInfoErrors struct { +//} +// +//func (d *fakeDockerClientWithExecInfoErrors) CreateExec(opts docker.CreateExecOptions) (*docker.Exec, error) { +// return &docker.Exec{ID: "123"}, nil +//} +// +//func (d *fakeDockerClientWithExecInfoErrors) StartExec(id string, opts docker.StartExecOptions) error { +// return nil +//} +// +//func (d *fakeDockerClientWithExecInfoErrors) InspectExec(id string) (*docker.ExecInspect, error) { +// return nil, errors.New("Unable to query exec info") +//} +// +//func expectDockerCheckStatus(t *testing.T, dockerClient DockerClient, status string, output string) { +// notif := mock.NewNotify() +// check := &CheckDocker{ +// Notify: notif, +// CheckID: types.CheckID("foo"), +// Script: "/health.sh", +// DockerContainerID: "54432bad1fc7", +// Shell: "/bin/sh", +// Interval: 25 * time.Millisecond, +// Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), +// dockerClient: dockerClient, +// } +// check.Start() +// defer check.Stop() +// +// time.Sleep(250 * time.Millisecond) +// +// // Should have at least 2 updates +// if notif.Updates("foo") < 2 { +// t.Fatalf("should have 2 updates %v", notif.UpdatesMap()) +// } +// +// if notif.State("foo") != status { +// t.Fatalf("should be %v %v", status, notif.StateMap()) +// } +// +// if notif.Output("foo") != output { +// t.Fatalf("should be %v %v", output, notif.OutputMap()) +// } +//} +// +//func TestDockerCheckWhenExecReturnsSuccessExitCode(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithNoErrors{}, api.HealthPassing, "output") +//} +// +//func TestDockerCheckWhenExecCreationFails(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithCreateExecFailure{}, api.HealthCritical, "Unable to create Exec, error: Exec Creation Failed") +//} +// +//func TestDockerCheckWhenExitCodeIsNonZero(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithExecNonZeroExitCode{}, api.HealthCritical, "") +//} +// +//func TestDockerCheckWhenExitCodeIsone(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithExecExitCodeOne{}, api.HealthWarning, "output") +//} +// +//func TestDockerCheckWhenExecStartFails(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithStartExecFailure{}, api.HealthCritical, "Unable to start Exec: Couldn't Start Exec") +//} +// +//func TestDockerCheckWhenExecInfoFails(t *testing.T) { +// t.Parallel() +// expectDockerCheckStatus(t, &fakeDockerClientWithExecInfoErrors{}, api.HealthCritical, "Unable to inspect Exec: Unable to query exec info") +//} +// +//func TestDockerCheckDefaultToSh(t *testing.T) { +// t.Parallel() +// os.Setenv("SHELL", "") +// notif := mock.NewNotify() +// check := &CheckDocker{ +// Notify: notif, +// CheckID: types.CheckID("foo"), +// Script: "/health.sh", +// DockerContainerID: "54432bad1fc7", +// Interval: 10 * time.Millisecond, +// Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), +// dockerClient: &fakeDockerClientWithNoErrors{}, +// } +// check.Start() +// defer check.Stop() +// +// time.Sleep(50 * time.Millisecond) +// if check.Shell != "/bin/sh" { +// t.Fatalf("Shell should be: %v , actual: %v", "/bin/sh", check.Shell) +// } +//} +// +//func TestDockerCheckUseShellFromEnv(t *testing.T) { +// t.Parallel() +// notif := mock.NewNotify() +// os.Setenv("SHELL", "/bin/bash") +// check := &CheckDocker{ +// Notify: notif, +// CheckID: types.CheckID("foo"), +// Script: "/health.sh", +// DockerContainerID: "54432bad1fc7", +// Interval: 10 * time.Millisecond, +// Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), +// dockerClient: &fakeDockerClientWithNoErrors{}, +// } +// check.Start() +// defer check.Stop() +// +// time.Sleep(50 * time.Millisecond) +// if check.Shell != "/bin/bash" { +// t.Fatalf("Shell should be: %v , actual: %v", "/bin/bash", check.Shell) +// } +// os.Setenv("SHELL", "") +//} +// +//func TestDockerCheckTruncateOutput(t *testing.T) { +// t.Parallel() +// notif := mock.NewNotify() +// check := &CheckDocker{ +// Notify: notif, +// CheckID: types.CheckID("foo"), +// Script: "/health.sh", +// DockerContainerID: "54432bad1fc7", +// Shell: "/bin/sh", +// Interval: 10 * time.Millisecond, +// Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), +// dockerClient: &fakeDockerClientWithLongOutput{}, +// } +// check.Start() +// defer check.Stop() +// +// time.Sleep(50 * time.Millisecond) +// +// // Allow for extra bytes for the truncation message +// if len(notif.Output("foo")) > CheckBufSize+100 { +// t.Fatalf("output size is too long") +// } +//} diff --git a/agent/docker.go b/agent/docker.go new file mode 100644 index 000000000000..3e3c32257a46 --- /dev/null +++ b/agent/docker.go @@ -0,0 +1,135 @@ +package agent + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "strings" + + "github.com/armon/circbuf" +) + +// DockerClient is a simplified client for the Docker Engine API +// to execute the health checks and avoid significant dependencies. +// It also consumes all data returned from the Docker API through +// a ring buffer with a fixed limit to avoid excessive resource +// consumption. +type DockerClient struct { + network string + addr string + baseurl string + maxbuf int64 + client *http.Client +} + +func NewDockerClient(host string, maxbuf int64) (*DockerClient, error) { + if host == "" { + host = DefaultDockerHost + } + p := strings.SplitN(host, "://", 2) + if len(p) == 1 { + return nil, fmt.Errorf("invalid docker host: %s", host) + } + network, addr := p[0], p[1] + basepath := "http://" + addr + if network == "unix" { + basepath = "http://unix" + } + client := &http.Client{} + if network == "unix" { + client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return net.Dial(network, addr) + }, + } + } + return &DockerClient{network, addr, basepath, maxbuf, client}, nil +} + +func (c *DockerClient) call(method, uri string, okStatus int, v interface{}) (*circbuf.Buffer, error) { + urlstr := c.baseurl + uri + req, err := http.NewRequest(method, urlstr, nil) + if err != nil { + return nil, err + } + + if v != nil { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(v); err != nil { + return nil, err + } + req.Body = ioutil.NopCloser(&b) + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, err := circbuf.NewBuffer(c.maxbuf) + if err != nil { + return nil, err + } + _, err = io.Copy(b, resp.Body) + if err == nil && resp.StatusCode != okStatus { + err = fmt.Errorf("bad status code: %s %d %s", urlstr, resp.StatusCode, b) + } + return b, err +} + +func (c *DockerClient) CreateExec(containerID string, cmd []string) (string, error) { + data := struct { + AttachStdin bool + AttachStdout bool + AttachStderr bool + Tty bool + Cmd []string + }{ + AttachStderr: true, + AttachStdout: true, + Cmd: cmd, + } + + uri := fmt.Sprintf("/containers/%s/exec", containerID) + b, err := c.call("POST", uri, http.StatusCreated, data) + if err != nil { + return "", fmt.Errorf("create exec: %v", err) + } + + var resp struct{ Id string } + if err = json.NewDecoder(bytes.NewReader(b.Bytes())).Decode(&resp); err != nil { + return "", fmt.Errorf("error in create exec: %v", err) + } + + return resp.Id, nil +} + +func (c *DockerClient) StartExec(execID string) (*circbuf.Buffer, error) { + data := struct{ Detach, Tty bool }{Detach: false, Tty: true} + uri := fmt.Sprintf("/exec/%s/start", execID) + b, err := c.call("POST", uri, http.StatusOK, data) + if err != nil { + return nil, fmt.Errorf("error in exec start: %v %s", err, b) + } + return b, nil +} + +func (c *DockerClient) InspectExec(execID string) (int, error) { + uri := fmt.Sprintf("/exec/%s/json", execID) + b, err := c.call("GET", uri, http.StatusOK, nil) + if err != nil { + return 0, fmt.Errorf("error in exec inspect: %v %s", err, b) + } + var resp struct{ ExitCode int } + if err := json.NewDecoder(bytes.NewReader(b.Bytes())).Decode(&resp); err != nil { + return 0, fmt.Errorf("error in exec inspect: %v %s", err, b) + } + return resp.ExitCode, nil +} diff --git a/agent/docker_unix.go b/agent/docker_unix.go new file mode 100644 index 000000000000..529a5ead1892 --- /dev/null +++ b/agent/docker_unix.go @@ -0,0 +1,3 @@ +package agent + +const DefaultDockerHost = "unix:///var/run/docker.sock" diff --git a/agent/docker_windows.go b/agent/docker_windows.go new file mode 100644 index 000000000000..0046ea97568c --- /dev/null +++ b/agent/docker_windows.go @@ -0,0 +1,3 @@ +package agent + +const DefaultDockerHost = "npipe:////./pipe/docker_engine"