Files
Memoh/cmd/memoh/docker.go
T

293 lines
7.2 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"golang.org/x/term"
)
type dockerComposeOptions struct {
projectDir string
files []string
profiles []string
envFile string
}
type serverContainerdOptions struct {
dockerComposeOptions
service string
namespace string
noNamespace bool
noTTY bool
}
func runDockerCompose(ctx context.Context, opts dockerComposeOptions, composeArgs []string) error {
dockerCmd, err := resolveDockerCommand(ctx)
if err != nil {
return err
}
args := append(buildComposeBaseArgs(opts), composeArgs...)
if err := runExternalCommand(ctx, opts.projectDir, dockerCmd, args); err != nil {
return fmt.Errorf("run docker compose: %w", err)
}
return nil
}
func runServerContainerd(ctx context.Context, opts serverContainerdOptions, ctrArgs []string) error {
dockerCmd, err := resolveDockerCommand(ctx)
if err != nil {
return err
}
args := buildContainerdExecArgs(opts, ctrArgs)
if err := runExternalCommand(ctx, opts.projectDir, dockerCmd, args); err != nil {
return fmt.Errorf("run ctr in %s service: %w", opts.service, err)
}
return nil
}
func resolveDockerCommand(ctx context.Context) ([]string, error) {
if _, err := exec.LookPath("docker"); err != nil {
return nil, errors.New("docker is not installed")
}
dockerCmd := []string{"docker"}
if !canRun(ctx, dockerCmd, "info") {
if _, err := exec.LookPath("sudo"); err == nil && canRun(ctx, []string{"sudo", "docker"}, "info") {
dockerCmd = []string{"sudo", "docker"}
} else {
return nil, errors.New("cannot connect to the Docker daemon")
}
}
if !canRun(ctx, dockerCmd, "compose", "version") {
return nil, errors.New("docker compose v2 is required")
}
return dockerCmd, nil
}
func canRun(ctx context.Context, command []string, args ...string) bool {
if len(command) == 0 {
return false
}
cmd := exec.CommandContext(ctx, command[0], append(command[1:], args...)...) //nolint:gosec // trusted local tooling
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
cmd.Stdin = nil
return cmd.Run() == nil
}
func runExternalCommand(ctx context.Context, dir string, base []string, args []string) error {
if len(base) == 0 {
return errors.New("missing command")
}
cmd := exec.CommandContext(ctx, base[0], append(base[1:], args...)...) //nolint:gosec // trusted local tooling
cmd.Dir = dir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func buildComposeBaseArgs(opts dockerComposeOptions) []string {
args := []string{"compose"}
if opts.projectDir != "" {
args = append(args, "--project-directory", opts.projectDir)
}
for _, file := range opts.files {
args = append(args, "-f", file)
}
for _, profile := range opts.profiles {
args = append(args, "--profile", profile)
}
if opts.envFile != "" {
args = append(args, "--env-file", opts.envFile)
}
return args
}
func buildContainerdExecArgs(opts serverContainerdOptions, ctrArgs []string) []string {
args := append(buildComposeBaseArgs(opts.dockerComposeOptions), "exec")
if opts.noTTY || shouldDisableTTY() {
args = append(args, "-T")
}
args = append(args, opts.service, "ctr")
if !opts.noNamespace && opts.namespace != "" && !containsCtrNamespaceArg(ctrArgs) {
args = append(args, "-n", opts.namespace)
}
return append(args, ctrArgs...)
}
func containsCtrNamespaceArg(args []string) bool {
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "-n", arg == "--namespace":
return true
case strings.HasPrefix(arg, "--namespace="), strings.HasPrefix(arg, "-n="):
return true
}
}
return false
}
func shouldDisableTTY() bool {
return !isTerminalFile(os.Stdin) || !isTerminalFile(os.Stdout)
}
func isTerminalFile(file *os.File) bool {
fd := file.Fd()
const maxInt = int(^uint(0) >> 1)
if fd > uintptr(maxInt) {
return false
}
return term.IsTerminal(int(fd))
}
func parseComposeOptions(args []string) (dockerComposeOptions, []string, error) {
opts := dockerComposeOptions{}
i := 0
for i < len(args) {
arg := args[i]
if arg == "--" {
i++
break
}
switch {
case arg == "--project-dir":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.projectDir = value
i = next
case strings.HasPrefix(arg, "--project-dir="):
opts.projectDir = strings.TrimPrefix(arg, "--project-dir=")
i++
case arg == "-f", arg == "--file":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.files = append(opts.files, value)
i = next
case strings.HasPrefix(arg, "--file="):
opts.files = append(opts.files, strings.TrimPrefix(arg, "--file="))
i++
case strings.HasPrefix(arg, "-f="):
opts.files = append(opts.files, strings.TrimPrefix(arg, "-f="))
i++
case arg == "--profile":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.profiles = append(opts.profiles, value)
i = next
case strings.HasPrefix(arg, "--profile="):
opts.profiles = append(opts.profiles, strings.TrimPrefix(arg, "--profile="))
i++
case arg == "--env-file":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.envFile = value
i = next
case strings.HasPrefix(arg, "--env-file="):
opts.envFile = strings.TrimPrefix(arg, "--env-file=")
i++
default:
return opts, args[i:], nil
}
}
return opts, args[i:], nil
}
func parseContainerdOptions(args []string) (serverContainerdOptions, []string, error) {
opts := serverContainerdOptions{
service: "server",
namespace: defaultContainerdNamespace(),
}
i := 0
for i < len(args) {
arg := args[i]
if arg == "--" {
i++
break
}
switch {
case arg == "--server-service":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.service = value
i = next
case strings.HasPrefix(arg, "--server-service="):
opts.service = strings.TrimPrefix(arg, "--server-service=")
i++
case arg == "--namespace":
value, next, err := requireNextValue(args, i, arg)
if err != nil {
return opts, nil, err
}
opts.namespace = value
opts.noNamespace = false
i = next
case strings.HasPrefix(arg, "--namespace="):
opts.namespace = strings.TrimPrefix(arg, "--namespace=")
opts.noNamespace = false
i++
case arg == "--no-namespace":
opts.noNamespace = true
i++
case arg == "--no-tty":
opts.noTTY = true
i++
default:
composeOpts, remaining, err := parseComposeOptions(args[i:])
if err != nil {
return opts, nil, err
}
opts.dockerComposeOptions = composeOpts
return opts, remaining, nil
}
}
return opts, args[i:], nil
}
func requireNextValue(args []string, index int, flagName string) (string, int, error) {
if index+1 >= len(args) {
return "", 0, fmt.Errorf("flag %s requires a value", flagName)
}
return args[index+1], index + 2, nil
}
func defaultContainerdNamespace() string {
cfg, err := provideConfig()
if err != nil {
return configDefaultContainerdNamespace
}
if strings.TrimSpace(cfg.Containerd.Namespace) == "" {
return configDefaultContainerdNamespace
}
return cfg.Containerd.Namespace
}
const configDefaultContainerdNamespace = "default"