![](/style/images/good.png)
![](/style/images/bad.png)
Controlling docker in golang
source link: https://willschenk.com/articles/2021/controlling_docker_in_golang/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
So meta
Published May 15, 2021 #golang, #docker
I've been thinking about running different ephemeral jobs with attached volumes, volumes that I could garbage collect as needed. This is a non-standard way of using docker, but I wanted to look to see how I could interact with the docker daemon programatically.
The use case is:
Create a docker volume for a container
Start up a docker container, with a specified environment
Monitor the running of the container, kill if its running for too long
Capture the output of the container
Clean up the container
Pull data from the volume
Clean up the volume
Setting up go environemnt
First we need to setup a go project and create the go.mod
file.
mkdir dockeringo
cd dockeringo
go mod init dockeringo
Then we can add the modules that we'll need. In our case, our go.mod
file should look like:
module dockeringo
go 1.16
require (
github.com/containerd/containerd v1.5.1 // indirect
github.com/docker/docker v20.10.6+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
google.golang.org/grpc v1.37.1 // indirect
)
And we can get those modules by
go mod download
This will create a go.sum
file that is basically your dependancies.
Our types
We are going to build a simple controller type to hang our methods off
of. Create a types.go
file:
package dockeringo
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type Controller struct {
cli *client.Client
}
type VolumeMount struct {
HostPath string
Volume *types.Volume
}
func NewController() (c *Controller, err error) {
c = new(Controller)
c.cli, err = client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
return c, nil
}
Images
package dockeringo
import (
"context"
"io"
"os"
"github.com/docker/docker/api/types"
)
//https://gist.github.com/miguelmota/4980b18d750fb3b1eb571c3e207b1b92
func (c *Controller) EnsureImage(image string) (err error) {
reader, err := c.cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
if err != nil {
return err
}
defer reader.Close()
io.Copy(os.Stdout, reader)
return nil
}
Then create a simple test in images_test.go
:
package dockeringo
import "testing"
func TestEnsureImage(t *testing.T) {
c, err := NewController()
if err != nil {
t.Error(err)
t.FailNow()
}
err = c.EnsureImage("alpine")
if err != nil {
t.Error(err)
}
}
We can run the test with:
go test --run Image
{"status":"Pulling from library/alpine","id":"latest"} {"status":"Digest: sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f"} {"status":"Status: Image is up to date for alpine:latest"} PASS ok dockeringo 0.685s
This makes sure that we have the image we want to run on our machine.
Container logs
Let's write a simple way to get the logs of a container. We won't write a test for this here, since we need to write the container run examples first.
container_log.go
:
package dockeringo
import (
"context"
"io"
"time"
"github.com/docker/docker/api/types"
)
func (c *Controller) ContainerLog(id string) (result string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
reader, err := c.cli.ContainerLogs(ctx, id, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true})
if err != nil {
return "", err
}
buffer, err := io.ReadAll(reader)
if err != nil && err != io.EOF {
return "", err
}
return string(buffer), nil
}
Running a container
container_run.go
:
package dockeringo
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
)
func (c *Controller) ContainerRun(image string, command []string, volumes []VolumeMount) (id string, err error) {
hostConfig := container.HostConfig{}
// hostConfig.Mounts = make([]mount.Mount,0);
var mounts []mount.Mount
for _, volume := range volumes {
mount := mount.Mount{
Type: mount.TypeVolume,
Source: volume.Volume.Name,
Target: volume.HostPath,
}
mounts = append(mounts, mount)
}
hostConfig.Mounts = mounts
resp, err := c.cli.ContainerCreate(context.Background(), &container.Config{
Tty: true,
Image: image,
Cmd: command,
}, &hostConfig, nil, nil, "")
if err != nil {
return "", err
}
err = c.cli.ContainerStart(context.Background(), resp.ID, types.ContainerStartOptions{})
if err != nil {
return "", err
}
return resp.ID, nil
}
func (c *Controller) ContainerWait(id string) (state int64, err error) {
resultC, errC := c.cli.ContainerWait(context.Background(), id, "")
select {
case err := <-errC:
return 0, err
case result := <-resultC:
return result.StatusCode, nil
}
}
func (c *Controller) ContainerRunAndClean(image string, command []string, volumes []VolumeMount) (statusCode int64, body string, err error) {
// Start the container
id, err := c.ContainerRun(image, command, volumes)
if err != nil {
return statusCode, body, err
}
// Wait for it to finish
statusCode, err = c.ContainerWait(id)
if err != nil {
return statusCode, body, err
}
// Get the log
body, _ = c.ContainerLog(id)
err = c.cli.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{})
if err != nil {
fmt.Printf("Unable to remove container %q: %q\n", id, err)
}
return statusCode, body, err
}
Now we can write a test to see if everything is running:
container_run_test.go
:
package dockeringo
import (
"testing"
)
func TestContainerRun(t *testing.T) {
c, err := NewController()
if err != nil {
t.Error(err)
}
statusCode, body, err := c.ContainerRunAndClean("alpine", []string{"echo", "hello world"}, []VolumeMount{})
if err != nil {
t.Error(err)
t.FailNow()
}
if body != "hello world\r\n" {
t.Errorf("Expected 'hello world'; received %q\n", body)
}
if statusCode != 0 {
t.Errorf( "Expect status to be 0; received %q\n", statusCode);
}
}
And the run the test:
go test --run Container
PASS ok dockeringo 1.414s
I'm not saying that it's a great test, but it does test something!
Volumes
Containers have volumes, lets look at how to create them:
volumes.go
:
package dockeringo
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
volumetypes "github.com/docker/docker/api/types/volume"
)
func (c *Controller) FindVolume(name string) (volume *types.Volume, err error) {
volumes, err := c.cli.VolumeList(context.Background(), filters.NewArgs())
if err != nil {
return nil, err
}
for _, v := range volumes.Volumes {
if v.Name == name {
return v, nil
}
}
return nil, nil
}
func (c *Controller) EnsureVolume(name string) (created bool, volume *types.Volume, err error) {
volume, err = c.FindVolume(name)
if err != nil {
return false, nil, err
}
if volume != nil {
return false, volume, nil
}
vol, err := c.cli.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{
Driver: "local",
// DriverOpts: map[string]string{},
// Labels: map[string]string{},
Name: name,
})
return true, &vol, err
}
func (c *Controller) RemoveVolume(name string) (removed bool, err error) {
vol, err := c.FindVolume(name)
if err != nil {
return false, err
}
if vol == nil {
return false, nil
}
err = c.cli.VolumeRemove(context.Background(), name, true)
if err != nil {
return false, err
}
return true, nil
}
And lets write some tests:
volumes_test.go
:
package dockeringo
import (
"testing"
)
func TestSingleCreate(t *testing.T) {
c, err := NewController()
if err != nil {
t.Error(err)
}
created, _, err := c.EnsureVolume("myvolume")
if created != true {
t.Errorf("Should have created the volume the first time")
}
created, _, err = c.EnsureVolume("myvolume")
if created != false {
t.Errorf("Should not have created the volume the second time")
}
removed, err := c.RemoveVolume("myvolume")
if removed != true {
t.Errorf("Should have removed the volume")
}
}
func TestEnsureVolume(t *testing.T) {
c, err := NewController()
if err != nil {
t.Error(err)
}
_, volume, err := c.EnsureVolume("myvolume")
if err != nil {
t.Error(err)
}
if volume.Name != "myvolume" {
t.Errorf("Expected volume name to be %s; got %s\n", "myvolume", volume.Name)
t.FailNow()
}
removed, err := c.RemoveVolume("myvolume")
if err != nil {
t.Error(err)
}
if removed != true {
t.Errorf("Volume should have been removed but wasn't")
}
}
And now we can run the tests:
go test --run Volume
PASS ok dockeringo 3.245s
Testing persisent volumes
Lets first create a simple script that will look for a file, and if it finds it prints it out and exits with a success. If it doesn't find it, it created it with the current date, prints it out, and exits with a failure.
Call this script.sh
:
if [ ! -f "output" ]; then
date > output
cat output
exit 1
fi
cat output
exit 0
Now lets create a Dockerfile
that runs this:
FROM debian:10
COPY script.sh /usr/bin/
WORKDIR /volume
CMD "bash" "/usr/bin/script.sh"
And we'll build this with
docker build . -t testimage
Now lets create a persistent_volume_test.go
file, where we will
Create a volume
Start the
testimage
container with the volume mountedRun it a second time
Make sure that the output is the same
Remove the volume
package dockeringo
import "testing"
func TestPersistentVolume(t *testing.T) {
c, err := NewController()
if err != nil {
t.Error(err)
t.FailNow()
}
created, volume, err := c.EnsureVolume("persistentvolume")
if err != nil {
t.Error(err)
t.FailNow()
}
if created != true {
t.Errorf("Should have created a volume at the start")
}
mounts := []VolumeMount{
{
HostPath: "/volume",
Volume: volume,
},
}
statusCode, body1, err := c.ContainerRunAndClean("testimage", []string{}, mounts)
// Second run
statusCode, body2, err := c.ContainerRunAndClean("testimage", []string{}, mounts)
if err != nil {
t.Error(err)
t.FailNow()
}
if statusCode != 0 {
t.Error("Second run should not have created a file")
}
if body1 != body2 {
t.Errorf("%s\nShould have been equal to:\n%s\n", body1, body2)
}
c.RemoveVolume("persistentvolume")
}
And now, lets run it:
go test --run Persistent
PASS ok dockeringo 4.186s
Final thoughts
Docker is cool.
References
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK