~dricottone/moby-demo

91966fcbbd6dbf9f10806fe86145a904546a2278 — Dominic Ricottone 1 year, 5 months ago dev
Initial commit
5 files changed, 301 insertions(+), 0 deletions(-)

A LICENSE.md
A Makefile
A README.md
A go.mod
A main.go
A  => LICENSE.md +31 -0
@@ 1,31 @@
BSD 3-Clause License
====================

_Copyright (c) 2021, Dominic Ricottone_  
_All rights reserved._

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


A  => Makefile +12 -0
@@ 1,12 @@
moby-demo: dir1 dir2
	go get -u
	go build .

.PHONY: clean
clean:
	rm --force go.sum moby-demo
	rm --force --recursive dir1 dir2

dir%:
	mkdir $@


A  => README.md +31 -0
@@ 1,31 @@
# moby-demo

A demo application for Moby, the library that powers Docker.

```bash
$ make
mkdir dir1
mkdir dir2
go get -u
go build .
$ ./moby-demo
uname -a
(exited with 0)
eLinux 9e1114f658a9 6.3.5-arch1-1 #1 SMP PREEMPT_DYNAMIC Tue, 30 May 2023 13:44:01 +0000 x86_64 Linux
$ ./moby-demo whoami
whoami
(exited with 0)
root
$ make clean
rm --force go.sum moby-demo
rm --force --recursive dir1 dir2
```

This source code demonstrates pulling and removing images;
creating, starting, stopping, and interacting with containers;
and how common options are set with this API.

## License

I license this work under the BSD 3-clause license.


A  => go.mod +22 -0
@@ 1,22 @@
module git.dominic-ricottone.com/~dricottone/moby-demo

go 1.20

require (
	github.com/docker/docker v24.0.2+incompatible
	github.com/opencontainers/image-spec v1.0.2
)

require (
	github.com/Microsoft/go-winio v0.6.1 // indirect
	github.com/docker/distribution v2.8.2+incompatible // indirect
	github.com/docker/go-connections v0.4.0 // indirect
	github.com/docker/go-units v0.5.0 // indirect
	github.com/gogo/protobuf v1.3.2 // indirect
	github.com/opencontainers/go-digest v1.0.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	golang.org/x/mod v0.10.0 // indirect
	golang.org/x/net v0.10.0 // indirect
	golang.org/x/sys v0.8.0 // indirect
	golang.org/x/tools v0.9.3 // indirect
)

A  => main.go +205 -0
@@ 1,205 @@
package main

import (
	"bufio"
	"context"
	"path/filepath"
	"fmt"
	"io"
	"os"
	"os/signal"
	"strings"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/client"
	specs "github.com/opencontainers/image-spec/specs-go/v1"
)

// Bind mounts require absolute paths for the source directory.
func makeAbsolute(rel_path string) string {
	abs_path, err := filepath.Abs(rel_path)
	if err != nil {
		panic(err)
	}
	return abs_path
}

// Create a container, start it, wait for it to stop running, and remove it.
func runContainer(cli *client.Client, ctx context.Context, args []string) {
	pullImage(cli, ctx)

	id := createContainer(cli, ctx, args)

	start_opts := types.ContainerStartOptions{
	}
	cli.ContainerStart(ctx, id, start_opts)

	watchContainer(cli, ctx, id)

	rm_opts := types.ContainerRemoveOptions{
		Force: true,
	}
	cli.ContainerRemove(ctx, id, rm_opts)
}

func createContainer(cli *client.Client, ctx context.Context, args []string) string {
	conf := container.Config{
		Image: "alpine:latest",
		Cmd: args,
	}

	con_conf := container.HostConfig{
		Mounts: []mount.Mount{
			{
				Type: mount.TypeBind,
				Source: makeAbsolute("dir1"),
				Target: "/dir1",
				ReadOnly: true,
			},
			{
				Type: mount.TypeBind,
				Source: makeAbsolute("dir2"),
				Target: "/dir2",
				ReadOnly: false,
			},
		},
	}

	net_conf := network.NetworkingConfig{
	}

	plats := specs.Platform{
		Architecture: "amd64", //"arm64"
		OS: "linux",
	}

	con, err := cli.ContainerCreate(ctx, &conf, &con_conf, &net_conf, &plats, "")
	if err != nil {
		panic(err)
	}

	return con.ID
}

// Pull an image from DockerHub. We aren't particularly concerned about the
// output so it's thrown away.
func pullImage(cli *client.Client, ctx context.Context) {
	opts := types.ImagePullOptions{
		Platform: "amd64", // "arm64"
	}

	out, err := cli.ImagePull(ctx, "alpine:latest", opts)
	if err != nil {
		panic(err)
	}
	defer out.Close()

	buf := make([]byte, 512)
	for {
		if _, err := out.Read(buf); err == io.EOF {
			break
		}
	}
}

// Watch for a container to stop running. Also handle user interrupts.
func watchContainer(cli *client.Client, ctx context.Context, id string) {
	statusC, errC := cli.ContainerWait(ctx, id, container.WaitConditionNotRunning)

	sigC := make(chan os.Signal)
	signal.Notify(sigC, os.Interrupt)

	select {
	case _ = <-sigC:
		fmt.Println("(caught SIGINT)")

	case err := <-errC:
		if err != nil {
			fmt.Println("An error occured with the docker daemon")
		}

	case status := <-statusC:
		fmt.Printf("(exited with %d)\n", status.StatusCode)
	}

	dumpContainerLogs(cli, ctx, id)

	imageRemove(cli, ctx)
}

// Print logs from a container.
func dumpContainerLogs(cli *client.Client, ctx context.Context, id string) {
	conf := types.ContainerLogsOptions{
		ShowStdout: true,
	}

	out, err := cli.ContainerLogs(ctx, id, conf)
	if err != nil {
		panic(err)
	}
	defer out.Close()

	scanner := bufio.NewScanner(out)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}

// Remove an image.
func imageRemove(cli *client.Client, ctx context.Context) {
	opts := types.ImageRemoveOptions{
		Force: true,
	}

	id := identifyImage(cli, ctx)

	_, err := cli.ImageRemove(ctx, id, opts)
	if err != nil {
		panic(err)
	}
}

// Get the ID of an image.
func identifyImage(cli *client.Client, ctx context.Context) string {
	opts := types.ImageListOptions{}

	images, err := cli.ImageList(ctx, opts)
	if err != nil {
		panic(err)
	}

	for _, image := range images {
		for _, tag := range image.RepoTags {
			if tag == "alpine:latest" {
				return image.ID
			}
		}
	}

	return ""
}

func main() {
	// Need a client to communicate with `dockerd(8)`
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		panic(err)
	}

	// Need a context for execution
	ctx := context.Background()

	// A command to run in an Alpine `sh(1)`
	args := os.Args[1:]
	if len(args) == 0 {
		args = []string{"uname", "-a"}
	}
	fmt.Println(strings.Join(args, " "))

	// Run the container
	runContainer(cli, ctx, args)
}