It's a blog

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.

3 Comments

  1. thadguidry

    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

  2. thadguidry

    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 ?

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2021 Addshore

Theme by Anders NorénUp ↑

%d bloggers like this: