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

Image Management: tar-архів → rootfs → контейнер

Image Management: tar-архів → rootfs → контейнер

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Контейнерний образ - це по суті zip-архів з файловою системою. Нічого складного. У Sheep образ - це директорія з rootfs і manifest.json.

Структура образу

/var/lib/sheep/images/
  abc123/
    manifest.json    <- метадані
    rootfs/          <- файлова система
      bin/
      etc/
      usr/
      var/

manifest.json:

type Image struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Tag       string    `json:"tag"`
    Size      int64     `json:"size"`
    CreatedAt time.Time `json:"created_at"`
    RootFS    string    `json:"rootfs"`
}

Два способи отримати образ

1. Import - з tar-архіву

func (im *ImageManager) Import(name, tag, tarPath string) (*Image, error) {
    id := GenerateID()
    rootfs := filepath.Join(im.baseDir, id, "rootfs")
    os.MkdirAll(rootfs, 0755)

    f, err := os.Open(tarPath)
    if err != nil {
        return nil, fmt.Errorf("open tar: %w", err)
    }
    defer f.Close()

    if err := extractTar(f, rootfs); err != nil {
        os.RemoveAll(filepath.Join(im.baseDir, id))
        return nil, fmt.Errorf("extract tar: %w", err)
    }

    img := &Image{
        ID: id, Name: name, Tag: tag,
        CreatedAt: time.Now(), RootFS: rootfs,
    }

    // Рахуємо розмір
    var size int64
    filepath.Walk(rootfs, func(_ string, info os.FileInfo, _ error) error {
        if info != nil && !info.IsDir() {
            size += info.Size()
        }
        return nil
    })
    img.Size = size

    im.saveMetadata(id, img)
    return img, nil
}

Використання: sheep import myimage rootfs.tar.gz

2. Bootstrap - з хостової системи

Для тестування можна створити мінімальний образ з бінарників хоста:

func (im *ImageManager) Bootstrap(name string) (*Image, error) {
    id := GenerateID()
    rootfs := filepath.Join(im.baseDir, id, "rootfs")

    dirs := []string{
        "bin", "sbin", "usr/bin", "usr/sbin", "usr/lib",
        "lib", "lib64", "etc", "dev", "proc", "sys",
        "tmp", "var", "run", "home", "root",
    }
    for _, d := range dirs {
        os.MkdirAll(filepath.Join(rootfs, d), 0755)
    }

    // Копіюємо базові бінарники
    binaries := []string{
        "/bin/sh", "/bin/ls", "/bin/cat",
        "/bin/echo", "/bin/ps", "/bin/mkdir", "/bin/sleep",
    }
    for _, bin := range binaries {
        if _, err := os.Stat(bin); err == nil {
            copyFile(bin, filepath.Join(rootfs, bin))
        }
    }

    // Мінімальні конфіги
    os.WriteFile(filepath.Join(rootfs, "etc/hostname"),
        []byte("sheep\n"), 0644)
    os.WriteFile(filepath.Join(rootfs, "etc/hosts"),
        []byte("127.0.0.1 localhost\n"), 0644)
    os.WriteFile(filepath.Join(rootfs, "etc/resolv.conf"),
        []byte("nameserver 8.8.8.8\n"), 0644)

    // ...
    return img, nil
}

Використання: sheep bootstrap minimal

Розпакування tar-архіву

func extractTar(r io.Reader, dst string) error {
    // Пробуємо gzip, fallback на plain tar
    gr, err := gzip.NewReader(r)
    var tr *tar.Reader
    if err != nil {
        if rs, ok := r.(io.ReadSeeker); ok {
            rs.Seek(0, io.SeekStart)
        }
        tr = tar.NewReader(r)
    } else {
        defer gr.Close()
        tr = tar.NewReader(gr)
    }

    for {
        hdr, err := tr.Next()
        if err == io.EOF { break }
        if err != nil { return err }

        target := filepath.Join(dst, hdr.Name)

        switch hdr.Typeflag {
        case tar.TypeDir:
            os.MkdirAll(target, os.FileMode(hdr.Mode))
        case tar.TypeReg:
            os.MkdirAll(filepath.Dir(target), 0755)
            f, _ := os.OpenFile(target,
                os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
                os.FileMode(hdr.Mode))
            io.Copy(f, tr)
            f.Close()
        case tar.TypeSymlink:
            os.Symlink(hdr.Linkname, target)
        case tar.TypeLink:
            os.Link(filepath.Join(dst, hdr.Linkname), target)
        }
    }
    return nil
}

Функція обробляє і gzip, і plain tar. Підтримує директорії, файли, символічні і жорсткі посилання.

Whiteout файли (OCI layers)

При pull з реєстру кожен шар може видаляти файли з попереднього шару через whiteout файли:

// В extractTarReader для OCI шарів:
name := hdr.Name
if strings.HasPrefix(filepath.Base(name), ".wh.") {
    // Whiteout: видаляємо відповідний файл
    target := filepath.Join(dst, filepath.Dir(name),
        strings.TrimPrefix(filepath.Base(name), ".wh."))
    os.RemoveAll(target)
    continue
}

Файл .wh.config.old означає "видали config.old з попереднього шару". Це дозволяє шарам не тільки додавати, а й видаляти файли.

Tag та пошук образів

func (im *ImageManager) Get(name, tag string) (*Image, error) {
    if tag == "" { tag = "latest" }

    entries, _ := os.ReadDir(im.baseDir)
    for _, e := range entries {
        if !e.IsDir() { continue }
        img, err := im.loadMetadata(e.Name())
        if err != nil { continue }
        if img.Name == name && img.Tag == tag {
            return img, nil
        }
    }
    return nil, fmt.Errorf("image %s:%s not found", name, tag)
}

Пошук лінійний - перебираємо всі директорії. Для тисяч образів це повільно, але для навчального проекту працює.

Що тут не ідеально

У Docker образи складаються з шарів, де кожен шар - це diff від попереднього. Це дозволяє ділити шари між образами. У Sheep кожен образ - це повний rootfs. Простіше, але займає більше місця.

Спробуй сам

# Імпорт з tar:
sudo ./sheep import myimage rootfs.tar.gz
# Bootstrap з хоста:
sudo ./sheep bootstrap test-img
# Список образів:
sudo ./sheep images

Образи готові. Далі - Container Lifecycle: повна state machine контейнера.

Ресурси

Попередня: NAT і iptables