Эволюция Docker. Часть 2.1

от автора

Вступление

v0.1.0
v0.1.0

Данная статья является второй, в цикле по истории развития и изучению исходного кода Docker. В ней мы разберем, что представлял собой первый публичный релиз от 23 марта 2013 года. 

Изначально я планировал уложить весь материал, посвященный этой версии, в одной статье, но в процессе стало ясно, что она получается слишком большой, поэтому я решил разделить ее на две. В текущей части (2.1) будет рассмотрена лишь общая структура и начальный код, а последующая часть (2.2) будет посвящена принципу работы и коду конкретных команд.

Некоторые части кода уже были разобраны в первой статье, так что для полноты восприятия, рекомендую начать с нее, а также пятиминутной презентации The Future of Linux Containers, на которой и была представлена первая версия Docker.

Docker v0.1.0

Все действия будут выполняться, как и ранее, на Windows 10 и Vagrant с Ubuntu 20.04. Начнем с установки требуемых пакетов:

sudo apt-get update && sudo apt-get -y install lxc libarchive-tools curl golang git debootstrap tree

Клонируем репозиторий и перейдем на версию v0.1.0:

cd /home/vagrant && git clone https://github.com/docker/engine.git && cd ./engine  git checkout -f v0.1.0 && git log | head -n 5 && tree HEAD is now at 57e2126a02 Bumped version to 0.1.0 commit 57e2126a02f8b96b0542df7f6a573233d8419bb1 Author: Solomon Hykes <solomon@dotcloud.com> Date:   Sat Mar 23 17:48:18 2013 -0700      Bumped version to 0.1.0  . ├── AUTHORS ├── LICENSE ├── NOTICE ├── README.md ├── Vagrantfile ├── archive.go ├── archive_test.go ├── auth │   ├── auth.go │   └── auth_test.go ├── changes.go ├── commands.go ├── container.go ├── container_test.go ├── contrib │   ├── README │   ├── install.sh │   └── mkimage-busybox.sh ├── deb │   ├── Makefile -> ../Makefile │   ├── Makefile.deb │   ├── README.md -> ../README.md │   ├── debian │   │   ├── changelog │   │   ├── compat │   │   ├── control │   │   ├── copyright │   │   ├── docs │   │   ├── rules │   │   └── source │   │       └── format │   └── etc │       ├── docker-dev.upstart │       └── docker.upstart ├── docker │   └── docker.go ├── docs │   ├── README.md │   └── images-repositories-push-pull.md ├── graph.go ├── graph_test.go ├── image.go ├── lxc_template.go ├── mount.go ├── mount_darwin.go ├── mount_linux.go ├── network.go ├── network_test.go ├── puppet │   ├── manifests │   │   └── quantal64.pp │   └── modules │       └── docker │           ├── manifests │           │   └── init.pp │           └── templates │               ├── dockerd.conf │               └── profile ├── rcli │   ├── http.go │   ├── tcp.go │   └── types.go ├── registry.go ├── runtime.go ├── runtime_test.go ├── state.go ├── sysinit.go ├── tags.go ├── term │   ├── term.go │   ├── termios_darwin.go │   └── termios_linux.go ├── utils.go └── utils_test.go  16 directories, 58 files 

Как можно заметить, со времени первого коммита файлов заметно прибавилось. Директории deb и puppet мы опустим, так как они нас мало интересуют.

Для начала попробуем скомпилировать и запустить данную версию программы: 

go mod init github.com/dotcloud/docker && cd docker && go build docker.go go: creating new go.mod: module github.com/dotcloud/docker go: finding github.com/kr/pty v1.1.8 go: downloading github.com/kr/pty v1.1.8 go: extracting github.com/kr/pty v1.1.8 go: downloading github.com/creack/pty v1.1.7 go: extracting github.com/creack/pty v1.1.7 go: finding github.com/creack/pty v1.1.7
vagrant@ubuntu-focal:/vagrant/engine/docker$ sudo ./docker version docker version Version:0.1.0   vagrant@ubuntu-focal:~/engine/docker$ sudo ./docker help Usage: docker COMMAND [arg...]  A self-sufficient runtime for linux containers.  Commands:     run       Run a command in a container     ps        Display a list of containers     import    Create a new filesystem image from the contents of a tarball     attach    Attach to a running container     commit    Create a new image from a container's changes     history   Show the history of an image     diff      Inspect changes on a container's filesystem     images    List images     info      Display system-wide information     inspect   Return low-level information on a container     kill      Kill a running container     login     Register or Login to the docker registry server     logs      Fetch the logs of a container     port      Lookup the public-facing port which is NAT-ed to PRIVATE_PORT     ps        List containers     pull      Pull an image or a repository to the docker registry server     push      Push an image or a repository to the docker registry server     restart   Restart a running container     rm        Remove a container     rmi       Remove an image     run       Run a command in a new container     start     Start a stopped container     stop      Stop a running container     export    Stream the contents of a container as a tar archive     version   Show the docker version information     wait      Block until a container stops, then print its exit code

