Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not Vladimir Eremin.
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package dockerclient
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/client"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type streamErrorConn struct {
|
|
net.Conn
|
|
err error
|
|
}
|
|
|
|
func (c streamErrorConn) Read(p []byte) (int, error) {
|
|
n, err := c.Conn.Read(p)
|
|
if errors.Is(err, io.EOF) && c.err != nil {
|
|
return n, c.err
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func TestExecCommand(t *testing.T) {
|
|
streamErr := errors.New("exec stream interrupted")
|
|
for _, tc := range []struct {
|
|
name string
|
|
exitCode int
|
|
running bool
|
|
inspectStatus int
|
|
streamErr error
|
|
wantError string
|
|
}{
|
|
{name: "successful exit", inspectStatus: http.StatusOK},
|
|
{name: "nonzero exit", exitCode: 23, inspectStatus: http.StatusOK, wantError: "exit code 23"},
|
|
{name: "still running", running: true, inspectStatus: http.StatusOK, wantError: "still running"},
|
|
{name: "inspect failure", inspectStatus: http.StatusInternalServerError, wantError: "inspect failed"},
|
|
{name: "stream failure", streamErr: streamErr, inspectStatus: http.StatusOK, wantError: streamErr.Error()},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var inspections atomic.Int32
|
|
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
local, remote := net.Pipe()
|
|
_ = local.SetDeadline(time.Now().Add(5 * time.Second))
|
|
_ = remote.SetDeadline(time.Now().Add(5 * time.Second))
|
|
t.Cleanup(func() { _ = local.Close(); _ = remote.Close() })
|
|
go func() {
|
|
defer remote.Close()
|
|
req, err := http.ReadRequest(bufio.NewReader(remote))
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
_, err = io.Copy(io.Discard, req.Body)
|
|
assert.NoError(t, err)
|
|
_ = req.Body.Close()
|
|
status, body := http.StatusCreated, `{"Id":"test-exec"}`
|
|
switch req.URL.Path {
|
|
case "/v1.47/containers/test-container/exec":
|
|
assert.Equal(t, http.MethodPost, req.Method)
|
|
case "/v1.47/exec/test-exec/start":
|
|
_, err = io.WriteString(remote, "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\nhook output")
|
|
assert.NoError(t, err)
|
|
return
|
|
case "/v1.47/exec/test-exec/json":
|
|
inspections.Add(1)
|
|
status = tc.inspectStatus
|
|
body = fmt.Sprintf(`{"Running":%t,"ExitCode":%d}`, tc.running, tc.exitCode)
|
|
if status != http.StatusOK {
|
|
body = `{"message":"inspect failed"}`
|
|
}
|
|
default:
|
|
t.Errorf("unexpected Docker API request: %s", req.URL.Path)
|
|
return
|
|
}
|
|
_, err = fmt.Fprintf(remote, "HTTP/1.1 %d %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", status, http.StatusText(status), len(body), body)
|
|
assert.NoError(t, err)
|
|
}()
|
|
return streamErrorConn{Conn: local, err: tc.streamErr}, nil
|
|
}
|
|
cli, err := client.NewClientWithOpts(client.WithHost("tcp://docker.invalid:2375"), client.WithVersion("1.47"), client.WithDialContext(dial))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = cli.Close() })
|
|
c := &DefaultClient{cli: cli}
|
|
err = c.ExecCommand(context.Background(), "test-container", []string{"sh", "-c", "synthetic hook"})
|
|
if tc.wantError == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.wantError)
|
|
assert.NotContains(t, fmt.Sprint(err), "synthetic hook")
|
|
}
|
|
if tc.streamErr != nil {
|
|
assert.ErrorIs(t, err, tc.streamErr)
|
|
assert.Zero(t, inspections.Load())
|
|
} else {
|
|
assert.EqualValues(t, 1, inspections.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|