From 91966fcbbd6dbf9f10806fe86145a904546a2278 Mon Sep 17 00:00:00 2001 From: Dominic Ricottone Date: Mon, 5 Jun 2023 20:21:45 -0500 Subject: [PATCH] Initial commit --- LICENSE.md | 31 ++++++++ Makefile | 12 ++++ README.md | 31 ++++++++ go.mod | 22 ++++++ main.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 main.go diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..41dc2f1 --- /dev/null +++ b/LICENSE.md @@ -0,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. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa8f9e8 --- /dev/null +++ b/Makefile @@ -0,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 $@ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..cffd78e --- /dev/null +++ b/README.md @@ -0,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. + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9827418 --- /dev/null +++ b/go.mod @@ -0,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 +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..33d592e --- /dev/null +++ b/main.go @@ -0,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) +} + -- 2.45.2