Вступление
Данная статья является четвертой в цикле (1, 2, 3), посвященном изучению исходного кода Docker и прямым продолжением предыдущей статьи, которую мне пришлось преждевременно завершить в виду зависания редактора хабра. В этой статье мы закончим изучать код первого публичного релиза Docker v0.1.0. Будут рассмотрены оставшиеся команды по управлению контейнерами, сетевой стек, а также создание и запуск образа.
Управление контейнерами
Start
Запускает остановленный контейнер вызовом метода container.Start, код которого был представлен в конце предыдущей статьи:
CmdStart
func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "start", "[OPTIONS] NAME", "Start a stopped container") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { cmd.Usage() return nil } for _, name := range cmd.Args() { if container := srv.runtime.Get(name); container != nil { if err := container.Start(); err != nil { return err } fmt.Fprintln(stdout, container.Id) } else { return errors.New("No such container: " + name) } } return nil }
Stop
Останавливает контейнер вызовом метода container.Stop:
CmdStop
func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "stop", "[OPTIONS] NAME", "Stop a running container") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { cmd.Usage() return nil } for _, name := range cmd.Args() { if container := srv.runtime.Get(name); container != nil { if err := container.Stop(); err != nil { return err } fmt.Fprintln(stdout, container.Id) } else { return errors.New("No such container: " + name) } } return nil } func (container *Container) Stop() error { if !container.State.Running { return nil } // 1. Send a SIGTERM if output, err := exec.Command("/usr/bin/lxc-kill", "-n", container.Id, "15").CombinedOutput(); err != nil { log.Printf(string(output)) log.Printf("Failed to send SIGTERM to the process, force killing") if err := container.Kill(); err != nil { return err } } // 2. Wait for the process to exit on its own if err := container.WaitTimeout(10 * time.Second); err != nil { log.Printf("Container %v failed to exit within 10 seconds of SIGTERM - using the force", container.Id) if err := container.Kill(); err != nil { return err } } return nil }
В начале метода производится попытка остановки утилитой lxc-kill, если по прошествии 10 секунд контейнер не остановлен, тогда вызывается метод container.Kill, который убивает процесс:
container.Kill
func (container *Container) Kill() error { if !container.State.Running { return nil } return container.kill() } func (container *Container) kill() error { if err := container.cmd.Process.Kill(); err != nil { return err } // Wait for the container to be actually stopped container.Wait() return nil }
Restart
Последовательно вызывает методы container.Stop и container.Start:
CmdRestart
func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "restart", "[OPTIONS] NAME", "Restart a running container") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { cmd.Usage() return nil } for _, name := range cmd.Args() { if container := srv.runtime.Get(name); container != nil { if err := container.Restart(); err != nil { return err } fmt.Fprintln(stdout, container.Id) } else { return errors.New("No such container: " + name) } } return nil } func (container *Container) Restart() error { if err := container.Stop(); err != nil { return err } if err := container.Start(); err != nil { return err } return nil }
Wait
Ждет завершения работы контейнера, вызывая метод container.Wait:
CmdWait
// 'docker wait': block until a container stops func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "wait", "[OPTIONS] NAME", "Block until a container stops, then print its exit code.") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { cmd.Usage() return nil } for _, name := range cmd.Args() { if container := srv.runtime.Get(name); container != nil { fmt.Fprintln(stdout, container.Wait()) } else { return errors.New("No such container: " + name) } } return nil } // Wait blocks until the container stops running, then returns its exit code. func (container *Container) Wait() int { for container.State.Running { container.State.wait() } return container.State.ExitCode }
Kill
Убивает процесс контейнера методом container.Kill, который уже был рассмотрен выше:
CmdKill
// 'docker kill NAME' kills a running container func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "kill", "[OPTIONS] CONTAINER [CONTAINER...]", "Kill a running container") if err := cmd.Parse(args); err != nil { return nil } for _, name := range cmd.Args() { container := srv.runtime.Get(name) if container == nil { return errors.New("No such container: " + name) } if err := container.Kill(); err != nil { fmt.Fprintln(stdout, "Error killing container "+name+": "+err.Error()) } } return nil }
Rm
Вызывает метод runtime.Destroy, который останавливает контейнер, при необходимости размонтирует файловую систему и удаляет директорию контейнера:
CmdRm
func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") if err := cmd.Parse(args); err != nil { return nil } for _, name := range cmd.Args() { container := srv.runtime.Get(name) if container == nil { return errors.New("No such container: " + name) } if err := srv.runtime.Destroy(container); err != nil { fmt.Fprintln(stdout, "Error destroying container "+name+": "+err.Error()) } } return nil } func (runtime *Runtime) Destroy(container *Container) error { element := runtime.getContainerElement(container.Id) if element == nil { return fmt.Errorf("Container %v not found - maybe it was already destroyed?", container.Id) } if err := container.Stop(); err != nil { return err } if mounted, err := container.Mounted(); err != nil { return err } else if mounted { if err := container.Unmount(); err != nil { return fmt.Errorf("Unable to unmount container %v: %v", container.Id, err) } } // Deregister the container before removing its directory, to avoid race conditions runtime.containers.Remove(element) if err := os.RemoveAll(container.root); err != nil { return fmt.Errorf("Unable to remove filesystem for %v: %v", container.Id, err) } return nil }
Ps
Получает весь список существующих контейнеров и отображает их в таблице в зависимости от переданных параметров:
CmdPs
func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "ps", "[OPTIONS]", "List containers") quiet := cmd.Bool("q", false, "Only display numeric IDs") fl_all := cmd.Bool("a", false, "Show all containers. Only running containers are shown by default.") fl_full := cmd.Bool("notrunc", false, "Don't truncate output") if err := cmd.Parse(args); err != nil { return nil } w := tabwriter.NewWriter(stdout, 12, 1, 3, ' ', 0) if !*quiet { fmt.Fprintf(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tCOMMENT\n") } for _, container := range srv.runtime.List() { if !container.State.Running && !*fl_all { continue } if !*quiet { command := fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) if !*fl_full { command = Trunc(command, 20) } for idx, field := range []string{ /* ID */ container.Id, /* IMAGE */ srv.runtime.repositories.ImageName(container.Image), /* COMMAND */ command, /* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago", /* STATUS */ container.State.String(), /* COMMENT */ "", } { if idx == 0 { w.Write([]byte(field)) } else { w.Write([]byte("\t" + field)) } } w.Write([]byte{'\n'}) } else { stdout.Write([]byte(container.Id + "\n")) } } if !*quiet { w.Flush() } return nil }
Diff
Показывает diff между образом контейнера и его текущей файловой системой, полученный из метода container.Changes:
CmdDiff
func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { return errors.New("Not enough arguments") } if container := srv.runtime.Get(cmd.Arg(0)); container == nil { return errors.New("No such container") } else { changes, err := container.Changes() if err != nil { return err } for _, change := range changes { fmt.Fprintln(stdout, change.String()) } } return nil } func (container *Container) Changes() ([]Change, error) { image, err := container.GetImage() if err != nil { return nil, err } return image.Changes(container.rwPath()) } func (image *Image) Changes(rw string) ([]Change, error) { layers, err := image.layers() if err != nil { return nil, err } return Changes(layers, rw) }
Changes является главной функцией, в которой и происходит вся работа по вычислению изменений, ее код находится в файле changes.go:
changes.go
type ChangeType int const ( ChangeModify = iota ChangeAdd ChangeDelete ) type Change struct { Path string Kind ChangeType } func (change *Change) String() string { var kind string switch change.Kind { case ChangeModify: kind = "C" case ChangeAdd: kind = "A" case ChangeDelete: kind = "D" } return fmt.Sprintf("%s %s", kind, change.Path) } func Changes(layers []string, rw string) ([]Change, error) { var changes []Change err := filepath.Walk(rw, func(path string, f os.FileInfo, err error) error { if err != nil { return err } // Rebase path path, err = filepath.Rel(rw, path) if err != nil { return err } path = filepath.Join("/", path) // Skip root if path == "/" { return nil } // Skip AUFS metadata if matched, err := filepath.Match("/.wh..wh.*", path); err != nil || matched { return err } change := Change{ Path: path, } // Find out what kind of modification happened file := filepath.Base(path) // If there is a whiteout, then the file was removed if strings.HasPrefix(file, ".wh.") { originalFile := strings.TrimLeft(file, ".wh.") change.Path = filepath.Join(filepath.Dir(path), originalFile) change.Kind = ChangeDelete } else { // Otherwise, the file was added change.Kind = ChangeAdd // ...Unless it already existed in a top layer, in which case, it's a modification for _, layer := range layers { stat, err := os.Stat(filepath.Join(layer, path)) if err != nil && !os.IsNotExist(err) { return err } if err == nil { // The file existed in the top layer, so that's a modification // However, if it's a directory, maybe it wasn't actually modified. // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar if stat.IsDir() && f.IsDir() { if f.Size() == stat.Size() && f.Mode() == stat.Mode() && f.ModTime() == stat.ModTime() { // Both directories are the same, don't record the change return nil } } change.Kind = ChangeModify break } } } // Record change changes = append(changes, change) return nil }) if err != nil { return nil, err } return changes, nil }
Функция обходит все директории в rw, если элемент имеет префикс .wh., то это означает, что он был удален (так Aufs регистрирует удаление). В ином случае происходит последовательный обход слоев образа в попытке найти элемент в них. Если он не был найден ни в одном из слоев, значит элемент был создан. В случае обнаружения элемента в слоях, функция считает, что это модификация. Каждое из действий отображается соответствующей буквой (D A C).
Общие команды
Info
Отображает версию, а также количество контейнеров и образов:
CmdInfo
// 'docker info': display system-wide information. func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string) error { images, _ := srv.runtime.graph.All() var imgcount int if images == nil { imgcount = 0 } else { imgcount = len(images) } cmd := rcli.Subcmd(stdout, "info", "", "Display system-wide information.") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() > 0 { cmd.Usage() return nil } fmt.Fprintf(stdout, "containers: %d\nversion: %s\nimages: %d\n", len(srv.runtime.List()), VERSION, imgcount) return nil }
Inspect
Пытается получить структуру контейнера или образа по переданному имени, после чего экспортирует и возвращает ее в json формате, предварительно добавив отступы для читаемости:
CmdInspect
func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "inspect", "[OPTIONS] CONTAINER", "Return low-level information on a container") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() < 1 { cmd.Usage() return nil } name := cmd.Arg(0) var obj interface{} if container := srv.runtime.Get(name); container != nil { obj = container } else if image, err := srv.runtime.repositories.LookupImage(name); err == nil && image != nil { obj = image } else { // No output means the object does not exist // (easier to script since stdout and stderr are not differentiated atm) return nil } data, err := json.Marshal(obj) if err != nil { return err } indented := new(bytes.Buffer) if err = json.Indent(indented, data, "", " "); err != nil { return err } if _, err := io.Copy(stdout, indented); err != nil { return err } stdout.Write([]byte{'\n'}) return nil }
Logs
Читает и копирует в stdout, stderr содержимое лог файлов контейнера:
CmdLogs
func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() != 1 { cmd.Usage() return nil } name := cmd.Arg(0) if container := srv.runtime.Get(name); container != nil { log_stdout, err := container.ReadLog("stdout") if err != nil { return err } log_stderr, err := container.ReadLog("stderr") if err != nil { return err } // FIXME: Interpolate stdout and stderr instead of concatenating them // FIXME: Differentiate stdout and stderr in the remote protocol if _, err := io.Copy(stdout, log_stdout); err != nil { return err } if _, err := io.Copy(stdout, log_stderr); err != nil { return err } return nil } return errors.New("No such container: " + cmd.Arg(0)) }
Attach
Подключается к контейнеру и перенаправляет потоки stdin, stdout, stderr:
CmdAttach
func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "attach", "[OPTIONS]", "Attach to a running container") fl_i := cmd.Bool("i", false, "Attach to stdin") fl_o := cmd.Bool("o", true, "Attach to stdout") fl_e := cmd.Bool("e", true, "Attach to stderr") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() != 1 { cmd.Usage() return nil } name := cmd.Arg(0) container := srv.runtime.Get(name) if container == nil { return errors.New("No such container: " + name) } var wg sync.WaitGroup if *fl_i { c_stdin, err := container.StdinPipe() if err != nil { return err } wg.Add(1) go func() { io.Copy(c_stdin, stdin); wg.Add(-1) }() } if *fl_o { c_stdout, err := container.StdoutPipe() if err != nil { return err } wg.Add(1) go func() { io.Copy(stdout, c_stdout); wg.Add(-1) }() } if *fl_e { c_stderr, err := container.StderrPipe() if err != nil { return err } wg.Add(1) go func() { io.Copy(stdout, c_stderr); wg.Add(-1) }() } wg.Wait() return nil }
Port
Отображает проброшенный порт контейнера из хеш таблицы PortMapping, заполняемой при запуске. Это будет подробно разобрано ниже, в разделе по сетевому стеку:
CmdPort
func (srv *Server) CmdPort(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "port", "[OPTIONS] CONTAINER PRIVATE_PORT", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT") if err := cmd.Parse(args); err != nil { return nil } if cmd.NArg() != 2 { cmd.Usage() return nil } name := cmd.Arg(0) privatePort := cmd.Arg(1) if container := srv.runtime.Get(name); container == nil { return errors.New("No such container: " + name) } else { if frontend, exists := container.NetworkSettings.PortMapping[privatePort]; !exists { return fmt.Errorf("No private port '%s' allocated on %s", privatePort, name) } else { fmt.Fprintln(stdout, frontend) } } return nil }
Сетевой стек
Основной код находится в файле network.go. Мы начнем разбор с функции newNetworkManager, которая инициализирует и возвращает структуру NetworkManager:
network.go
// Network Manager manages a set of network interfaces // Only *one* manager per host machine should be used type NetworkManager struct { bridgeIface string bridgeNetwork *net.IPNet ipAllocator *IPAllocator portAllocator *PortAllocator portMapper *PortMapper } func newNetworkManager(bridgeIface string) (*NetworkManager, error) { addr, err := getIfaceAddr(bridgeIface) if err != nil { return nil, err } network := addr.(*net.IPNet) ipAllocator, err := newIPAllocator(network) if err != nil { return nil, err } portAllocator, err := newPortAllocator(portRangeStart, portRangeEnd) if err != nil { return nil, err } portMapper, err := newPortMapper() manager := &NetworkManager{ bridgeIface: bridgeIface, bridgeNetwork: network, ipAllocator: ipAllocator, portAllocator: portAllocator, portMapper: portMapper, } return manager, nil } // Return the IPv4 address of a network interface func getIfaceAddr(name string) (net.Addr, error) { iface, err := net.InterfaceByName(name) if err != nil { return nil, err } addrs, err := iface.Addrs() if err != nil { return nil, err } var addrs4 []net.Addr for _, addr := range addrs { ip := (addr.(*net.IPNet)).IP if ip4 := ip.To4(); len(ip4) == net.IPv4len { addrs4 = append(addrs4, addr) } } switch { case len(addrs4) == 0: return nil, fmt.Errorf("Interface %v has no IP addresses", name) case len(addrs4) > 1: fmt.Printf("Interface %v has more than 1 IPv4 address. Defaulting to using %v\n", name, (addrs4[0].(*net.IPNet)).IP) } return addrs4[0], nil }
newNetworkManager при помощи функции getIfaceAddr получает структуру net.IPNet с адресом, установленным на интерфейсе networkBridgeIface (lxcbr0). Данный сетевой мост создается при запуске lxc. Если на интерфейсе имеется несколько ip адресов, то выбирается первый из них. Далее функция newIPAllocator инициализирует и возвращает структуру IPAllocator, которая в свою очередь, аллоцирует адреса для контейнеров в подсети назначенной lxcbr0.
IPAllocator
// IP allocator: Atomatically allocate and release networking ports type IPAllocator struct { network *net.IPNet queue chan (net.IP) } func newIPAllocator(network *net.IPNet) (*IPAllocator, error) { alloc := &IPAllocator{ network: network, } if err := alloc.populate(); err != nil { return nil, err } return alloc, nil }
Метод populate вычисляет диапазон подсети на основании ip и маски, после чего создает канал очереди. Для аллокации новых адресов используется простой инкремент. Хелпер методы для вычисления networkRange, networkSize, intToIp, ipToInt определенны в том же файле:
alloc.populate
func (alloc *IPAllocator) populate() error { firstIP, _ := networkRange(alloc.network) size, err := networkSize(alloc.network.Mask) if err != nil { return err } // The queue size should be the network size - 3 // -1 for the network address, -1 for the broadcast address and // -1 for the gateway address alloc.queue = make(chan net.IP, size-3) for i := int32(1); i < size-1; i++ { ipNum, err := ipToInt(firstIP) if err != nil { return err } ip, err := intToIp(ipNum + int32(i)) if err != nil { return err } // Discard the network IP (that's the host IP address) if ip.Equal(alloc.network.IP) { continue } alloc.queue <- ip } return nil } // Calculates the first and last IP addresses in an IPNet func networkRange(network *net.IPNet) (net.IP, net.IP) { netIP := network.IP.To4() firstIP := netIP.Mask(network.Mask) lastIP := net.IPv4(0, 0, 0, 0).To4() for i := 0; i < len(lastIP); i++ { lastIP[i] = netIP[i] | ^network.Mask[i] } return firstIP, lastIP } // Converts a 4 bytes IP into a 32 bit integer func ipToInt(ip net.IP) (int32, error) { buf := bytes.NewBuffer(ip.To4()) var n int32 if err := binary.Read(buf, binary.BigEndian, &n); err != nil { return 0, err } return n, nil } // Converts 32 bit integer into a 4 bytes IP address func intToIp(n int32) (net.IP, error) { var buf bytes.Buffer if err := binary.Write(&buf, binary.BigEndian, &n); err != nil { return net.IP{}, err } ip := net.IPv4(0, 0, 0, 0).To4() for i := 0; i < net.IPv4len; i++ { ip[i] = buf.Bytes()[i] } return ip, nil } // Given a netmask, calculates the number of available hosts func networkSize(mask net.IPMask) (int32, error) { m := net.IPv4Mask(0, 0, 0, 0) for i := 0; i < net.IPv4len; i++ { m[i] = ^mask[i] } buf := bytes.NewBuffer(m) var n int32 if err := binary.Read(buf, binary.BigEndian, &n); err != nil { return 0, err } return n + 1, nil }
Аллокация портов основана на аналогичном принципе с каналом очереди:
PortAllocator
// Port allocator: Atomatically allocate and release networking ports type PortAllocator struct { ports chan (int) } func (alloc *PortAllocator) populate(start, end int) { alloc.ports = make(chan int, end-start) for port := start; port < end; port++ { alloc.ports <- port } } func (alloc *PortAllocator) Acquire() (int, error) { select { case port := <-alloc.ports: return port, nil default: return -1, errors.New("No more ports available") } return -1, nil } func (alloc *PortAllocator) Release(port int) error { select { case alloc.ports <- port: return nil default: return errors.New("Too many ports have been released") } return nil } func newPortAllocator(start, end int) (*PortAllocator, error) { allocator := &PortAllocator{} allocator.populate(start, end) return allocator, nil }
Для маппинга портов используется стандартная утилита iptables:
iptables
// Wrapper around the iptables command func iptables(args ...string) error { if err := exec.Command("/sbin/iptables", args...).Run(); err != nil { return fmt.Errorf("iptables failed: iptables %v", strings.Join(args, " ")) } return nil } func newPortMapper() (*PortMapper, error) { mapper := &PortMapper{} if err := mapper.cleanup(); err != nil { return nil, err } if err := mapper.setup(); err != nil { return nil, err } return mapper, nil } // Port mapper takes care of mapping external ports to containers by setting // up iptables rules. // It keeps track of all mappings and is able to unmap at will type PortMapper struct { mapping map[int]net.TCPAddr }
Функция newPortMapper инициализирует структуру PortMapper, в хеш таблице которой будут сохраняться все проброшенные порты. В методах setup и cleanup соответственно создаются и удаляются NAT цепочки:
PortMapper
func (mapper *PortMapper) cleanup() error { // Ignore errors - This could mean the chains were never set up iptables("-t", "nat", "-D", "PREROUTING", "-j", "DOCKER") iptables("-t", "nat", "-D", "OUTPUT", "-j", "DOCKER") iptables("-t", "nat", "-F", "DOCKER") iptables("-t", "nat", "-X", "DOCKER") mapper.mapping = make(map[int]net.TCPAddr) return nil } func (mapper *PortMapper) setup() error { if err := iptables("-t", "nat", "-N", "DOCKER"); err != nil { return errors.New("Unable to setup port networking: Failed to create DOCKER chain") } if err := iptables("-t", "nat", "-A", "PREROUTING", "-j", "DOCKER"); err != nil { return errors.New("Unable to setup port networking: Failed to inject docker in PREROUTING chain") } if err := iptables("-t", "nat", "-A", "OUTPUT", "-j", "DOCKER"); err != nil { return errors.New("Unable to setup port networking: Failed to inject docker in OUTPUT chain") } return nil }
Весь маппинг портов основан на добавлении правил в созданные цепочки при помощи вызова iptables:
iptablesForward
func (mapper *PortMapper) Map(port int, dest net.TCPAddr) error { if err := mapper.iptablesForward("-A", port, dest); err != nil { return err } mapper.mapping[port] = dest return nil } func (mapper *PortMapper) Unmap(port int) error { dest, ok := mapper.mapping[port] if !ok { return errors.New("Port is not mapped") } if err := mapper.iptablesForward("-D", port, dest); err != nil { return err } delete(mapper.mapping, port) return nil } func (mapper *PortMapper) iptablesForward(rule string, port int, dest net.TCPAddr) error { return iptables("-t", "nat", rule, "DOCKER", "-p", "tcp", "--dport", strconv.Itoa(port), "-j", "DNAT", "--to-destination", net.JoinHostPort(dest.IP.String(), strconv.Itoa(dest.Port))) }
Создание образа и запуск контейнера
Для создания образа будем использовать утилиту debootstrap, которая скачает и подготовит файловую систему дистрибутива Ubuntu.
cd /tmp && sudo debootstrap trusty trusty && sudo chroot /tmp/trusty/ apt-get install iptables -y
Далее нужно будет удалить симлинк на /etc/resolv.conf и создать пустой файл для точки монтирования, так как без этого lxc выдаст ошибку.
sudo rm /tmp/trusty/etc/resolv.conf && sudo touch /tmp/trusty/etc/resolv.conf
Вместо заглушки systemd-resolved, будем использовать гугловый dns:
sudo rm /etc/resolv.conf && echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
Теперь вернемся в папку с докером и импортируем образ.
cd /home/vagrant/engine/docker && sudo tar -C /tmp/trusty -c . | sudo ./docker import - ubuntu docker import - ubuntu 10834b53c26eb579
Проверим, что образ импортировался:
vagrant@ubuntu-focal:~/engine/docker$ sudo ./docker images docker images REPOSITORY TAG ID CREATED PARENT ubuntu latest 10834b53c26eb579 12 seconds ago
В виду изменений в конфигурационном файле новых версий LXC нам нужно применить патч:
docker.patch
diff --git a/container.go b/container.go index f900599d00..941cff0455 100644 --- a/container.go +++ b/container.go @@ -217,6 +217,7 @@ func (container *Container) Start() error { params := []string{ "-n", container.Id, "-f", container.lxcConfigPath(), +"-F", "--", "/sbin/init", } diff --git a/lxc_template.go b/lxc_template.go index e3beb037f9..715522738c 100755 --- a/lxc_template.go +++ b/lxc_template.go @@ -7,33 +7,23 @@ import ( const LxcTemplate = ` # hostname {{if .Config.Hostname}} -lxc.utsname = {{.Config.Hostname}} +lxc.uts.name = {{.Config.Hostname}} {{else}} -lxc.utsname = {{.Id}} +lxc.uts.name = {{.Id}} {{end}} #lxc.aa_profile = unconfined # network configuration -lxc.network.type = veth -lxc.network.flags = up -lxc.network.link = lxcbr0 -lxc.network.name = eth0 -lxc.network.mtu = 1500 -lxc.network.ipv4 = {{.NetworkSettings.IpAddress}}/{{.NetworkSettings.IpPrefixLen}} +lxc.net.0.type = veth +lxc.net.0.flags = up +lxc.net.0.link = lxcbr0 +lxc.net.0.name = eth0 +lxc.net.0.mtu = 1500 +lxc.net.0.ipv4.address = {{.NetworkSettings.IpAddress}}/{{.NetworkSettings.IpPrefixLen}} # root filesystem {{$ROOTFS := .RootfsPath}} -lxc.rootfs = {{$ROOTFS}} - -# use a dedicated pts for the container (and limit the number of pseudo terminal -# available) -lxc.pts = 1024 - -# disable the main console -lxc.console = none - -# no controlling tty at all -lxc.tty = 1 +lxc.rootfs.path = {{$ROOTFS}} # no implicit access to devices lxc.cgroup.devices.deny = a
Скомпилируем и запустим контейнер:
vagrant@ubuntu-focal:~/engine/docker$ sudo ./docker run ubuntu /bin/bash root@9aac67f055e3731a:/# cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04 LTS" root@9aac67f055e3731a:/# cat /etc/resolv.conf nameserver 8.8.8.8 root@9aac67f055e3731a:/# ping google.com PING google.com (142.250.180.238) 56(84) bytes of data. 64 bytes from bud02s34-in-f14.1e100.net (142.250.180.238): icmp_seq=1 ttl=117 time=25.3 ms 64 bytes from bud02s34-in-f14.1e100.net (142.250.180.238): icmp_seq=2 ttl=117 time=27.5 ms 64 bytes from bud02s34-in-f14.1e100.net (142.250.180.238): icmp_seq=3 ttl=117 time=27.8 ms root@9aac67f055e3731a:/# exit exit vagrant@ubuntu-focal:~/engine/docker$ sudo ./docker diff 9aac67f055e3731a docker diff 9aac67f055e3731a C /root A /root/.bash_history
Заключение
Это был практически полный обзор кода первой версии Docker. С тех пор прошло уже много лет и сейчас код Docker состоит из сотен файлов, в котором будет сложно разобраться без подготовки. Но, как мы увидели, начинался он достаточно просто и при желании можно продолжать прослеживать развитие кода, переходя по версиям в git репозитории. Лично мне такой способ помогает понять и разобраться, как появился и развивался тот или иной функционал. Если появится время, то следующая статья будет про libcontainer, на который перешел Docker после LXC.
ссылка на оригинал статьи https://habr.com/ru/articles/575296/
Добавить комментарий