Уже в первой версии имеется знакомый нам функционал для работы с контейнерами, образами, историей, репозиторием, сетевыми портами и тп. 

К сожалению, воспользоваться репозиторием для скачивания образа не получится, так как с тех пор формат и сам репозиторий поменялись, а для запуска контейнера нужно будет применить патч для lxc шаблона. Но во второй части, мы вручную создадим образ при помощи утилиты debootstrap, применим патч, после чего импортируем и запустим контейнер. А пока приступим к изучению кода.

Entry point

Главной точкой входа, является функция main в файле docker/docker.go:

func main() { if docker.SelfPath() == "/sbin/init" { // Running in init mode docker.SysInit() return } // FIXME: Switch d and D ? (to be more sshd like) fl_daemon := flag.Bool("d", false, "Daemon mode") fl_debug := flag.Bool("D", false, "Debug mode") flag.Parse() rcli.DEBUG_FLAG = *fl_debug if *fl_daemon { if flag.NArg() != 0 { flag.Usage() return } if err := daemon(); err != nil { log.Fatal(err) } } else { if err := runCommand(flag.Args()); err != nil { log.Fatal(err) } } }

В самом начале функции находится, как может показаться, довольно странная проверка. Запускаемый файл проверяется на соответствие с /sbin/init. В следующей части, когда мы перейдем к функции запуска контейнера, будет видно, что исполняемый файл docker, монтируется в точку /sbin/init, запускаемого lxc контейнера, а функция docker.SysInit() из файла sysinit.go производит настройку окружения и последующий запуск требуемого процесса в контейнере. На данном этапе мы пропустим эту часть.

// Sys Init code // This code is run INSIDE the container and is responsible for setting // up the environment before running the actual process func SysInit() { if len(os.Args) <= 1 { fmt.Println("You should not invoke docker-init manually") os.Exit(1) } var u = flag.String("u", "", "username or uid") var gw = flag.String("g", "", "gateway address")  flag.Parse()  setupNetworking(*gw) changeUser(*u) executeProgram(flag.Arg(0), flag.Args()) }

Далее в зависимости от флага -d, докер может запускаться в режиме демона работая по сетевому сокету или выполнять команды локально.

func daemon() error { service, err := docker.NewServer() if err != nil { return err } return rcli.ListenAndServe("tcp", "127.0.0.1:4242", service) }

Функция daemon стартует простой tcp сервер на порту 4242, принимает соединения, читает и выполняет команды переданные в json формате. Его код можно найти в файле rcli/tcp.go:

// Listen on `addr`, using protocol `proto`, for incoming rcli calls, // and pass them to `service`. func ListenAndServe(proto, addr string, service Service) error { listener, err := net.Listen(proto, addr) if err != nil { return err } log.Printf("Listening for RCLI/%s on %s\n", proto, addr) defer listener.Close() for { if conn, err := listener.Accept(); err != nil { return err } else { go func() { if DEBUG_FLAG { CLIENT_SOCKET = conn } if err := Serve(conn, service); err != nil { log.Printf("Error: " + err.Error() + "\n") fmt.Fprintf(conn, "Error: "+err.Error()+"\n") } conn.Close() }() } } return nil }  // Parse an rcli call on a new connection, and pass it to `service` if it // is valid. func Serve(conn io.ReadWriter, service Service) error { r := bufio.NewReader(conn) var args []string if line, err := r.ReadString('\n'); err != nil { return err } else if err := json.Unmarshal([]byte(line), &args); err != nil { return err } else { return call(service, ioutil.NopCloser(r), conn, args...) } return nil }  // FIXME: For reverse compatibility func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { return LocalCall(service, stdin, stdout, args...) }

В случае же запуска докера без флага -d выполнение переходит к функции runCommand:

func runCommand(args []string) error { var oldState *term.State var err error if term.IsTerminal(0) && os.Getenv("NORAW") == "" { oldState, err = term.MakeRaw(0) if err != nil { return err } defer term.Restore(0, oldState) } // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose // CloseWrite(), which we need to cleanly signal that stdin is closed without // closing the connection. // See http://code.google.com/p/go/issues/detail?id=3345 if conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...); err == nil { receive_stdout := docker.Go(func() error { _, err := io.Copy(os.Stdout, conn) return err }) send_stdin := docker.Go(func() error { _, err := io.Copy(conn, os.Stdin) if err := conn.CloseWrite(); err != nil { log.Printf("Couldn't send EOF: " + err.Error()) } return err }) if err := <-receive_stdout; err != nil { return err } if !term.IsTerminal(0) { if err := <-send_stdin; err != nil { return err } } } else { service, err := docker.NewServer() if err != nil { return err } if err := rcli.LocalCall(service, os.Stdin, os.Stdout, args...); err != nil { return err } } if oldState != nil { term.Restore(0, oldState) } return nil }

В самом начале происходит перевод терминала в raw режим. Весь функционал, отвечающий за работу с терминалом, находится в папке term.

Далее идет попытка подключения и отправка команды демону функцией rcli.Call из файла rcli/tcp.go (демон должен быть запущен заранее с флагом -d):

// Connect to a remote endpoint using protocol `proto` and address `addr`, // issue a single call, and return the result. // `proto` may be "tcp", "unix", etc. See the `net` package for available protocols. func Call(proto, addr string, args ...string) (*net.TCPConn, error) { cmd, err := json.Marshal(args) if err != nil { return nil, err } conn, err := net.Dial(proto, addr) if err != nil { return nil, err } if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { return nil, err } return conn.(*net.TCPConn), nil }

В случае, если попытка не удалась, команда выполняется локально при помощи вызова LocalCall и структуры возвращаемой функцией docker.NewServer, которая находится в файле commands.go:

func NewServer() (*Server, error) { rand.Seed(time.Now().UTC().UnixNano()) if runtime.GOARCH != "amd64" { log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) } runtime, err := NewRuntime() if err != nil { return nil, err } srv := &Server{ runtime: runtime, } return srv, nil }  type Server struct { runtime *Runtime }

Мы вернемся к структуре Server и Runtime немного позже, а сперва посмотрим на функцию LocalCall из файла rcli/types.go:

func LocalCall(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { if len(args) == 0 { args = []string{"help"} } flags := flag.NewFlagSet("main", flag.ContinueOnError) flags.SetOutput(stdout) flags.Usage = func() { stdout.Write([]byte(service.Help())) } if err := flags.Parse(args); err != nil { return err } cmd := flags.Arg(0) log.Printf("%s\n", strings.Join(append(append([]string{service.Name()}, cmd), flags.Args()[1:]...), " ")) if cmd == "" { cmd = "help" } method := getMethod(service, cmd) if method != nil { return method(stdin, stdout, flags.Args()[1:]...) } return errors.New("No such command: " + cmd) }

В ней происходит разбор аргументов и выполнение команды, полученной из getMethod, который использует рефлексию структуры Server для вычисления метода соответствующего переданной команде по шаблону «Cmd» + MethodName:

func getMethod(service Service, name string) Cmd { if name == "help" { return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { if len(args) == 0 { stdout.Write([]byte(service.Help())) } else { if method := getMethod(service, args[0]); method == nil { return errors.New("No such command: " + args[0]) } else { method(stdin, stdout, "--help") } } return nil } } methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:]) method, exists := reflect.TypeOf(service).MethodByName(methodName) if !exists { return nil } return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { ret := method.Func.CallSlice([]reflect.Value{ reflect.ValueOf(service), reflect.ValueOf(stdin), reflect.ValueOf(stdout), reflect.ValueOf(args), })[0].Interface() if ret == nil { return nil } return ret.(error) } }

Все доступные методы лежат в файле commands.go, их разбором мы займемся во второй части, а пока вернемся к функции NewRuntime вызываемой в функции docker.NewServer. Она содержится в файле runtime.go и которая возвращает структуру Runtime сохраняемую в структуре Server:

func NewRuntime() (*Runtime, error) { return NewRuntimeFromDirectory("/var/lib/docker") }  func NewRuntimeFromDirectory(root string) (*Runtime, error) { runtime_repo := path.Join(root, "containers")  if err := os.MkdirAll(runtime_repo, 0700); err != nil && !os.IsExist(err) { return nil, err }  g, err := NewGraph(path.Join(root, "graph")) if err != nil { return nil, err } repositories, err := NewTagStore(path.Join(root, "repositories"), g) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } netManager, err := newNetworkManager(networkBridgeIface) if err != nil { return nil, err } authConfig, err := auth.LoadConfig(root) if err != nil && authConfig == nil { // If the auth file does not exist, keep going return nil, err }  runtime := &Runtime{ root:           root, repository:     runtime_repo, containers:     list.New(), networkManager: netManager, graph:          g, repositories:   repositories, authConfig:     authConfig, }  if err := runtime.restore(); err != nil { return nil, err } return runtime, nil }

Как можно заметить, часть этого функционала раньше находилась в файле docker.go,  который мы разобрали в первой статье. Теперь же сюда добавлены и новые структуры:

  • graph (NewGraph) — отвечает за работу со слоями и зависимостями образа. Файл graph.go

  • networkManager (newNetworkManager) — отвечает за весь сетевой стек. Файл network.go

  • repositories (NewTagStore) — отвечает за локальный репозиторий и работу с метками (tags) образов. Файл tags.go

  • authConfig (auth.LoadConfig) — отвечает за хранение данных для авторизации на удаленном репозитории образов. Файл auth/auth.go

Работу метода restore мы также разбирали в первой части. Принцип остался прежним, изменения коснулись лишь процедуры инициализации существующих контейнеров:

func (runtime *Runtime) restore() error { dir, err := ioutil.ReadDir(runtime.repository) if err != nil { return err } for _, v := range dir { id := v.Name() container, err := runtime.Load(id) if err != nil { Debugf("Failed to load container %v: %v", id, err) continue } Debugf("Loaded container %v", container.Id) } return nil }   func (runtime *Runtime) Load(id string) (*Container, error) { container := &Container{root: runtime.containerRoot(id)} if err := container.FromDisk(); err != nil { return nil, err } if container.Id != id { return container, fmt.Errorf("Container %s is stored at %s", container.Id, id) } if err := runtime.Register(container); err != nil { return nil, err } return container, nil }   func (container *Container) FromDisk() error { data, err := ioutil.ReadFile(container.jsonPath()) if err != nil { return err } // Load container settings if err := json.Unmarshal(data, container); err != nil { return err } return nil }

В первой части мне пришлось делать небольшой патч, теперь же метод Register осуществляет полную инициализацию контейнера:

// Register makes a container object usable by the runtime as <container.Id> func (runtime *Runtime) Register(container *Container) error { if container.runtime != nil || runtime.Exists(container.Id) { return fmt.Errorf("Container is already loaded") } if err := validateId(container.Id); err != nil { return err } container.runtime = runtime // Setup state lock (formerly in newState() lock := new(sync.Mutex) container.State.stateChangeLock = lock container.State.stateChangeCond = sync.NewCond(lock) // Attach to stdout and stderr container.stderr = newWriteBroadcaster() container.stdout = newWriteBroadcaster() // Attach to stdin if container.Config.OpenStdin { container.stdin, container.stdinPipe = io.Pipe() } else { container.stdinPipe = NopWriteCloser(ioutil.Discard) // Silently drop stdin } // Setup logging of stdout and stderr to disk if err := runtime.LogToDisk(container.stdout, container.logPath("stdout")); err != nil { return err } if err := runtime.LogToDisk(container.stderr, container.logPath("stderr")); err != nil { return err } // done runtime.containers.PushBack(container) return nil } 

Функционал writeBroadcaster, State знакомый нам из первой части, остался без изменения, добавилась лишь заглушка для stdin:

type nopWriteCloser struct { io.Writer }  func (w *nopWriteCloser) Close() error { return nil }  func NopWriteCloser(w io.Writer) io.WriteCloser { return &nopWriteCloser{w} }

и логирование потоков stdout stderr в файл на диск:

func (container *Container) logPath(name string) string { return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name)) }  func (runtime *Runtime) LogToDisk(src *writeBroadcaster, dst string) error { log, err := os.OpenFile(dst, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600) if err != nil { return err } src.AddWriter(NopWriteCloser(log)) return nil }

На этом, думаю, можно завершить общую частью и перейти непосредственно к разбору кода команд.

Заключение

В следующей части мы подробно рассмотрим код отвечающий за работу с контейнерами, образами, репозиторием, сетевым стеком, а также создадим образ вручную и запустим контейнер на его основе.


ссылка на оригинал статьи https://habr.com/ru/articles/574750/