4ef50eeae7
* feat: handle context cancelation during docker exec To allow interrupting docker exec (which could be long running) we process the log output in a go routine and handle context cancelation as well as command result. In case of context cancelation a CTRL+C is written into the docker container. This should be enough to terminate the running command. To make sure we do not get stuck during cleanup, we do set the cleanup contexts with a timeout of 5 minutes Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * feat: handle SIGTERM signal and abort run * test: on context cancel, abort running command This test makes sure that whenever the act Context was canceled, the currently running docker exec is sent a 0x03 (ctrl+c). Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: make sure the exec funcction handles command exit code This test makes sure that the exec function does handle docker command error results Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>
165 lines
4.1 KiB
Go
165 lines
4.1 KiB
Go
package container
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/client"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
func TestDocker(t *testing.T) {
|
|
ctx := context.Background()
|
|
client, err := GetDockerClient(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
|
|
ContextDir: "testdata",
|
|
ImageTag: "envmergetest",
|
|
})
|
|
|
|
err = dockerBuild(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
cr := &containerReference{
|
|
cli: client,
|
|
input: &NewContainerInput{
|
|
Image: "envmergetest",
|
|
},
|
|
}
|
|
env := map[string]string{
|
|
"PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin",
|
|
"RANDOM_VAR": "WITH_VALUE",
|
|
"ANOTHER_VAR": "",
|
|
"CONFLICT_VAR": "I_EXIST_IN_MULTIPLE_PLACES",
|
|
}
|
|
|
|
envExecutor := cr.extractFromImageEnv(&env)
|
|
err = envExecutor(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, map[string]string{
|
|
"PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:/this/path/does/not/exists/anywhere:/this/either",
|
|
"RANDOM_VAR": "WITH_VALUE",
|
|
"ANOTHER_VAR": "",
|
|
"SOME_RANDOM_VAR": "",
|
|
"ANOTHER_ONE": "BUT_I_HAVE_VALUE",
|
|
"CONFLICT_VAR": "I_EXIST_IN_MULTIPLE_PLACES",
|
|
}, env)
|
|
}
|
|
|
|
type mockDockerClient struct {
|
|
client.APIClient
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockDockerClient) ContainerExecCreate(ctx context.Context, id string, opts types.ExecConfig) (types.IDResponse, error) {
|
|
args := m.Called(ctx, id, opts)
|
|
return args.Get(0).(types.IDResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *mockDockerClient) ContainerExecAttach(ctx context.Context, id string, opts types.ExecStartCheck) (types.HijackedResponse, error) {
|
|
args := m.Called(ctx, id, opts)
|
|
return args.Get(0).(types.HijackedResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
|
|
args := m.Called(ctx, execID)
|
|
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
|
}
|
|
|
|
type endlessReader struct {
|
|
io.Reader
|
|
}
|
|
|
|
func (r endlessReader) Read(p []byte) (n int, err error) {
|
|
return 1, nil
|
|
}
|
|
|
|
type mockConn struct {
|
|
net.Conn
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockConn) Write(b []byte) (n int, err error) {
|
|
args := m.Called(b)
|
|
return args.Int(0), args.Error(1)
|
|
}
|
|
|
|
func (m *mockConn) Close() (err error) {
|
|
return nil
|
|
}
|
|
|
|
func TestDockerExecAbort(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
conn := &mockConn{}
|
|
conn.On("Write", mock.AnythingOfType("[]uint8")).Return(1, nil)
|
|
|
|
client := &mockDockerClient{}
|
|
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
|
|
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
|
|
Conn: conn,
|
|
Reader: bufio.NewReader(endlessReader{}),
|
|
}, nil)
|
|
|
|
cr := &containerReference{
|
|
id: "123",
|
|
cli: client,
|
|
input: &NewContainerInput{
|
|
Image: "image",
|
|
},
|
|
}
|
|
|
|
channel := make(chan error)
|
|
|
|
go func() {
|
|
channel <- cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
|
}()
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
cancel()
|
|
|
|
err := <-channel
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
|
|
conn.AssertExpectations(t)
|
|
client.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDockerExecFailure(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
conn := &mockConn{}
|
|
|
|
client := &mockDockerClient{}
|
|
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
|
|
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
|
|
Conn: conn,
|
|
Reader: bufio.NewReader(strings.NewReader("output")),
|
|
}, nil)
|
|
client.On("ContainerExecInspect", ctx, "id").Return(types.ContainerExecInspect{
|
|
ExitCode: 1,
|
|
}, nil)
|
|
|
|
cr := &containerReference{
|
|
id: "123",
|
|
cli: client,
|
|
input: &NewContainerInput{
|
|
Image: "image",
|
|
},
|
|
}
|
|
|
|
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
|
assert.Error(t, err, "exit with `FAILURE`: 1")
|
|
|
|
conn.AssertExpectations(t)
|
|
client.AssertExpectations(t)
|
|
}
|