Go Docker SDK, Raw Terminal Ctrl+C handling
I spent my weekend in working on a new project called dockerit. It’s a simple wrapper around Docker written in Go and making use of the Docker SDK.
One of the biggest sticking points for me, being fairly new with the Golang world, was trying to pass stdin stdout and stderr between the container and host terminal correctly, while also having good performance and doing the expected things (like Ctrl+C to cancel).
The full code for setting up and, interacting with and removing my container can be found here. The main steps are broken down below.
Breakdown
Firstly a Docker client is needed.
I highly recommend using client.WithAPIVersionNegotiation()
so that your code will work out of the box with multiple Docker versions.
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
Then we need to make a container to work with. Here using the “ubuntu” image, and all of the options needed to correctly pass through stdin, stdout, stderr.
cont, err := cli.ContainerCreate(
context.Background(),
&container.Config{
Image: "ubuntu",
AttachStderr:true,
AttachStdin: true,
Tty: true,
AttachStdout:true,
OpenStdin: true,
Labels: labels,
},
&container.HostConfig{
AutoRemove: true,
},
nil,
nil,
"",
);
Code language: JavaScript (javascript)
We can then attach to the container before we start it. The io.Copy
calls deal with data coming from the container to the host. Stdin will be handled a different way.
waiter, err := cli.ContainerAttach(context.Background(), cont.ID, types.ContainerAttachOptions{
Stderr: true,
Stdout: true,
Stdin: true,
Stream: true,
})
go io.Copy(os.Stdout, waiter.Reader)
go io.Copy(os.Stderr, waiter.Reader)
Code language: JavaScript (javascript)
Next we want to start the container.
err = cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
And then to the fun. This code section is essentially split into two.
The first bit sets the terminal to Raw mode using MakeRaw
.
The second bit starts reading from Stdin, byte by byte, looking for the a Ctrl+C press. If found it will start removing the container. If not found in the byte then the byte is sent to the container waiter that we created earlier when attaching to the container.
fd := int(os.Stdin.Fd())
var oldState *terminal.State
if terminal.IsTerminal(fd) {
oldState, err = terminal.MakeRaw(fd)
if err != nil {
// TODO handle error?
}
go func() {
for {
consoleReader := bufio.NewReaderSize(os.Stdin, 1)
input, _ := consoleReader.ReadByte()
// Ctrl-C = 3
if input == 3 {
cli.ContainerRemove( context.Background(), cont.ID, types.ContainerRemoveOptions{
Force: true,
} )
}
waiter.Conn.Write([]byte{input})
}
}()
}
Code language: JavaScript (javascript)
So now the container is running, and waiting to end naturally, or for Ctrl+C to be fired and the container stopped. We don’t want to stop execution, so we must wait for the container!
statusCh, errCh := cli.ContainerWait(context.Background(), cont.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
panic(err)
}
case <-statusCh:
}
Code language: PHP (php)
And once the container is no longer running, we need to restore the terminal to its previous state.
if terminal.IsTerminal(fd) {
terminal.Restore(fd, oldState)
}
And for good measure, make sure that the container has stopped.
cli.ContainerRemove( context.Background(), cont.ID, types.ContainerRemoveOptions{
Force: true,
} )
Code language: CSS (css)
You can probably do some of this cleanup using defer, but I skipped that in this example for clarity.
You might also look at Gitlab Runner source code which does a few interesting similar things with Docker. https://gitlab.com/gitlab-org/gitlab-runner
In production, I typically just use Ansible for driving automation.https://docs.ansible.com/ansible/latest/scenario_guides/guide_docker.html
You can even make Ansible playbooks and turn them into containers with https://github.com/ansible-community/ansible-bender
But it’s cool that your learning how to build your own automation tools… but I wouldn’t spend much time there… the community already has really good CI/CD tools for Docker depending on your language of choice.
Did you see https://godoc.org/docker.io/go-docker ?
Indeed, this code uses https://godoc.org/docker.io/go-docker :)
This was mainly a way to explore what the Docker SDK lets you do, as I’m thinking about using it in some tooling for a MediaWiki development environment.
Currently that code does lots of shelling out to docker-compose https://github.com/addshore/mediawiki-docker-dev
I haven’t merged your code to mine, yet. Say I merge it, do a docker exec with your code, and in my container to a ‘tail -f $SOME_FILE’, and then CTRL+C.. will it stop the tail command, or exit from docker exec ?
The CTRL+C will call for the container to be removed using the docker API, as a result any processes in the container should be stopped.