Вступление
Данная статья является четвертой в цикле (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.
kruftik
а кто может объяснить, в чем смысл Add(-1) вместо Done?
nick1612 Автор
Может быть в старой версии Go не было метода Done или они про него не знали:) Но вообще, это одно и тоже.