165 lines
3.6 KiB
Go
165 lines
3.6 KiB
Go
package rootfs
|
|
|
|
import (
|
|
"archive/tar"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.uber.org/multierr"
|
|
)
|
|
|
|
const (
|
|
_manifestJSON = "manifest.json"
|
|
_layerSuffix = "/layer.tar"
|
|
)
|
|
|
|
var (
|
|
errBadManifest = errors.New("bad or missing manifest.json")
|
|
)
|
|
|
|
type dockerManifestJSON []struct {
|
|
Config string `json:"Config,omitempty"`
|
|
Layers []string `json:"Layers"`
|
|
}
|
|
|
|
// RootFS accepts a docker layer tarball and writes it to outfile.
|
|
// 1. create map[string]io.ReadSeeker for each layer.
|
|
// 2. parse manifest.json and get the layer order.
|
|
// 3. go through each layer in order and write:
|
|
// a) to an ordered slice: the file name.
|
|
// b) to an FS map: where does the file come from?
|
|
// I) layer name
|
|
// II) offset (0 being the first file in the layer)
|
|
// 4. go through
|
|
func RootFS(in io.ReadSeeker, out io.Writer) (err error) {
|
|
tr := tar.NewReader(in)
|
|
tw := tar.NewWriter(out)
|
|
defer func() { err = multierr.Append(err, tw.Close()) }()
|
|
// layerOffsets maps a layer name (a9b123c0daa/layer.tar) to it's offset
|
|
layerOffsets := map[string]int64{}
|
|
|
|
// manifest is the docker manifest in the image
|
|
var manifest dockerManifestJSON
|
|
|
|
// phase 1: get layer offsets and manifest.json
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if hdr.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case filepath.Clean(hdr.Name) == _manifestJSON:
|
|
dec := json.NewDecoder(tr)
|
|
if err := dec.Decode(&manifest); err != nil {
|
|
return fmt.Errorf("parse %s: %w", _manifestJSON, err)
|
|
}
|
|
case strings.HasSuffix(hdr.Name, _layerSuffix):
|
|
here, err := in.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layerOffsets[hdr.Name] = here
|
|
}
|
|
}
|
|
|
|
if len(manifest) == 0 || len(layerOffsets) != len(manifest[0].Layers) {
|
|
return errBadManifest
|
|
}
|
|
|
|
// enumerate layers the way they would be laid down in the image
|
|
layers := make([]int64, len(layerOffsets))
|
|
for i, name := range manifest[0].Layers {
|
|
layers[i] = layerOffsets[name]
|
|
}
|
|
|
|
// file2layer maps a filename to layer number (index in "layers")
|
|
file2layer := map[string]int{}
|
|
|
|
// iterate through all layers and save filenames for all kinds of files.
|
|
for i, offset := range layers {
|
|
if _, err := in.Seek(offset, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
tr = tar.NewReader(in)
|
|
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file2layer[hdr.Name] = i
|
|
}
|
|
}
|
|
|
|
// phase 3: iterate through all layers and write files.
|
|
for i, offset := range layers {
|
|
if _, err := in.Seek(offset, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
tr = tar.NewReader(in)
|
|
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only directories can have multiple entries with the same name.
|
|
// all other file types cannot.
|
|
if hdr.Typeflag != tar.TypeDir && file2layer[hdr.Name] != i {
|
|
continue
|
|
}
|
|
|
|
if err := writeFile(tr, tw, hdr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeFile(tr *tar.Reader, tw *tar.Writer, hdr *tar.Header) error {
|
|
hdrOut := &tar.Header{
|
|
Typeflag: hdr.Typeflag,
|
|
Name: hdr.Name,
|
|
Linkname: hdr.Linkname,
|
|
Size: hdr.Size,
|
|
Mode: int64(hdr.Mode & 0777),
|
|
Uid: hdr.Uid,
|
|
Gid: hdr.Gid,
|
|
Uname: hdr.Uname,
|
|
Gname: hdr.Gname,
|
|
ModTime: hdr.ModTime,
|
|
Devmajor: hdr.Devmajor,
|
|
Devminor: hdr.Devminor,
|
|
Format: tar.FormatGNU,
|
|
}
|
|
|
|
if err := tw.WriteHeader(hdrOut); err != nil {
|
|
return err
|
|
}
|
|
|
|
if hdr.Typeflag == tar.TypeReg {
|
|
if _, err := io.Copy(tw, tr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|