Перейти до змісту

Docker CLI за 500 рядків Go

Docker CLI за 500 рядків Go

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Sheep має CLI, який працює як Docker: sheep run, sheep ps, sheep stop. Все в одному файлі на 500 рядків. Без cobra, без urfave/cli - тільки стандартна бібліотека.

Маршрутизація команд

func main() {
    if len(os.Args) < 2 {
        printUsage()
        os.Exit(1)
    }

    switch os.Args[1] {
    case "init":
        handleInit()   // re-exec для контейнера
        return
    case "version":
        fmt.Println("sheep v0.1.0")
        return
    }

    mgr := container.NewManager(dataDir)
    mgr.Init()

    switch os.Args[1] {
    case "run":     cmdRun(mgr)
    case "create":  cmdCreate(mgr)
    case "start":   cmdStart(mgr)
    case "stop":    cmdStop(mgr)
    case "rm":      cmdRemove(mgr)
    case "ps":      cmdPs(mgr)
    case "inspect": cmdInspect(mgr)
    case "images":  cmdImages(mgr)
    case "pull":    cmdPull(mgr)
    case "push":    cmdPush(mgr)
    case "import":  cmdImport(mgr)
    case "bootstrap": cmdBootstrap(mgr)
    default:
        fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
        os.Exit(1)
    }
}

Перший switch перехоплює init - це re-exec від дочірнього контейнерного процесу, тому Manager не ініціалізується. Другий switch - звичайні команди.

Парсинг прапорців для run

func parseRunFlags(args []string) container.RunOpts {
    opts := container.RunOpts{}
    for i := 0; i < len(args); i++ {
        switch args[i] {
        case "--name":
            i++; opts.Name = args[i]
        case "-d", "--detach":
            opts.Detach = true
        case "-m", "--memory":
            i++; opts.Config.Memory = parseMemory(args[i])
        case "--cpu-shares":
            i++; v, _ := strconv.ParseInt(args[i], 10, 64)
            opts.Config.CPUShares = v
        case "--pids-limit":
            i++; v, _ := strconv.ParseInt(args[i], 10, 64)
            opts.Config.PidsLimit = v
        case "-e", "--env":
            i++; opts.Config.Env = append(opts.Config.Env, args[i])
        case "-v", "--volume":
            i++
            parts := strings.SplitN(args[i], ":", 3)
            if len(parts) >= 2 {
                m := container.Mount{
                    Source: parts[0], Target: parts[1],
                }
                if len(parts) == 3 && parts[2] == "ro" {
                    m.ReadOnly = true
                }
                opts.Mounts = append(opts.Mounts, m)
            }
        default:
            if opts.Image == "" {
                opts.Image = args[i]
            } else {
                opts.Command = append(opts.Command, args[i])
            }
        }
    }
    if len(opts.Command) == 0 {
        opts.Command = []string{"/bin/sh"}
    }
    return opts
}

Ручний парсинг без бібліотек. Перший аргумент без прапорця - це образ, решта - команда. Як у Docker.

Команда run

func cmdRun(mgr *container.Manager) {
    opts := parseRunFlags(os.Args[2:])

    c, err := mgr.Create(opts)
    if err != nil { fatal("create: %v", err) }

    if err := mgr.Start(c.ID); err != nil {
        fatal("start: %v", err)
    }

    if opts.Detach {
        fmt.Println(c.ID)
    } else {
        fmt.Printf("container %s started (pid %d)\n",
            container.ShortID(c.ID), c.Pid)
    }
}

run = create + start. З прапорцем -d виводить тільки ID (як Docker).

Команда ps

func cmdPs(mgr *container.Manager) {
    all := false
    for _, arg := range os.Args[2:] {
        if arg == "-a" || arg == "--all" { all = true }
    }

    containers := mgr.List(all)
    tbl := cli.NewTable("CONTAINER ID", "IMAGE",
        "COMMAND", "CREATED", "STATUS", "NAME")

    for _, c := range containers {
        tbl.AddRow(
            container.ShortID(c.ID),
            c.Image,
            cli.Truncate(strings.Join(c.Command, " "), 30),
            timeAgo(c.CreatedAt),
            formatStatus(c),
            c.Name,
        )
    }
    tbl.Render(os.Stdout)
}

Вивід форматується таблицею з вирівнюванням колонок.

Форматування статусу

func formatStatus(c *container.Container) string {
    switch c.State {
    case container.StateRunning:
        return fmt.Sprintf("Up %s", timeAgo(c.StartedAt))
    case container.StateStopped:
        return fmt.Sprintf("Exited (%d) %s",
            c.ExitCode, timeAgo(c.StoppedAt))
    default:
        return string(c.State)
    }
}

func timeAgo(t time.Time) string {
    d := time.Since(t)
    switch {
    case d < time.Minute:
        return fmt.Sprintf("%ds ago", int(d.Seconds()))
    case d < time.Hour:
        return fmt.Sprintf("%dm ago", int(d.Minutes()))
    case d < 24*time.Hour:
        return fmt.Sprintf("%dh ago", int(d.Hours()))
    default:
        return fmt.Sprintf("%dd ago", int(d.Hours()/24))
    }
}

Повний набір команд

graph LR
    subgraph "Container"
        RUN["run"] --> CREATE["create"] & START["start"]
        STOP["stop"]
        RM["rm"]
        PS["ps"]
        INSPECT["inspect"]
        LOGS["logs"]
    end

    subgraph "Images"
        PULL["pull"]
        PUSH["push"]
        IMAGES["images"]
        IMPORT["import"]
        BOOTSTRAP["bootstrap"]
        TAG["tag"]
        RMI["rmi"]
    end

Всі команди - прямолінійні обгортки над Manager і ImageManager. Жодна не перевищує 30 рядків.

Чому без cobra?

Cobra дає автодоповнення, вкладені команди, генерацію документації. Для великого проекту це цінно. Але для 15 простих команд switch/case з ручним парсингом прапорців: - Нуль залежностей - Швидша компіляція - Простий для читання і розуміння - Легко додати нову команду

Мінус - немає автодоповнення і help по прапорцях генерується вручну.

Що варто знати

Ручний парсинг не валідує невідомі прапорці. sheep run --typo value nginx тихо проігнорує --typo. Cobra показала б помилку. Для production CLI це критично.

Спробуй сам

# Повна довідка:
./sheep help
# Запуск з усіма опціями:
sudo ./sheep run --name web -m 256m -d \
  -e PORT=8080 -v /data:/app:ro nginx /bin/sh
sudo ./sheep ps
sudo ./sheep inspect web
sudo ./sheep logs web

Це завершує серію Container Runtime. Починаємо будувати оркестратор.

Серія: Container Runtime (Sheep = Docker)

  1. Linux Namespaces
  2. Re-Exec Pattern
  3. pivot_root
  4. Cgroups v2
  5. OverlayFS
  6. Bridge Networking
  7. NAT і iptables
  8. Image Management
  9. Container Lifecycle
  10. Docker CLI (ця стаття)

Ресурси

  • Docker CLI reference — офіційний довідник команд
  • Docker CLI source code — вихідний код CLI Docker на Go
  • spf13/cobra — популярний CLI-фреймворк на Go (Docker, kubectl)
  • urfave/cli — ще один популярний CLI-фреймворк на Go
  • flag package — вбудований парсер прапорців у Go
  • text/tabwriter — вбудоване форматування таблиць з вирівнюванням колонок
  • os.Args — прямий доступ до аргументів процесу в Go
  • 12 Factor CLI Apps — принципи побудови якісних CLI

Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow

Попередня: Container Lifecycle