Using Docker Without Leaving Go

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
)

func main() {
	// Создаем клиента
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		// проверяем на ошибку
		log.Fatal(err)
	}
	// в конце программы закрываем клиента
	defer cli.Close()

	logger := slog.New(
		slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
	)

	ctx := context.Background()

	if err = pullImage("docker/welcome-to-docker", cli, &ctx, logger); err != nil {
		log.Fatal(err)
	}

	if err = buildImage("docker/welcome-to-docker", cli, &ctx, logger); err != nil {
		log.Fatal(err)
	}
}

func pullImage(
	imageName string,
	cli *client.Client,
	ctx *context.Context,
	logger *slog.Logger,
) error {
	// объявляем константу op, где будет храниться название действия для легкого определения источника ошибки
	const op = "pullImage"

	logger = logger.With(slog.String("op", op))

	// получаем мой образ. Аналог консольной команды docker pull egorklimenko/test-logs
	res, err := cli.ImagePull(*ctx, imageName, image.PullOptions{})
	// проверяем на ошибку
	if err != nil {
		return fmt.Errorf("%s: %w", op, err)
	}
	// в конце программы закрываем данные, которые получили. Не сам образ, а именно сообщение о его удачном получении
	defer res.Close()

	// из-за того, что полученные данные храняться в io.ReadCloser, их можно вывести в консоль таким образом
	io.Copy(os.Stdout, res)
	return nil
}

func buildImage(
	imageName string,
	cli *client.Client,
	ctx *context.Context,
	logger *slog.Logger,
) error {
	// объявляем константу op, где будет храниться название действия для легкого определения источника ошибки
	const op = "buildImage"

	logger = logger.With(slog.String("op", op))

	// создаем hostConfig, где будем привязывать порт 9990 к 0.0.0.0
	hostConfig := &container.HostConfig{
		PortBindings: nat.PortMap{
			// 80 - изначальный порт welcome-to-docker
			"80/tcp": []nat.PortBinding{
				{
					HostIP:   "0.0.0.0",
					HostPort: "9990", // новый порт
				},
			},
		},
	}

	// создаем контейнер с названием SuperContainer
	resp, err := cli.ContainerCreate(*ctx, &container.Config{
		Image: imageName,
		// tty - выключаем использование интерактивной консоли внутри контейнера
		Tty: false,
		// Указываем hostConfig
	}, hostConfig,
		nil, nil, "SuperContainer")
	// проверяем на ошибку
	if err != nil {
		return fmt.Errorf("%s: %w", op, err)
	}

	// стартуем наш контейнер
	if err = cli.ContainerStart(*ctx, resp.ID, container.StartOptions{}); err != nil {
		return fmt.Errorf("%s: %w", op, err)
	}

	return nil
}

After launching, try to go to “localhost:9990”. There you will see congratulations from Docker.

Enough congratulations

To stop the container that is running in the background, you need to write the command “sudo docker kill SuperContainer”, but let's do it on Goshka:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
)

func main() {
	// Создаем клиента
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		// проверяем на ошибку
		log.Fatal(err)
	}
	// в конце программы закрываем клиента
	defer cli.Close()

	// получаем containerID
	var containerID string
	fmt.Scan(&containerID)

	// Останавливаем контейнер. Есть одно интересное поле конфигурации остановки(StopOptions) - timeout. Про него расскажу под кодом
	if err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{}); err != nil {
		log.Fatal(err)
	}
}

Let's run a container in the background, for example “docker/welcome-to-docker”: sudo docker run -d -p 9990:80 docker/welcome-to-docker. Let's go to localhost:9990 and get another congratulations (why not). Read the list of running containers with the command docker ps -afind “welcome-to-docker” there and copy its “container id”. Now run our program on Goshka: sudo go run main.go and enter the container ID. Go to localhost:9990 again and feel sad because the congratulations have disappeared.

About timeout

What about timeout? This field means that if during the time passed in timeout from the moment the container is asked to stop it does not stop, it will be killed.

We stop all necessary and unnecessary containers

What if you want to stop all containers that are currently running? Let's try to solve this problem:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
)

func main() {
	// Создаем клиента
	cli, err := client.NewClientWithOpts(client.FromEnv)
	// проверяем на ошибку
	if err != nil {
		log.Fatal(err)
	}
	// в конце программы закрываем клиента
	defer cli.Close()

	// получаем список всех запущенных на данный момент контейнеров
	runningContainer, err := cli.ContainerList(context.Background(), container.ListOptions{})
	if err != nil {
		log.Fatal(err)
	}

	for _, c := range runningContainer {
		fmt.Printf("сейчас попрошу остановиться контейнер %s\n", c.ID[:10])
		// Останавливаем контейнер
		if err = cli.ContainerStop(context.Background(), c.ID, container.StopOptions{}); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("контейнер %s больше не бежит\n", c.ID[:10])
	}
}

Statistics, statistics

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
)

func main() {
	// Создаем клиента
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		// проверяем на ошибку
		log.Fatal(err)
	}
	// в конце программы закрываем клиента
	defer cli.Close()

	logger := slog.New(
		slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
	)

	ctx := context.Background()

	if err = pullImage("docker/welcome-to-docker", cli, &ctx, logger); err != nil {
		log.Fatal(err)
	}
	containerID, err := buildImage("docker/welcome-to-docker", cli, &ctx, logger)
	if err != nil {
		log.Fatal(err)
	}

	if err = getStats(&ctx, cli, containerID); err != nil {
		log.Fatal(err)
	}
}

func pullImage(
	imageName string,
	cli *client.Client,
	ctx *context.Context,
	logger *slog.Logger,
) error {
	// объявляем константу op, где будет храниться название действия для легкого определения источника ошибки
	const op = "pullImage"

	logger = logger.With(slog.String("op", op))

	// получаем мой образ. Аналог консольной команды docker pull egorklimenko/test-logs
	res, err := cli.ImagePull(*ctx, imageName, image.PullOptions{})
	// проверяем на ошибку
	if err != nil {
		return fmt.Errorf("%s: %w", op, err)
	}
	// в конце программы закрываем данные, которые получили. Не сам образ, а именно сообщение о его удачном получении
	defer res.Close()

	// из-за того, что полученные данные храняться в io.ReadCloser, их можно вывести в консоль таким образом
	io.Copy(os.Stdout, res)
	return nil
}

func buildImage(
	imageName string,
	cli *client.Client,
	ctx *context.Context,
	logger *slog.Logger,
) (containerID string, err error) {
	// объявляем константу op, где будет храниться название действия для легкого определения источника ошибки
	const op = "buildImage"

	logger = logger.With(slog.String("op", op))

	// создаем hostConfig, где будем привязывать порт 9990 к 0.0.0.0
	hostConfig := &container.HostConfig{
		PortBindings: nat.PortMap{
			// 80 - изначальный порт welcome-to-docker
			"80/tcp": []nat.PortBinding{
				{
					HostIP:   "0.0.0.0",
					HostPort: "9990", // новый порт
				},
			},
		},
	}

	// создаем контейнер с названием SuperContainer
	resp, err := cli.ContainerCreate(*ctx, &container.Config{
		Image: imageName,
		// tty - выключаем использование интерактивной консоли внутри контейнера
		Tty: false,
		// Указываем hostConfig
	}, hostConfig,
		nil, nil, "SuperContainer")
	// проверяем на ошибку
	if err != nil {
		return "", fmt.Errorf("%s: %w", op, err)
	}

	// стартуем наш контейнер
	if err = cli.ContainerStart(*ctx, resp.ID, container.StartOptions{}); err != nil {
		return "", fmt.Errorf("%s: %w", op, err)
	}

	return resp.ID, nil
}

func getStats(ctx *context.Context, cli *client.Client, containerID string) error {
	const op = "getStats"

	// получаем статистику
	stats, err := cli.ContainerStats(*ctx, containerID, false)
	if err != nil {
		return fmt.Errorf("%s: %w", op, err)
	}
	defer stats.Body.Close()

	io.Copy(os.Stdout, stats.Body)

	return nil
}

After launching you will see a huge amount of text in the console, if not – then something is wrong. You can work with this data in the future

Conclusion

I think that's all, this is certainly not a complete guide, but I tried to introduce you to the concept of the Docker SDK, so that it would be easier later. For a full introduction to it, there is official documentation.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *