mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
293 lines
7.2 KiB
Go
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"
|