Add layer store
Layer store manages read-only and read-write layers on a union file system. Read only layers are always referenced by content addresses. Read-write layer identifiers are handled by the caller but upon registering its difference, the committed read-only layer will be referenced by content hash. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan) Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
parent
48c7a1618d
commit
500e77bad0
13 changed files with 3278 additions and 0 deletions
47
layer/empty.go
Normal file
47
layer/empty.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DigestSHA256EmptyTar is the canonical sha256 digest of empty tar file -
|
||||||
|
// (1024 NULL bytes)
|
||||||
|
const DigestSHA256EmptyTar = DiffID("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef")
|
||||||
|
|
||||||
|
type emptyLayer struct{}
|
||||||
|
|
||||||
|
// EmptyLayer is a layer that corresponds to empty tar.
|
||||||
|
var EmptyLayer = &emptyLayer{}
|
||||||
|
|
||||||
|
func (el *emptyLayer) TarStream() (io.Reader, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
tarWriter := tar.NewWriter(buf)
|
||||||
|
tarWriter.Close()
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) ChainID() ChainID {
|
||||||
|
return ChainID(DigestSHA256EmptyTar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) DiffID() DiffID {
|
||||||
|
return DigestSHA256EmptyTar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) Parent() Layer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) Size() (size int64, err error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) DiffSize() (size int64, err error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *emptyLayer) Metadata() (map[string]string, error) {
|
||||||
|
return make(map[string]string), nil
|
||||||
|
}
|
46
layer/empty_test.go
Normal file
46
layer/empty_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyLayer(t *testing.T) {
|
||||||
|
if EmptyLayer.ChainID() != ChainID(DigestSHA256EmptyTar) {
|
||||||
|
t.Fatal("wrong ID for empty layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if EmptyLayer.DiffID() != DigestSHA256EmptyTar {
|
||||||
|
t.Fatal("wrong DiffID for empty layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if EmptyLayer.Parent() != nil {
|
||||||
|
t.Fatal("expected no parent for empty layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if size, err := EmptyLayer.Size(); err != nil || size != 0 {
|
||||||
|
t.Fatal("expected zero size for empty layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diffSize, err := EmptyLayer.DiffSize(); err != nil || diffSize != 0 {
|
||||||
|
t.Fatal("expected zero diffsize for empty layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
tarStream, err := EmptyLayer.TarStream()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error streaming tar for empty layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
digester := digest.Canonical.New()
|
||||||
|
_, err = io.Copy(digester.Hash(), tarStream)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error hashing empty tar layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if digester.Digest() != digest.Digest(DigestSHA256EmptyTar) {
|
||||||
|
t.Fatal("empty layer tar stream hashes to wrong value")
|
||||||
|
}
|
||||||
|
}
|
318
layer/filestore.go
Normal file
318
layer/filestore.go
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stringIDRegexp = regexp.MustCompile(`^[a-f0-9]{64}(-init)?$`)
|
||||||
|
supportedAlgorithms = []digest.Algorithm{
|
||||||
|
digest.SHA256,
|
||||||
|
// digest.SHA384, // Currently not used
|
||||||
|
// digest.SHA512, // Currently not used
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileMetadataStore struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileMetadataTransaction struct {
|
||||||
|
store *fileMetadataStore
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSMetadataStore returns an instance of a metadata store
|
||||||
|
// which is backed by files on disk using the provided root
|
||||||
|
// as the root of metadata files.
|
||||||
|
func NewFSMetadataStore(root string) (MetadataStore, error) {
|
||||||
|
if err := os.MkdirAll(root, 0700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &fileMetadataStore{
|
||||||
|
root: root,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) getLayerDirectory(layer ChainID) string {
|
||||||
|
dgst := digest.Digest(layer)
|
||||||
|
return filepath.Join(fms.root, string(dgst.Algorithm()), dgst.Hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) getLayerFilename(layer ChainID, filename string) string {
|
||||||
|
return filepath.Join(fms.getLayerDirectory(layer), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) getMountDirectory(mount string) string {
|
||||||
|
return filepath.Join(fms.root, "mounts", mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) getMountFilename(mount, filename string) string {
|
||||||
|
return filepath.Join(fms.getMountDirectory(mount), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) StartTransaction() (MetadataTransaction, error) {
|
||||||
|
tmpDir := filepath.Join(fms.root, "tmp")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
td, err := ioutil.TempDir(tmpDir, "layer-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Create a new tempdir
|
||||||
|
return &fileMetadataTransaction{
|
||||||
|
store: fms,
|
||||||
|
root: td,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) SetSize(size int64) error {
|
||||||
|
content := fmt.Sprintf("%d", size)
|
||||||
|
return ioutil.WriteFile(filepath.Join(fm.root, "size"), []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) SetParent(parent ChainID) error {
|
||||||
|
return ioutil.WriteFile(filepath.Join(fm.root, "parent"), []byte(digest.Digest(parent).String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) SetDiffID(diff DiffID) error {
|
||||||
|
return ioutil.WriteFile(filepath.Join(fm.root, "diff"), []byte(digest.Digest(diff).String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) SetCacheID(cacheID string) error {
|
||||||
|
return ioutil.WriteFile(filepath.Join(fm.root, "cache-id"), []byte(cacheID), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) TarSplitWriter() (io.WriteCloser, error) {
|
||||||
|
f, err := os.OpenFile(filepath.Join(fm.root, "tar-split.json.gz"), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fz := gzip.NewWriter(f)
|
||||||
|
|
||||||
|
return ioutils.NewWriteCloserWrapper(fz, func() error {
|
||||||
|
fz.Close()
|
||||||
|
return f.Close()
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) Commit(layer ChainID) error {
|
||||||
|
finalDir := fm.store.getLayerDirectory(layer)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(finalDir), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(fm.root, finalDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) Cancel() error {
|
||||||
|
return os.RemoveAll(fm.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *fileMetadataTransaction) String() string {
|
||||||
|
return fm.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetSize(layer ChainID) (int64, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "size"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := strconv.ParseInt(string(content), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetParent(layer ChainID) (ChainID, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "parent"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := digest.ParseDigest(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChainID(dgst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetDiffID(layer ChainID) (DiffID, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "diff"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := digest.ParseDigest(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiffID(dgst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetCacheID(layer ChainID) (string, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getLayerFilename(layer, "cache-id"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringIDRegexp.MatchString(string(content)) {
|
||||||
|
return "", errors.New("invalid cache id value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) TarSplitReader(layer ChainID) (io.ReadCloser, error) {
|
||||||
|
fz, err := os.Open(fms.getLayerFilename(layer, "tar-split.json.gz"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, err := gzip.NewReader(fz)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutils.NewReadCloserWrapper(f, func() error {
|
||||||
|
f.Close()
|
||||||
|
return fz.Close()
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) SetMountID(mount string, mountID string) error {
|
||||||
|
if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(fms.getMountFilename(mount, "mount-id"), []byte(mountID), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) SetInitID(mount string, init string) error {
|
||||||
|
if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(fms.getMountFilename(mount, "init-id"), []byte(init), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) SetMountParent(mount string, parent ChainID) error {
|
||||||
|
if err := os.MkdirAll(fms.getMountDirectory(mount), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(fms.getMountFilename(mount, "parent"), []byte(digest.Digest(parent).String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetMountID(mount string) (string, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getMountFilename(mount, "mount-id"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringIDRegexp.MatchString(string(content)) {
|
||||||
|
return "", errors.New("invalid mount id value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetInitID(mount string) (string, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getMountFilename(mount, "init-id"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringIDRegexp.MatchString(string(content)) {
|
||||||
|
return "", errors.New("invalid init id value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) GetMountParent(mount string) (ChainID, error) {
|
||||||
|
content, err := ioutil.ReadFile(fms.getMountFilename(mount, "parent"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := digest.ParseDigest(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChainID(dgst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) List() ([]ChainID, []string, error) {
|
||||||
|
var ids []ChainID
|
||||||
|
for _, algorithm := range supportedAlgorithms {
|
||||||
|
fileInfos, err := ioutil.ReadDir(filepath.Join(fms.root, string(algorithm)))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fi := range fileInfos {
|
||||||
|
if fi.IsDir() && fi.Name() != "mounts" {
|
||||||
|
dgst := digest.NewDigestFromHex(string(algorithm), fi.Name())
|
||||||
|
if err := dgst.Validate(); err != nil {
|
||||||
|
logrus.Debugf("Ignoring invalid digest %s:%s", algorithm, fi.Name())
|
||||||
|
} else {
|
||||||
|
ids = append(ids, ChainID(dgst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfos, err := ioutil.ReadDir(filepath.Join(fms.root, "mounts"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return ids, []string{}, nil
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mounts []string
|
||||||
|
for _, fi := range fileInfos {
|
||||||
|
if fi.IsDir() {
|
||||||
|
mounts = append(mounts, fi.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) Remove(layer ChainID) error {
|
||||||
|
return os.RemoveAll(fms.getLayerDirectory(layer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fms *fileMetadataStore) RemoveMount(mount string) error {
|
||||||
|
return os.RemoveAll(fms.getMountDirectory(mount))
|
||||||
|
}
|
119
layer/filestore_test.go
Normal file
119
layer/filestore_test.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomLayerID(seed int64) ChainID {
|
||||||
|
r := rand.New(rand.NewSource(seed))
|
||||||
|
dgst, err := digest.FromBytes([]byte(fmt.Sprintf("%d", r.Int63())))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChainID(dgst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileMetadataStore(t *testing.T) (*fileMetadataStore, string, func()) {
|
||||||
|
td, err := ioutil.TempDir("", "layers-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fms, err := NewFSMetadataStore(td)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fms.(*fileMetadataStore), td, func() {
|
||||||
|
if err := os.RemoveAll(td); err != nil {
|
||||||
|
t.Logf("Failed to cleanup %q: %s", td, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNotDirectoryError(t *testing.T, err error) {
|
||||||
|
perr, ok := err.(*os.PathError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unexpected error %#v, expected path error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if perr.Err != syscall.ENOTDIR {
|
||||||
|
t.Fatalf("Unexpected error %s, expected %s", perr.Err, syscall.ENOTDIR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPermissionError(t *testing.T, err error) {
|
||||||
|
perr, ok := err.(*os.PathError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unexpected error %#v, expected path error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if perr.Err != syscall.EACCES {
|
||||||
|
t.Fatalf("Unexpected error %s, expected %s", perr.Err, syscall.EACCES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommitFailure(t *testing.T) {
|
||||||
|
fms, td, cleanup := newFileMetadataStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(td, "sha256"), []byte("was here first!"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := fms.StartTransaction()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.SetSize(0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(randomLayerID(5))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error committing with invalid layer parent directory")
|
||||||
|
}
|
||||||
|
assertNotDirectoryError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartTransactionFailure(t *testing.T) {
|
||||||
|
fms, td, cleanup := newFileMetadataStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(td, "tmp"), []byte("was here first!"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fms.StartTransaction()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error starting transaction with invalid layer parent directory")
|
||||||
|
}
|
||||||
|
assertNotDirectoryError(t, err)
|
||||||
|
|
||||||
|
if err := os.Remove(filepath.Join(td, "tmp")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := fms.StartTransaction()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := filepath.Join(td, "tmp"); strings.HasPrefix(expected, tx.String()) {
|
||||||
|
t.Fatalf("Unexpected transaction string %q, expected prefix %q", tx.String(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Cancel(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
238
layer/layer.go
Normal file
238
layer/layer.go
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
// Package layer is package for managing read only
|
||||||
|
// and read-write mounts on the union file system
|
||||||
|
// driver. Read-only mounts are refenced using a
|
||||||
|
// content hash and are protected from mutation in
|
||||||
|
// the exposed interface. The tar format is used
|
||||||
|
// to create read only layers and export both
|
||||||
|
// read only and writable layers. The exported
|
||||||
|
// tar data for a read only layer should match
|
||||||
|
// the tar used to create the layer.
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrLayerDoesNotExist is used when an operation is
|
||||||
|
// attempted on a layer which does not exist.
|
||||||
|
ErrLayerDoesNotExist = errors.New("layer does not exist")
|
||||||
|
|
||||||
|
// ErrLayerNotRetained is used when a release is
|
||||||
|
// attempted on a layer which is not retained.
|
||||||
|
ErrLayerNotRetained = errors.New("layer not retained")
|
||||||
|
|
||||||
|
// ErrMountDoesNotExist is used when an operation is
|
||||||
|
// attempted on a mount layer which does not exist.
|
||||||
|
ErrMountDoesNotExist = errors.New("mount does not exist")
|
||||||
|
|
||||||
|
// ErrActiveMount is used when an operation on a
|
||||||
|
// mount is attempted but the layer is still
|
||||||
|
// mounted and the operation cannot be performed.
|
||||||
|
ErrActiveMount = errors.New("mount still active")
|
||||||
|
|
||||||
|
// ErrNotMounted is used when requesting an active
|
||||||
|
// mount but the layer is not mounted.
|
||||||
|
ErrNotMounted = errors.New("not mounted")
|
||||||
|
|
||||||
|
// ErrMaxDepthExceeded is used when a layer is attempted
|
||||||
|
// to be created which would result in a layer depth
|
||||||
|
// greater than the 125 max.
|
||||||
|
ErrMaxDepthExceeded = errors.New("max depth exceeded")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChainID is the content-addressable ID of a layer.
|
||||||
|
type ChainID digest.Digest
|
||||||
|
|
||||||
|
// String returns a string rendition of a layer ID
|
||||||
|
func (id ChainID) String() string {
|
||||||
|
return string(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiffID is the hash of an individual layer tar.
|
||||||
|
type DiffID digest.Digest
|
||||||
|
|
||||||
|
// String returns a string rendition of a layer DiffID
|
||||||
|
func (diffID DiffID) String() string {
|
||||||
|
return string(diffID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TarStreamer represents an object which may
|
||||||
|
// have its contents exported as a tar stream.
|
||||||
|
type TarStreamer interface {
|
||||||
|
// TarStream returns a tar archive stream
|
||||||
|
// for the contents of a layer.
|
||||||
|
TarStream() (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer represents a read only layer
|
||||||
|
type Layer interface {
|
||||||
|
TarStreamer
|
||||||
|
|
||||||
|
// ChainID returns the content hash of the entire layer chain. The hash
|
||||||
|
// chain is made up of DiffID of top layer and all of its parents.
|
||||||
|
ChainID() ChainID
|
||||||
|
|
||||||
|
// DiffID returns the content hash of the layer
|
||||||
|
// tar stream used to create this layer.
|
||||||
|
DiffID() DiffID
|
||||||
|
|
||||||
|
// Parent returns the next layer in the layer chain.
|
||||||
|
Parent() Layer
|
||||||
|
|
||||||
|
// Size returns the size of the entire layer chain. The size
|
||||||
|
// is calculated from the total size of all files in the layers.
|
||||||
|
Size() (int64, error)
|
||||||
|
|
||||||
|
// DiffSize returns the size difference of the top layer
|
||||||
|
// from parent layer.
|
||||||
|
DiffSize() (int64, error)
|
||||||
|
|
||||||
|
// Metadata returns the low level storage metadata associated
|
||||||
|
// with layer.
|
||||||
|
Metadata() (map[string]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RWLayer represents a layer which is
|
||||||
|
// read and writable
|
||||||
|
type RWLayer interface {
|
||||||
|
TarStreamer
|
||||||
|
|
||||||
|
// Path returns the filesystem path to the writable
|
||||||
|
// layer.
|
||||||
|
Path() (string, error)
|
||||||
|
|
||||||
|
// Parent returns the layer which the writable
|
||||||
|
// layer was created from.
|
||||||
|
Parent() Layer
|
||||||
|
|
||||||
|
// Size represents the size of the writable layer
|
||||||
|
// as calculated by the total size of the files
|
||||||
|
// changed in the mutable layer.
|
||||||
|
Size() (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata holds information about a
|
||||||
|
// read only layer
|
||||||
|
type Metadata struct {
|
||||||
|
// ChainID is the content hash of the layer
|
||||||
|
ChainID ChainID
|
||||||
|
|
||||||
|
// DiffID is the hash of the tar data used to
|
||||||
|
// create the layer
|
||||||
|
DiffID DiffID
|
||||||
|
|
||||||
|
// Size is the size of the layer and all parents
|
||||||
|
Size int64
|
||||||
|
|
||||||
|
// DiffSize is the size of the top layer
|
||||||
|
DiffSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MountInit is a function to initialize a
|
||||||
|
// writable mount. Changes made here will
|
||||||
|
// not be included in the Tar stream of the
|
||||||
|
// RWLayer.
|
||||||
|
type MountInit func(root string) error
|
||||||
|
|
||||||
|
// Store represents a backend for managing both
|
||||||
|
// read-only and read-write layers.
|
||||||
|
type Store interface {
|
||||||
|
Register(io.Reader, ChainID) (Layer, error)
|
||||||
|
Get(ChainID) (Layer, error)
|
||||||
|
Release(Layer) ([]Metadata, error)
|
||||||
|
|
||||||
|
Mount(id string, parent ChainID, label string, init MountInit) (RWLayer, error)
|
||||||
|
Unmount(id string) error
|
||||||
|
DeleteMount(id string) ([]Metadata, error)
|
||||||
|
Changes(id string) ([]archive.Change, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadataTransaction represents functions for setting layer metadata
|
||||||
|
// with a single transaction.
|
||||||
|
type MetadataTransaction interface {
|
||||||
|
SetSize(int64) error
|
||||||
|
SetParent(parent ChainID) error
|
||||||
|
SetDiffID(DiffID) error
|
||||||
|
SetCacheID(string) error
|
||||||
|
TarSplitWriter() (io.WriteCloser, error)
|
||||||
|
|
||||||
|
Commit(ChainID) error
|
||||||
|
Cancel() error
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadataStore represents a backend for persisting
|
||||||
|
// metadata about layers and providing the metadata
|
||||||
|
// for restoring a Store.
|
||||||
|
type MetadataStore interface {
|
||||||
|
// StartTransaction starts an update for new metadata
|
||||||
|
// which will be used to represent an ID on commit.
|
||||||
|
StartTransaction() (MetadataTransaction, error)
|
||||||
|
|
||||||
|
GetSize(ChainID) (int64, error)
|
||||||
|
GetParent(ChainID) (ChainID, error)
|
||||||
|
GetDiffID(ChainID) (DiffID, error)
|
||||||
|
GetCacheID(ChainID) (string, error)
|
||||||
|
TarSplitReader(ChainID) (io.ReadCloser, error)
|
||||||
|
|
||||||
|
SetMountID(string, string) error
|
||||||
|
SetInitID(string, string) error
|
||||||
|
SetMountParent(string, ChainID) error
|
||||||
|
|
||||||
|
GetMountID(string) (string, error)
|
||||||
|
GetInitID(string) (string, error)
|
||||||
|
GetMountParent(string) (ChainID, error)
|
||||||
|
|
||||||
|
// List returns the full list of referened
|
||||||
|
// read-only and read-write layers
|
||||||
|
List() ([]ChainID, []string, error)
|
||||||
|
|
||||||
|
Remove(ChainID) error
|
||||||
|
RemoveMount(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateChainID returns ID for a layerDigest slice
|
||||||
|
func CreateChainID(dgsts []DiffID) ChainID {
|
||||||
|
return createChainIDFromParent("", dgsts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createChainIDFromParent(parent ChainID, dgsts ...DiffID) ChainID {
|
||||||
|
if len(dgsts) == 0 {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
if parent == "" {
|
||||||
|
return createChainIDFromParent(ChainID(dgsts[0]), dgsts[1:]...)
|
||||||
|
}
|
||||||
|
// H = "H(n-1) SHA256(n)"
|
||||||
|
dgst, err := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0])))
|
||||||
|
if err != nil {
|
||||||
|
// Digest calculation is not expected to throw an error,
|
||||||
|
// any error at this point is a program error
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return createChainIDFromParent(ChainID(dgst), dgsts[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseAndLog releases the provided layer from the given layer
|
||||||
|
// store, logging any error and release metadata
|
||||||
|
func ReleaseAndLog(ls Store, l Layer) {
|
||||||
|
metadata, err := ls.Release(l)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error releasing layer %s: %v", l.ChainID(), err)
|
||||||
|
}
|
||||||
|
LogReleaseMetadata(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogReleaseMetadata logs a metadata array, use this to
|
||||||
|
// ensure consistent logging for release metadata
|
||||||
|
func LogReleaseMetadata(metadatas []Metadata) {
|
||||||
|
for _, metadata := range metadatas {
|
||||||
|
logrus.Infof("Layer %s cleaned up", metadata.ChainID)
|
||||||
|
}
|
||||||
|
}
|
649
layer/layer_store.go
Normal file
649
layer/layer_store.go
Normal file
|
@ -0,0 +1,649 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/docker/daemon/graphdriver"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/vbatts/tar-split/tar/asm"
|
||||||
|
"github.com/vbatts/tar-split/tar/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxLayerDepth represents the maximum number of
|
||||||
|
// layers which can be chained together. 125 was
|
||||||
|
// chosen to account for the 127 max in some
|
||||||
|
// graphdrivers plus the 2 additional layers
|
||||||
|
// used to create a rwlayer.
|
||||||
|
const maxLayerDepth = 125
|
||||||
|
|
||||||
|
type layerStore struct {
|
||||||
|
store MetadataStore
|
||||||
|
driver graphdriver.Driver
|
||||||
|
|
||||||
|
layerMap map[ChainID]*roLayer
|
||||||
|
layerL sync.Mutex
|
||||||
|
|
||||||
|
mounts map[string]*mountedLayer
|
||||||
|
mountL sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStore creates a new Store instance using
|
||||||
|
// the provided metadata store and graph driver.
|
||||||
|
// The metadata store will be used to restore
|
||||||
|
// the Store.
|
||||||
|
func NewStore(store MetadataStore, driver graphdriver.Driver) (Store, error) {
|
||||||
|
ls := &layerStore{
|
||||||
|
store: store,
|
||||||
|
driver: driver,
|
||||||
|
layerMap: map[ChainID]*roLayer{},
|
||||||
|
mounts: map[string]*mountedLayer{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, mounts, err := store.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
l, err := ls.loadLayer(id)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Failed to load layer %s: %s", id, err)
|
||||||
|
}
|
||||||
|
if l.parent != nil {
|
||||||
|
l.parent.referenceCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mount := range mounts {
|
||||||
|
if err := ls.loadMount(mount); err != nil {
|
||||||
|
logrus.Debugf("Failed to load mount %s: %s", mount, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) loadLayer(layer ChainID) (*roLayer, error) {
|
||||||
|
cl, ok := ls.layerMap[layer]
|
||||||
|
if ok {
|
||||||
|
return cl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
diff, err := ls.store.GetDiffID(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := ls.store.GetSize(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheID, err := ls.store.GetCacheID(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := ls.store.GetParent(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cl = &roLayer{
|
||||||
|
chainID: layer,
|
||||||
|
diffID: diff,
|
||||||
|
size: size,
|
||||||
|
cacheID: cacheID,
|
||||||
|
layerStore: ls,
|
||||||
|
references: map[Layer]struct{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent != "" {
|
||||||
|
p, err := ls.loadLayer(parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cl.parent = p
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerMap[cl.chainID] = cl
|
||||||
|
|
||||||
|
return cl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) loadMount(mount string) error {
|
||||||
|
if _, ok := ls.mounts[mount]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mountID, err := ls.store.GetMountID(mount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
initID, err := ls.store.GetInitID(mount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := ls.store.GetMountParent(mount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ml := &mountedLayer{
|
||||||
|
name: mount,
|
||||||
|
mountID: mountID,
|
||||||
|
initID: initID,
|
||||||
|
layerStore: ls,
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent != "" {
|
||||||
|
p, err := ls.loadLayer(parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ml.parent = p
|
||||||
|
|
||||||
|
p.referenceCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.mounts[ml.name] = ml
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) applyTar(tx MetadataTransaction, ts io.Reader, parent string, layer *roLayer) error {
|
||||||
|
digester := digest.Canonical.New()
|
||||||
|
tr := io.TeeReader(ts, digester.Hash())
|
||||||
|
|
||||||
|
tsw, err := tx.TarSplitWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metaPacker := storage.NewJSONPacker(tsw)
|
||||||
|
defer tsw.Close()
|
||||||
|
|
||||||
|
// we're passing nil here for the file putter, because the ApplyDiff will
|
||||||
|
// handle the extraction of the archive
|
||||||
|
rdr, err := asm.NewInputTarStream(tr, metaPacker, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
applySize, err := ls.driver.ApplyDiff(layer.cacheID, parent, archive.Reader(rdr))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard trailing data but ensure metadata is picked up to reconstruct stream
|
||||||
|
io.Copy(ioutil.Discard, rdr) // ignore error as reader may be closed
|
||||||
|
|
||||||
|
layer.size = applySize
|
||||||
|
layer.diffID = DiffID(digester.Digest())
|
||||||
|
|
||||||
|
logrus.Debugf("Applied tar %s to %s, size: %d", layer.diffID, layer.cacheID, applySize)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Register(ts io.Reader, parent ChainID) (Layer, error) {
|
||||||
|
// err is used to hold the error which will always trigger
|
||||||
|
// cleanup of creates sources but may not be an error returned
|
||||||
|
// to the caller (already exists).
|
||||||
|
var err error
|
||||||
|
var pid string
|
||||||
|
var p *roLayer
|
||||||
|
if string(parent) != "" {
|
||||||
|
p = ls.get(parent)
|
||||||
|
if p == nil {
|
||||||
|
return nil, ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
pid = p.cacheID
|
||||||
|
// Release parent chain if error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
ls.releaseLayer(p)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if p.depth() >= maxLayerDepth {
|
||||||
|
err = ErrMaxDepthExceeded
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new roLayer
|
||||||
|
layer := &roLayer{
|
||||||
|
parent: p,
|
||||||
|
cacheID: stringid.GenerateRandomID(),
|
||||||
|
referenceCount: 1,
|
||||||
|
layerStore: ls,
|
||||||
|
references: map[Layer]struct{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ls.driver.Create(layer.cacheID, pid, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := ls.store.StartTransaction()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Cleaning up layer %s: %v", layer.cacheID, err)
|
||||||
|
if err := ls.driver.Remove(layer.cacheID); err != nil {
|
||||||
|
logrus.Errorf("Error cleaning up cache layer %s: %v", layer.cacheID, err)
|
||||||
|
}
|
||||||
|
if err := tx.Cancel(); err != nil {
|
||||||
|
logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = ls.applyTar(tx, ts, pid, layer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if layer.parent == nil {
|
||||||
|
layer.chainID = ChainID(layer.diffID)
|
||||||
|
} else {
|
||||||
|
layer.chainID = createChainIDFromParent(layer.parent.chainID, layer.diffID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = storeLayer(tx, layer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
|
||||||
|
if existingLayer := ls.getAndRetainLayer(layer.chainID); existingLayer != nil {
|
||||||
|
// Set error for cleanup, but do not return the error
|
||||||
|
err = errors.New("layer already exists")
|
||||||
|
return existingLayer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(layer.chainID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerMap[layer.chainID] = layer
|
||||||
|
|
||||||
|
return layer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) get(l ChainID) *roLayer {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
|
||||||
|
layer, ok := ls.layerMap[l]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.referenceCount++
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Get(l ChainID) (Layer, error) {
|
||||||
|
layer := ls.get(l)
|
||||||
|
if layer == nil {
|
||||||
|
return nil, ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) deleteLayer(layer *roLayer, metadata *Metadata) error {
|
||||||
|
err := ls.driver.Remove(layer.cacheID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ls.store.Remove(layer.chainID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metadata.DiffID = layer.diffID
|
||||||
|
metadata.ChainID = layer.chainID
|
||||||
|
metadata.Size, err = layer.Size()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metadata.DiffSize = layer.size
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) releaseLayers(l *roLayer, removed *[]Metadata, depth int) error {
|
||||||
|
if l.referenceCount == 0 {
|
||||||
|
panic("layer not retained")
|
||||||
|
}
|
||||||
|
l.referenceCount--
|
||||||
|
if l.referenceCount != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*removed) == 0 && depth > 0 {
|
||||||
|
panic("cannot remove layer with child")
|
||||||
|
}
|
||||||
|
if l.hasReferences() {
|
||||||
|
panic("cannot delete referenced layer")
|
||||||
|
}
|
||||||
|
var metadata Metadata
|
||||||
|
if err := ls.deleteLayer(l, &metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ls.layerMap, l.chainID)
|
||||||
|
*removed = append(*removed, metadata)
|
||||||
|
|
||||||
|
if l.parent != nil {
|
||||||
|
if err := ls.releaseLayers(l.parent, removed, depth+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) releaseLayer(layer *roLayer) ([]Metadata, error) {
|
||||||
|
removed := []Metadata{}
|
||||||
|
err := ls.releaseLayers(layer, &removed, 0)
|
||||||
|
return removed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Release(l Layer) ([]Metadata, error) {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
layer, ok := ls.layerMap[l.ChainID()]
|
||||||
|
if !ok {
|
||||||
|
return []Metadata{}, nil
|
||||||
|
}
|
||||||
|
if !layer.hasReference(l) {
|
||||||
|
return nil, ErrLayerNotRetained
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.deleteReference(l)
|
||||||
|
|
||||||
|
return ls.releaseLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) mount(m *mountedLayer, mountLabel string) error {
|
||||||
|
dir, err := ls.driver.Get(m.mountID, mountLabel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.path = dir
|
||||||
|
m.activityCount++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) saveMount(mount *mountedLayer) error {
|
||||||
|
if err := ls.store.SetMountID(mount.name, mount.mountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.initID != "" {
|
||||||
|
if err := ls.store.SetInitID(mount.name, mount.initID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.parent != nil {
|
||||||
|
if err := ls.store.SetMountParent(mount.name, mount.parent.chainID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.mounts[mount.name] = mount
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) getAndRetainLayer(layer ChainID) *roLayer {
|
||||||
|
l, ok := ls.layerMap[layer]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.referenceCount++
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) initMount(graphID, parent, mountLabel string, initFunc MountInit) (string, error) {
|
||||||
|
// Use "<graph-id>-init" to maintain compatibility with graph drivers
|
||||||
|
// which are expecting this layer with this special name. If all
|
||||||
|
// graph drivers can be updated to not rely on knowin about this layer
|
||||||
|
// then the initID should be randomly generated.
|
||||||
|
initID := fmt.Sprintf("%s-init", graphID)
|
||||||
|
|
||||||
|
if err := ls.driver.Create(initID, parent, mountLabel); err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
p, err := ls.driver.Get(initID, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initFunc(p); err != nil {
|
||||||
|
ls.driver.Put(initID)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.driver.Put(initID); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return initID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Mount(name string, parent ChainID, mountLabel string, initFunc MountInit) (l RWLayer, err error) {
|
||||||
|
ls.mountL.Lock()
|
||||||
|
defer ls.mountL.Unlock()
|
||||||
|
m, ok := ls.mounts[name]
|
||||||
|
if ok {
|
||||||
|
// Check if has path
|
||||||
|
if err := ls.mount(m, mountLabel); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pid string
|
||||||
|
var p *roLayer
|
||||||
|
if string(parent) != "" {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
p = ls.getAndRetainLayer(parent)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
if p == nil {
|
||||||
|
return nil, ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
pid = p.cacheID
|
||||||
|
|
||||||
|
// Release parent chain if error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
ls.releaseLayer(p)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
mountID := name
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
// windows has issues if container ID doesn't match mount ID
|
||||||
|
mountID = stringid.GenerateRandomID()
|
||||||
|
}
|
||||||
|
|
||||||
|
m = &mountedLayer{
|
||||||
|
name: name,
|
||||||
|
parent: p,
|
||||||
|
mountID: mountID,
|
||||||
|
layerStore: ls,
|
||||||
|
}
|
||||||
|
|
||||||
|
if initFunc != nil {
|
||||||
|
pid, err = ls.initMount(m.mountID, pid, mountLabel, initFunc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.initID = pid
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ls.driver.Create(m.mountID, pid, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ls.saveMount(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ls.mount(m, mountLabel); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Unmount(name string) error {
|
||||||
|
ls.mountL.Lock()
|
||||||
|
defer ls.mountL.Unlock()
|
||||||
|
|
||||||
|
m := ls.mounts[name]
|
||||||
|
if m == nil {
|
||||||
|
return ErrMountDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
m.activityCount--
|
||||||
|
|
||||||
|
if err := ls.driver.Put(m.mountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) DeleteMount(name string) ([]Metadata, error) {
|
||||||
|
ls.mountL.Lock()
|
||||||
|
defer ls.mountL.Unlock()
|
||||||
|
|
||||||
|
m := ls.mounts[name]
|
||||||
|
if m == nil {
|
||||||
|
return nil, ErrMountDoesNotExist
|
||||||
|
}
|
||||||
|
if m.activityCount > 0 {
|
||||||
|
return nil, ErrActiveMount
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ls.mounts, name)
|
||||||
|
|
||||||
|
if err := ls.driver.Remove(m.mountID); err != nil {
|
||||||
|
logrus.Errorf("Error removing mounted layer %s: %s", m.name, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.initID != "" {
|
||||||
|
if err := ls.driver.Remove(m.initID); err != nil {
|
||||||
|
logrus.Errorf("Error removing init layer %s: %s", m.name, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.store.RemoveMount(m.name); err != nil {
|
||||||
|
logrus.Errorf("Error removing mount metadata: %s: %s", m.name, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
if m.parent != nil {
|
||||||
|
return ls.releaseLayer(m.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []Metadata{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) Changes(name string) ([]archive.Change, error) {
|
||||||
|
ls.mountL.Lock()
|
||||||
|
m := ls.mounts[name]
|
||||||
|
ls.mountL.Unlock()
|
||||||
|
if m == nil {
|
||||||
|
return nil, ErrMountDoesNotExist
|
||||||
|
}
|
||||||
|
pid := m.initID
|
||||||
|
if pid == "" && m.parent != nil {
|
||||||
|
pid = m.parent.cacheID
|
||||||
|
}
|
||||||
|
return ls.driver.Changes(m.mountID, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) assembleTar(graphID string, metadata io.ReadCloser, size *int64) (io.Reader, error) {
|
||||||
|
type diffPathDriver interface {
|
||||||
|
DiffPath(string) (string, func() error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
diffDriver, ok := ls.driver.(diffPathDriver)
|
||||||
|
if !ok {
|
||||||
|
diffDriver = &naiveDiffPathDriver{ls.driver}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get our relative path to the container
|
||||||
|
fsPath, releasePath, err := diffDriver.DiffPath(graphID)
|
||||||
|
if err != nil {
|
||||||
|
metadata.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pR, pW := io.Pipe()
|
||||||
|
// this will need to be in a goroutine, as we are returning the stream of a
|
||||||
|
// tar archive, but can not close the metadata reader early (when this
|
||||||
|
// function returns)...
|
||||||
|
go func() {
|
||||||
|
defer releasePath()
|
||||||
|
defer metadata.Close()
|
||||||
|
|
||||||
|
metaUnpacker := storage.NewJSONUnpacker(metadata)
|
||||||
|
upackerCounter := &unpackSizeCounter{metaUnpacker, size}
|
||||||
|
fileGetter := storage.NewPathFileGetter(fsPath)
|
||||||
|
logrus.Debugf("Assembling tar data for %s from %s", graphID, fsPath)
|
||||||
|
ots := asm.NewOutputTarStream(fileGetter, upackerCounter)
|
||||||
|
defer ots.Close()
|
||||||
|
if _, err := io.Copy(pW, ots); err != nil {
|
||||||
|
pW.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pW.Close()
|
||||||
|
}()
|
||||||
|
return pR, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type naiveDiffPathDriver struct {
|
||||||
|
graphdriver.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *naiveDiffPathDriver) DiffPath(id string) (string, func() error, error) {
|
||||||
|
p, err := n.Driver.Get(id, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
return p, func() error {
|
||||||
|
return n.Driver.Put(id)
|
||||||
|
}, nil
|
||||||
|
}
|
725
layer/layer_test.go
Normal file
725
layer/layer_test.go
Normal file
|
@ -0,0 +1,725 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/docker/daemon/graphdriver"
|
||||||
|
"github.com/docker/docker/daemon/graphdriver/vfs"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/idtools"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
graphdriver.ApplyUncompressedLayer = archive.UnpackLayer
|
||||||
|
vfs.CopyWithTar = archive.CopyWithTar
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVFSGraphDriver(td string) (graphdriver.Driver, error) {
|
||||||
|
uidMap := []idtools.IDMap{
|
||||||
|
{
|
||||||
|
ContainerID: 0,
|
||||||
|
HostID: os.Getuid(),
|
||||||
|
Size: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
gidMap := []idtools.IDMap{
|
||||||
|
{
|
||||||
|
ContainerID: 0,
|
||||||
|
HostID: os.Getgid(),
|
||||||
|
Size: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return graphdriver.GetDriver("vfs", td, nil, uidMap, gidMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestGraphDriver(t *testing.T) (graphdriver.Driver, func()) {
|
||||||
|
td, err := ioutil.TempDir("", "graph-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := newVFSGraphDriver(td)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return driver, func() {
|
||||||
|
os.RemoveAll(td)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestStore(t *testing.T) (Store, func()) {
|
||||||
|
td, err := ioutil.TempDir("", "layerstore-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph, graphcleanup := newTestGraphDriver(t)
|
||||||
|
fms, err := NewFSMetadataStore(td)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ls, err := NewStore(fms, graph)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ls, func() {
|
||||||
|
graphcleanup()
|
||||||
|
os.RemoveAll(td)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type layerInit func(root string) error
|
||||||
|
|
||||||
|
func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) {
|
||||||
|
containerID := stringid.GenerateRandomID()
|
||||||
|
mount, err := ls.Mount(containerID, parent, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := mount.Path()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := layerFunc(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := mount.TarStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err := ls.Register(ts, parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.Unmount(containerID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.DeleteMount(containerID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileApplier interface {
|
||||||
|
ApplyFile(root string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type testFile struct {
|
||||||
|
name string
|
||||||
|
content []byte
|
||||||
|
permission os.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestFile(name string, content []byte, perm os.FileMode) FileApplier {
|
||||||
|
return &testFile{
|
||||||
|
name: name,
|
||||||
|
content: content,
|
||||||
|
permission: perm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *testFile) ApplyFile(root string) error {
|
||||||
|
fullPath := filepath.Join(root, tf.name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Check if already exists
|
||||||
|
if stat, err := os.Stat(fullPath); err == nil && stat.Mode().Perm() != tf.permission {
|
||||||
|
if err := os.Chmod(fullPath, tf.permission); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(fullPath, tf.content, tf.permission); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initWithFiles(files ...FileApplier) layerInit {
|
||||||
|
return func(root string) error {
|
||||||
|
for _, f := range files {
|
||||||
|
if err := f.ApplyFile(root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedLayer(l Layer) *roLayer {
|
||||||
|
if rl, ok := l.(*referencedCacheLayer); ok {
|
||||||
|
return rl.roLayer
|
||||||
|
}
|
||||||
|
return l.(*roLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMetadata(layers ...Layer) []Metadata {
|
||||||
|
metadata := make([]Metadata, len(layers))
|
||||||
|
for i := range layers {
|
||||||
|
size, err := layers[i].Size()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata[i].ChainID = layers[i].ChainID()
|
||||||
|
metadata[i].DiffID = layers[i].DiffID()
|
||||||
|
metadata[i].Size = size
|
||||||
|
metadata[i].DiffSize = getCachedLayer(layers[i]).size
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertMetadata(t *testing.T, metadata, expectedMetadata []Metadata) {
|
||||||
|
if len(metadata) != len(expectedMetadata) {
|
||||||
|
t.Fatalf("Unexpected number of deletes %d, expected %d", len(metadata), len(expectedMetadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range metadata {
|
||||||
|
if metadata[i] != expectedMetadata[i] {
|
||||||
|
t.Errorf("Unexpected metadata\n\tExpected: %#v\n\tActual: %#v", expectedMetadata[i], metadata[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.Failed() {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseAndCheckDeleted(t *testing.T, ls Store, layer Layer, removed ...Layer) {
|
||||||
|
layerCount := len(ls.(*layerStore).layerMap)
|
||||||
|
expectedMetadata := createMetadata(removed...)
|
||||||
|
metadata, err := ls.Release(layer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadata(t, metadata, expectedMetadata)
|
||||||
|
|
||||||
|
if expected := layerCount - len(removed); len(ls.(*layerStore).layerMap) != expected {
|
||||||
|
t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheID(l Layer) string {
|
||||||
|
return getCachedLayer(l).cacheID
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLayerEqual(t *testing.T, l1, l2 Layer) {
|
||||||
|
if l1.ChainID() != l2.ChainID() {
|
||||||
|
t.Fatalf("Mismatched ID: %s vs %s", l1.ChainID(), l2.ChainID())
|
||||||
|
}
|
||||||
|
if l1.DiffID() != l2.DiffID() {
|
||||||
|
t.Fatalf("Mismatched DiffID: %s vs %s", l1.DiffID(), l2.DiffID())
|
||||||
|
}
|
||||||
|
|
||||||
|
size1, err := l1.Size()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size2, err := l2.Size()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if size1 != size2 {
|
||||||
|
t.Fatalf("Mismatched size: %d vs %d", size1, size2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheID(l1) != cacheID(l2) {
|
||||||
|
t.Fatalf("Mismatched cache id: %s vs %s", cacheID(l1), cacheID(l2))
|
||||||
|
}
|
||||||
|
|
||||||
|
p1 := l1.Parent()
|
||||||
|
p2 := l2.Parent()
|
||||||
|
if p1 != nil && p2 != nil {
|
||||||
|
assertLayerEqual(t, p1, p2)
|
||||||
|
} else if p1 != nil || p2 != nil {
|
||||||
|
t.Fatalf("Mismatched parents: %v vs %v", p1, p2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountAndRegister(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
li := initWithFiles(newTestFile("testfile.txt", []byte("some test data"), 0644))
|
||||||
|
layer, err := createLayer(ls, "", li)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size, _ := layer.Size()
|
||||||
|
t.Logf("Layer size: %d", size)
|
||||||
|
|
||||||
|
mount2, err := ls.Mount("new-test-mount", layer.ChainID(), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path2, err := mount2.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(filepath.Join(path2, "testfile.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := "some test data"; string(b) != expected {
|
||||||
|
t.Fatalf("Wrong file data, expected %q, got %q", expected, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.Unmount("new-test-mount"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.DeleteMount("new-test-mount"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayerRelease(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer3a, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3a file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer3b, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3b file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Layer1: %s", layer1.ChainID())
|
||||||
|
t.Logf("Layer2: %s", layer2.ChainID())
|
||||||
|
t.Logf("Layer3a: %s", layer3a.ChainID())
|
||||||
|
t.Logf("Layer3b: %s", layer3b.ChainID())
|
||||||
|
|
||||||
|
if expected := 4; len(ls.(*layerStore).layerMap) != expected {
|
||||||
|
t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAndCheckDeleted(t, ls, layer3b, layer3b)
|
||||||
|
releaseAndCheckDeleted(t, ls, layer3a, layer3a, layer2, layer1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreRestore(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer3, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3 file"), 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ls.Mount("some-mount_name", layer3.ChainID(), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := m.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(path, "testfile.txt"), []byte("nothing here"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.Unmount("some-mount_name"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ls2, err := NewStore(ls.(*layerStore).store, ls.(*layerStore).driver)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer3b, err := ls2.Get(layer3.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLayerEqual(t, layer3b, layer3)
|
||||||
|
|
||||||
|
// Mount again with same name, should already be loaded
|
||||||
|
m2, err := ls2.Mount("some-mount_name", layer3b.ChainID(), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path2, err := m2.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(filepath.Join(path2, "testfile.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if expected := "nothing here"; string(b) != expected {
|
||||||
|
t.Fatalf("Unexpected content %q, expected %q", string(b), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls2.Unmount("some-mount_name"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata, err := ls2.DeleteMount("some-mount_name"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(metadata) != 0 {
|
||||||
|
t.Fatalf("Unexpectedly deleted layers: %#v", metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAndCheckDeleted(t, ls2, layer3b, layer3, layer2, layer1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTarStreamStability(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
files1 := []FileApplier{
|
||||||
|
newTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
|
||||||
|
newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
|
||||||
|
}
|
||||||
|
addedFile := newTestFile("/etc/shadow", []byte("root:::::::"), 0644)
|
||||||
|
files2 := []FileApplier{
|
||||||
|
newTestFile("/etc/hosts", []byte("mydomain 10.0.0.2"), 0644),
|
||||||
|
newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0664),
|
||||||
|
newTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
tar1, err := tarFromFiles(files1...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tar2, err := tarFromFiles(files2...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1, err := ls.Register(bytes.NewReader(tar1), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hack layer to add file
|
||||||
|
p, err := ls.(*layerStore).driver.Get(layer1.(*referencedCacheLayer).cacheID, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addedFile.ApplyFile(p); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.(*layerStore).driver.Put(layer1.(*referencedCacheLayer).cacheID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2, err := ls.Register(bytes.NewReader(tar2), layer1.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id1 := layer1.ChainID()
|
||||||
|
t.Logf("Layer 1: %s", layer1.ChainID())
|
||||||
|
t.Logf("Layer 2: %s", layer2.ChainID())
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLayerDiff(t, tar2, layer2)
|
||||||
|
|
||||||
|
layer1b, err := ls.Get(id1)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Content of layer map: %#v", ls.(*layerStore).layerMap)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLayerDiff(t, tar1, layer1b)
|
||||||
|
|
||||||
|
if _, err := ls.Release(layer1b); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLayerDiff(t *testing.T, expected []byte, layer Layer) {
|
||||||
|
expectedDigest, err := digest.FromBytes(expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if digest.Digest(layer.DiffID()) != expectedDigest {
|
||||||
|
t.Fatalf("Mismatched diff id for %s, got %s, expected %s", layer.ChainID(), layer.DiffID(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := layer.TarStream()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, err := ioutil.ReadAll(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actual) != len(expected) {
|
||||||
|
logByteDiff(t, actual, expected)
|
||||||
|
t.Fatalf("Mismatched tar stream size for %s, got %d, expected %d", layer.ChainID(), len(actual), len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
actualDigest, err := digest.FromBytes(actual)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualDigest != expectedDigest {
|
||||||
|
logByteDiff(t, actual, expected)
|
||||||
|
t.Fatalf("Wrong digest of tar stream, got %s, expected %s", actualDigest, expectedDigest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxByteLog = 4 * 1024
|
||||||
|
|
||||||
|
func logByteDiff(t *testing.T, actual, expected []byte) {
|
||||||
|
d1, d2 := byteDiff(actual, expected)
|
||||||
|
if len(d1) == 0 && len(d2) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := len(actual) - len(d1)
|
||||||
|
if len(d1) > maxByteLog || len(d2) > maxByteLog {
|
||||||
|
t.Logf("Byte diff after %d matching bytes", prefix)
|
||||||
|
} else {
|
||||||
|
t.Logf("Byte diff after %d matching bytes\nActual bytes after prefix:\n%x\nExpected bytes after prefix:\n%x", prefix, d1, d2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// byteDiff returns the differing bytes after the matching prefix
|
||||||
|
func byteDiff(b1, b2 []byte) ([]byte, []byte) {
|
||||||
|
i := 0
|
||||||
|
for i < len(b1) && i < len(b2) {
|
||||||
|
if b1[i] != b2[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return b1[i:], b2[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarFromFiles(files ...FileApplier) ([]byte, error) {
|
||||||
|
td, err := ioutil.TempDir("", "tar-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if err := f.ApplyFile(td); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := archive.Tar(td, archive.Uncompressed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
if _, err := io.Copy(buf, r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertReferences asserts that all the references are to the same
|
||||||
|
// image and represent the full set of references to that image.
|
||||||
|
func assertReferences(t *testing.T, references ...Layer) {
|
||||||
|
if len(references) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
base := references[0].(*referencedCacheLayer).roLayer
|
||||||
|
seenReferences := map[Layer]struct{}{
|
||||||
|
references[0]: {},
|
||||||
|
}
|
||||||
|
for i := 1; i < len(references); i++ {
|
||||||
|
other := references[i].(*referencedCacheLayer).roLayer
|
||||||
|
if base != other {
|
||||||
|
t.Fatalf("Unexpected referenced cache layer %s, expecting %s", other.ChainID(), base.ChainID())
|
||||||
|
}
|
||||||
|
if _, ok := base.references[references[i]]; !ok {
|
||||||
|
t.Fatalf("Reference not part of reference list: %v", references[i])
|
||||||
|
}
|
||||||
|
if _, ok := seenReferences[references[i]]; ok {
|
||||||
|
t.Fatalf("Duplicated reference %v", references[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rc := len(base.references); rc != len(references) {
|
||||||
|
t.Fatalf("Unexpected number of references %d, expecting %d", rc, len(references))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterExistingLayer(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
baseFiles := []FileApplier{
|
||||||
|
newTestFile("/etc/profile", []byte("# Base configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
layerFiles := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Root configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
li := initWithFiles(baseFiles...)
|
||||||
|
layer1, err := createLayer(ls, "", li)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tar1, err := tarFromFiles(layerFiles...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2a, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2b, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReferences(t, layer2a, layer2b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func graphDiffSize(ls Store, l Layer) (int64, error) {
|
||||||
|
cl := getCachedLayer(l)
|
||||||
|
var parent string
|
||||||
|
if cl.parent != nil {
|
||||||
|
parent = cl.parent.cacheID
|
||||||
|
}
|
||||||
|
return ls.(*layerStore).driver.DiffSize(cl.cacheID, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayerSize(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
content1 := []byte("Base contents")
|
||||||
|
content2 := []byte("Added contents")
|
||||||
|
|
||||||
|
layer1, err := createLayer(ls, "", initWithFiles(newTestFile("file1", content1, 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("file2", content2, 0644)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1DiffSize, err := graphDiffSize(ls, layer1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(layer1DiffSize) != len(content1) {
|
||||||
|
t.Fatalf("Unexpected diff size %d, expected %d", layer1DiffSize, len(content1))
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1Size, err := layer1.Size()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := len(content1); int(layer1Size) != expected {
|
||||||
|
t.Fatalf("Unexpected size %d, expected %d", layer1Size, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2DiffSize, err := graphDiffSize(ls, layer2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(layer2DiffSize) != len(content2) {
|
||||||
|
t.Fatalf("Unexpected diff size %d, expected %d", layer2DiffSize, len(content2))
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2Size, err := layer2.Size()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := len(content1) + len(content2); int(layer2Size) != expected {
|
||||||
|
t.Fatalf("Unexpected size %d, expected %d", layer2Size, expected)
|
||||||
|
}
|
||||||
|
}
|
109
layer/layer_windows.go
Normal file
109
layer/layer_windows.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLayerPath returns the path to a layer
|
||||||
|
func GetLayerPath(s Store, layer ChainID) (string, error) {
|
||||||
|
ls, ok := s.(*layerStore)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("unsupported layer store")
|
||||||
|
}
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
|
||||||
|
rl, ok := ls.layerMap[layer]
|
||||||
|
if !ok {
|
||||||
|
return "", ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := ls.driver.Get(rl.cacheID, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.driver.Put(rl.cacheID); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RWLayerMetadata returns the graph metadata for the provided
|
||||||
|
// mount name.
|
||||||
|
func RWLayerMetadata(s Store, name string) (map[string]string, error) {
|
||||||
|
ls, ok := s.(*layerStore)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("unsupported layer store")
|
||||||
|
}
|
||||||
|
ls.mountL.Lock()
|
||||||
|
defer ls.mountL.Unlock()
|
||||||
|
|
||||||
|
ml, ok := ls.mounts[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("mount does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ls.driver.GetMetadata(ml.mountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) RegisterDiffID(graphID string, size int64) (Layer, error) {
|
||||||
|
var err error // this is used for cleanup in existingLayer case
|
||||||
|
diffID, err := digest.FromBytes([]byte(graphID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new roLayer
|
||||||
|
layer := &roLayer{
|
||||||
|
cacheID: graphID,
|
||||||
|
diffID: DiffID(diffID),
|
||||||
|
referenceCount: 1,
|
||||||
|
layerStore: ls,
|
||||||
|
references: map[Layer]struct{}{},
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := ls.store.StartTransaction()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
if err := tx.Cancel(); err != nil {
|
||||||
|
logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
layer.chainID = createChainIDFromParent("", layer.diffID)
|
||||||
|
|
||||||
|
if !ls.driver.Exists(layer.cacheID) {
|
||||||
|
return nil, fmt.Errorf("layer %q is unknown to driver", layer.cacheID)
|
||||||
|
}
|
||||||
|
if err = storeLayer(tx, layer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
|
||||||
|
if existingLayer := ls.getAndRetainLayer(layer.chainID); existingLayer != nil {
|
||||||
|
// Set error for cleanup, but do not return
|
||||||
|
err = errors.New("layer already exists")
|
||||||
|
return existingLayer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(layer.chainID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerMap[layer.chainID] = layer
|
||||||
|
|
||||||
|
return layer.getReference(), nil
|
||||||
|
}
|
251
layer/migration.go
Normal file
251
layer/migration.go
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
"github.com/vbatts/tar-split/tar/asm"
|
||||||
|
"github.com/vbatts/tar-split/tar/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ls *layerStore) MountByGraphID(name string, graphID string, parent ChainID) (l RWLayer, err error) {
|
||||||
|
ls.mountL.Lock()
|
||||||
|
defer ls.mountL.Unlock()
|
||||||
|
m, ok := ls.mounts[name]
|
||||||
|
if ok {
|
||||||
|
if m.parent.chainID != parent {
|
||||||
|
return nil, errors.New("name conflict, mismatched parent")
|
||||||
|
}
|
||||||
|
if m.mountID != graphID {
|
||||||
|
return nil, errors.New("mount already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ls.driver.Exists(graphID) {
|
||||||
|
return nil, errors.New("graph ID does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var p *roLayer
|
||||||
|
if string(parent) != "" {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
p = ls.getAndRetainLayer(parent)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
if p == nil {
|
||||||
|
return nil, ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release parent chain if error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
ls.releaseLayer(p)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Ensure graphID has correct parent
|
||||||
|
|
||||||
|
m = &mountedLayer{
|
||||||
|
name: name,
|
||||||
|
parent: p,
|
||||||
|
mountID: graphID,
|
||||||
|
layerStore: ls,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing init layer
|
||||||
|
initID := fmt.Sprintf("%s-init", graphID)
|
||||||
|
if ls.driver.Exists(initID) {
|
||||||
|
m.initID = initID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ls.saveMount(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: provide a mount label
|
||||||
|
if err = ls.mount(m, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) migrateLayer(tx MetadataTransaction, tarDataFile string, layer *roLayer) error {
|
||||||
|
var ar io.Reader
|
||||||
|
var tdf *os.File
|
||||||
|
var err error
|
||||||
|
if tarDataFile != "" {
|
||||||
|
tdf, err = os.Open(tarDataFile)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tdf = nil
|
||||||
|
}
|
||||||
|
defer tdf.Close()
|
||||||
|
}
|
||||||
|
if tdf != nil {
|
||||||
|
tsw, err := tx.TarSplitWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tsw.Close()
|
||||||
|
|
||||||
|
uncompressed, err := gzip.NewReader(tdf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer uncompressed.Close()
|
||||||
|
|
||||||
|
tr := io.TeeReader(uncompressed, tsw)
|
||||||
|
trc := ioutils.NewReadCloserWrapper(tr, uncompressed.Close)
|
||||||
|
|
||||||
|
ar, err = ls.assembleTar(layer.cacheID, trc, &layer.size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var graphParent string
|
||||||
|
if layer.parent != nil {
|
||||||
|
graphParent = layer.parent.cacheID
|
||||||
|
}
|
||||||
|
archiver, err := ls.driver.Diff(layer.cacheID, graphParent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer archiver.Close()
|
||||||
|
|
||||||
|
tsw, err := tx.TarSplitWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metaPacker := storage.NewJSONPacker(tsw)
|
||||||
|
packerCounter := &packSizeCounter{metaPacker, &layer.size}
|
||||||
|
defer tsw.Close()
|
||||||
|
|
||||||
|
ar, err = asm.NewInputTarStream(archiver, packerCounter, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
digester := digest.Canonical.New()
|
||||||
|
_, err = io.Copy(digester.Hash(), ar)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.diffID = DiffID(digester.Digest())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *layerStore) RegisterByGraphID(graphID string, parent ChainID, tarDataFile string) (Layer, error) {
|
||||||
|
// err is used to hold the error which will always trigger
|
||||||
|
// cleanup of creates sources but may not be an error returned
|
||||||
|
// to the caller (already exists).
|
||||||
|
var err error
|
||||||
|
var p *roLayer
|
||||||
|
if string(parent) != "" {
|
||||||
|
p = ls.get(parent)
|
||||||
|
if p == nil {
|
||||||
|
return nil, ErrLayerDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release parent chain if error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
ls.layerL.Lock()
|
||||||
|
ls.releaseLayer(p)
|
||||||
|
ls.layerL.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new roLayer
|
||||||
|
layer := &roLayer{
|
||||||
|
parent: p,
|
||||||
|
cacheID: graphID,
|
||||||
|
referenceCount: 1,
|
||||||
|
layerStore: ls,
|
||||||
|
references: map[Layer]struct{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := ls.store.StartTransaction()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Cleaning up transaction after failed migration for %s: %v", graphID, err)
|
||||||
|
if err := tx.Cancel(); err != nil {
|
||||||
|
logrus.Errorf("Error canceling metadata transaction %q: %s", tx.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = ls.migrateLayer(tx, tarDataFile, layer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.chainID = createChainIDFromParent(parent, layer.diffID)
|
||||||
|
|
||||||
|
if err = storeLayer(tx, layer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerL.Lock()
|
||||||
|
defer ls.layerL.Unlock()
|
||||||
|
|
||||||
|
if existingLayer := ls.getAndRetainLayer(layer.chainID); existingLayer != nil {
|
||||||
|
// Set error for cleanup, but do not return
|
||||||
|
err = errors.New("layer already exists")
|
||||||
|
return existingLayer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(layer.chainID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.layerMap[layer.chainID] = layer
|
||||||
|
|
||||||
|
return layer.getReference(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type unpackSizeCounter struct {
|
||||||
|
unpacker storage.Unpacker
|
||||||
|
size *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unpackSizeCounter) Next() (*storage.Entry, error) {
|
||||||
|
e, err := u.unpacker.Next()
|
||||||
|
if err == nil && u.size != nil {
|
||||||
|
*u.size += e.Size
|
||||||
|
}
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type packSizeCounter struct {
|
||||||
|
packer storage.Packer
|
||||||
|
size *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *packSizeCounter) AddEntry(e storage.Entry) (int, error) {
|
||||||
|
n, err := p.packer.AddEntry(e)
|
||||||
|
if err == nil && p.size != nil {
|
||||||
|
*p.size += e.Size
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
385
layer/migration_test.go
Normal file
385
layer/migration_test.go
Normal file
|
@ -0,0 +1,385 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/daemon/graphdriver"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/vbatts/tar-split/tar/asm"
|
||||||
|
"github.com/vbatts/tar-split/tar/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTarSplitFile(name string, tarContent []byte) error {
|
||||||
|
f, err := os.OpenFile(name, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fz := gzip.NewWriter(f)
|
||||||
|
|
||||||
|
metaPacker := storage.NewJSONPacker(fz)
|
||||||
|
defer fz.Close()
|
||||||
|
|
||||||
|
rdr, err := asm.NewInputTarStream(bytes.NewReader(tarContent), metaPacker, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(ioutil.Discard, rdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayerMigration(t *testing.T) {
|
||||||
|
td, err := ioutil.TempDir("", "migration-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
|
layer1Files := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644),
|
||||||
|
newTestFile("/etc/profile", []byte("# Base configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2Files := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
tar1, err := tarFromFiles(layer1Files...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tar2, err := tarFromFiles(layer2Files...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph, err := newVFSGraphDriver(filepath.Join(td, "graphdriver-"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graphID1 := stringid.GenerateRandomID()
|
||||||
|
if err := graph.Create(graphID1, "", ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := graph.ApplyDiff(graphID1, "", archive.Reader(bytes.NewReader(tar1))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tf1 := filepath.Join(td, "tar1.json.gz")
|
||||||
|
if err := writeTarSplitFile(tf1, tar1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fms, err := NewFSMetadataStore(filepath.Join(td, "layers"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ls, err := NewStore(fms, graph)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1a, err := ls.(*layerStore).RegisterByGraphID(graphID1, "", tf1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1b, err := ls.Register(bytes.NewReader(tar1), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReferences(t, layer1a, layer1b)
|
||||||
|
|
||||||
|
// Attempt register, should be same
|
||||||
|
layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graphID2 := stringid.GenerateRandomID()
|
||||||
|
if err := graph.Create(graphID2, graphID1, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := graph.ApplyDiff(graphID2, graphID1, archive.Reader(bytes.NewReader(tar2))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tf2 := filepath.Join(td, "tar2.json.gz")
|
||||||
|
if err := writeTarSplitFile(tf2, tar2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2b, err := ls.(*layerStore).RegisterByGraphID(graphID2, layer1a.ChainID(), tf2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReferences(t, layer2a, layer2b)
|
||||||
|
|
||||||
|
if metadata, err := ls.Release(layer2a); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(metadata) > 0 {
|
||||||
|
t.Fatalf("Unexpected layer removal after first release: %#v", metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := ls.Release(layer2b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadata(t, metadata, createMetadata(layer2a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarFromFilesInGraph(graph graphdriver.Driver, graphID, parentID string, files ...FileApplier) ([]byte, error) {
|
||||||
|
t, err := tarFromFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := graph.Create(graphID, parentID, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := graph.ApplyDiff(graphID, parentID, archive.Reader(bytes.NewReader(t))); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ar, err := graph.Diff(graphID, parentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer ar.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(ar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayerMigrationNoTarsplit(t *testing.T) {
|
||||||
|
td, err := ioutil.TempDir("", "migration-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
|
layer1Files := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644),
|
||||||
|
newTestFile("/etc/profile", []byte("# Base configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2Files := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
graph, err := newVFSGraphDriver(filepath.Join(td, "graphdriver-"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
graphID1 := stringid.GenerateRandomID()
|
||||||
|
graphID2 := stringid.GenerateRandomID()
|
||||||
|
|
||||||
|
tar1, err := tarFromFilesInGraph(graph, graphID1, "", layer1Files...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tar2, err := tarFromFilesInGraph(graph, graphID2, graphID1, layer2Files...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fms, err := NewFSMetadataStore(filepath.Join(td, "layers"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ls, err := NewStore(fms, graph)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1a, err := ls.(*layerStore).RegisterByGraphID(graphID1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer1b, err := ls.Register(bytes.NewReader(tar1), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReferences(t, layer1a, layer1b)
|
||||||
|
|
||||||
|
// Attempt register, should be same
|
||||||
|
layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer2b, err := ls.(*layerStore).RegisterByGraphID(graphID2, layer1a.ChainID(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReferences(t, layer2a, layer2b)
|
||||||
|
|
||||||
|
if metadata, err := ls.Release(layer2a); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(metadata) > 0 {
|
||||||
|
t.Fatalf("Unexpected layer removal after first release: %#v", metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := ls.Release(layer2b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadata(t, metadata, createMetadata(layer2a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountMigration(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
baseFiles := []FileApplier{
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Boring configuration"), 0644),
|
||||||
|
newTestFile("/etc/profile", []byte("# Base configuration"), 0644),
|
||||||
|
}
|
||||||
|
initFiles := []FileApplier{
|
||||||
|
newTestFile("/etc/hosts", []byte{}, 0644),
|
||||||
|
newTestFile("/etc/resolv.conf", []byte{}, 0644),
|
||||||
|
}
|
||||||
|
mountFiles := []FileApplier{
|
||||||
|
newTestFile("/etc/hosts", []byte("localhost 127.0.0.1"), 0644),
|
||||||
|
newTestFile("/root/.bashrc", []byte("# Updated configuration"), 0644),
|
||||||
|
newTestFile("/root/testfile1.txt", []byte("nothing valuable"), 0644),
|
||||||
|
}
|
||||||
|
|
||||||
|
initTar, err := tarFromFiles(initFiles...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountTar, err := tarFromFiles(mountFiles...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := ls.(*layerStore).driver
|
||||||
|
|
||||||
|
layer1, err := createLayer(ls, "", initWithFiles(baseFiles...))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
graphID1 := layer1.(*referencedCacheLayer).cacheID
|
||||||
|
|
||||||
|
containerID := stringid.GenerateRandomID()
|
||||||
|
containerInit := fmt.Sprintf("%s-init", containerID)
|
||||||
|
|
||||||
|
if err := graph.Create(containerInit, graphID1, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := graph.ApplyDiff(containerInit, graphID1, archive.Reader(bytes.NewReader(initTar))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := graph.Create(containerID, containerInit, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := graph.ApplyDiff(containerID, containerInit, archive.Reader(bytes.NewReader(mountTar))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rwLayer1, err := ls.(*layerStore).MountByGraphID("migration-mount", containerID, layer1.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, err := ls.Changes("migration-mount")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := 5; len(changes) != expected {
|
||||||
|
t.Logf("Changes %#v", changes)
|
||||||
|
t.Fatalf("Wrong number of changes %d, expected %d", len(changes), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortChanges(changes)
|
||||||
|
|
||||||
|
assertChange(t, changes[0], archive.Change{
|
||||||
|
Path: "/etc",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[1], archive.Change{
|
||||||
|
Path: "/etc/hosts",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[2], archive.Change{
|
||||||
|
Path: "/root",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[3], archive.Change{
|
||||||
|
Path: "/root/.bashrc",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[4], archive.Change{
|
||||||
|
Path: "/root/testfile1.txt",
|
||||||
|
Kind: archive.ChangeAdd,
|
||||||
|
})
|
||||||
|
|
||||||
|
if expectedCount := 1; rwLayer1.(*mountedLayer).activityCount != expectedCount {
|
||||||
|
t.Fatalf("Wrong activity count %d, expected %d", rwLayer1.(*mountedLayer).activityCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
rwLayer2, err := ls.Mount("migration-mount", layer1.ChainID(), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rwLayer1 != rwLayer2 {
|
||||||
|
t.Fatalf("Wrong rwlayer %v, expected %v", rwLayer2, rwLayer1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedCount := 2; rwLayer2.(*mountedLayer).activityCount != expectedCount {
|
||||||
|
t.Fatalf("Wrong activity count %d, expected %d", rwLayer2.(*mountedLayer).activityCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata, err := ls.Release(layer1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(metadata) > 0 {
|
||||||
|
t.Fatalf("Expected no layers to be deleted, deleted %#v", metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.Unmount("migration-mount"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ls.DeleteMount("migration-mount"); err == nil {
|
||||||
|
t.Fatal("Expected error deleting active mount")
|
||||||
|
}
|
||||||
|
if err := ls.Unmount("migration-mount"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
metadata, err := ls.DeleteMount("migration-mount")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
t.Fatal("Expected base layer to be deleted when deleting mount")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadata(t, metadata, createMetadata(layer1))
|
||||||
|
}
|
217
layer/mount_test.go
Normal file
217
layer/mount_test.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountInit(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
basefile := newTestFile("testfile.txt", []byte("base data!"), 0644)
|
||||||
|
initfile := newTestFile("testfile.txt", []byte("init data!"), 0777)
|
||||||
|
|
||||||
|
li := initWithFiles(basefile)
|
||||||
|
layer, err := createLayer(ls, "", li)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountInit := func(root string) error {
|
||||||
|
return initfile.ApplyFile(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ls.Mount("fun-mount", layer.ChainID(), "", mountInit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := m.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filepath.Join(path, "testfile.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := "init data!"; string(b) != expected {
|
||||||
|
t.Fatalf("Unexpected test file contents %q, expected %q", string(b), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode().Perm() != 0777 {
|
||||||
|
t.Fatalf("Unexpected filemode %o, expecting %o", fi.Mode().Perm(), 0777)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountSize(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
content1 := []byte("Base contents")
|
||||||
|
content2 := []byte("Mutable contents")
|
||||||
|
contentInit := []byte("why am I excluded from the size ☹")
|
||||||
|
|
||||||
|
li := initWithFiles(newTestFile("file1", content1, 0644))
|
||||||
|
layer, err := createLayer(ls, "", li)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountInit := func(root string) error {
|
||||||
|
return newTestFile("file-init", contentInit, 0777).ApplyFile(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ls.Mount("mount-size", layer.ChainID(), "", mountInit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := m.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(path, "file2"), content2, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountSize, err := m.Size()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := len(content2); int(mountSize) != expected {
|
||||||
|
t.Fatalf("Unexpected mount size %d, expected %d", int(mountSize), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountChanges(t *testing.T) {
|
||||||
|
ls, cleanup := newTestStore(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
basefiles := []FileApplier{
|
||||||
|
newTestFile("testfile1.txt", []byte("base data!"), 0644),
|
||||||
|
newTestFile("testfile2.txt", []byte("base data!"), 0644),
|
||||||
|
newTestFile("testfile3.txt", []byte("base data!"), 0644),
|
||||||
|
}
|
||||||
|
initfile := newTestFile("testfile1.txt", []byte("init data!"), 0777)
|
||||||
|
|
||||||
|
li := initWithFiles(basefiles...)
|
||||||
|
layer, err := createLayer(ls, "", li)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountInit := func(root string) error {
|
||||||
|
return initfile.ApplyFile(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ls.Mount("mount-changes", layer.ChainID(), "", mountInit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := m.Path()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(filepath.Join(path, "testfile1.txt"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(path, "testfile1.txt"), []byte("mount data!"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(filepath.Join(path, "testfile2.txt")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(filepath.Join(path, "testfile3.txt"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(path, "testfile4.txt"), []byte("mount data!"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, err := ls.Changes("mount-changes")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected := 4; len(changes) != expected {
|
||||||
|
t.Fatalf("Wrong number of changes %d, expected %d", len(changes), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortChanges(changes)
|
||||||
|
|
||||||
|
assertChange(t, changes[0], archive.Change{
|
||||||
|
Path: "/testfile1.txt",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[1], archive.Change{
|
||||||
|
Path: "/testfile2.txt",
|
||||||
|
Kind: archive.ChangeDelete,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[2], archive.Change{
|
||||||
|
Path: "/testfile3.txt",
|
||||||
|
Kind: archive.ChangeModify,
|
||||||
|
})
|
||||||
|
assertChange(t, changes[3], archive.Change{
|
||||||
|
Path: "/testfile4.txt",
|
||||||
|
Kind: archive.ChangeAdd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertChange(t *testing.T, actual, expected archive.Change) {
|
||||||
|
if actual.Path != expected.Path {
|
||||||
|
t.Fatalf("Unexpected change path %s, expected %s", actual.Path, expected.Path)
|
||||||
|
}
|
||||||
|
if actual.Kind != expected.Kind {
|
||||||
|
t.Fatalf("Unexpected change type %s, expected %s", actual.Kind, expected.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortChanges(changes []archive.Change) {
|
||||||
|
cs := &changeSorter{
|
||||||
|
changes: changes,
|
||||||
|
}
|
||||||
|
sort.Sort(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
type changeSorter struct {
|
||||||
|
changes []archive.Change
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *changeSorter) Len() int {
|
||||||
|
return len(cs.changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *changeSorter) Swap(i, j int) {
|
||||||
|
cs.changes[i], cs.changes[j] = cs.changes[j], cs.changes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *changeSorter) Less(i, j int) bool {
|
||||||
|
return cs.changes[i].Path < cs.changes[j].Path
|
||||||
|
}
|
64
layer/mounted_layer.go
Normal file
64
layer/mounted_layer.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type mountedLayer struct {
|
||||||
|
name string
|
||||||
|
mountID string
|
||||||
|
initID string
|
||||||
|
parent *roLayer
|
||||||
|
path string
|
||||||
|
layerStore *layerStore
|
||||||
|
activityCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mountedLayer) cacheParent() string {
|
||||||
|
if ml.initID != "" {
|
||||||
|
return ml.initID
|
||||||
|
}
|
||||||
|
if ml.parent != nil {
|
||||||
|
return ml.parent.cacheID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mountedLayer) TarStream() (io.Reader, error) {
|
||||||
|
archiver, err := ml.layerStore.driver.Diff(ml.mountID, ml.cacheParent())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return autoClosingReader{archiver}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mountedLayer) Path() (string, error) {
|
||||||
|
if ml.path == "" {
|
||||||
|
return "", ErrNotMounted
|
||||||
|
}
|
||||||
|
return ml.path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mountedLayer) Parent() Layer {
|
||||||
|
if ml.parent != nil {
|
||||||
|
return ml.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a nil interface instead of an interface wrapping a nil
|
||||||
|
// pointer.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mountedLayer) Size() (int64, error) {
|
||||||
|
return ml.layerStore.driver.DiffSize(ml.mountID, ml.cacheParent())
|
||||||
|
}
|
||||||
|
|
||||||
|
type autoClosingReader struct {
|
||||||
|
source io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r autoClosingReader) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = r.source.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
r.source.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
110
layer/ro_layer.go
Normal file
110
layer/ro_layer.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type roLayer struct {
|
||||||
|
chainID ChainID
|
||||||
|
diffID DiffID
|
||||||
|
parent *roLayer
|
||||||
|
cacheID string
|
||||||
|
size int64
|
||||||
|
layerStore *layerStore
|
||||||
|
|
||||||
|
referenceCount int
|
||||||
|
references map[Layer]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) TarStream() (io.Reader, error) {
|
||||||
|
r, err := rl.layerStore.store.TarSplitReader(rl.chainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rl.layerStore.assembleTar(rl.cacheID, r, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) ChainID() ChainID {
|
||||||
|
return rl.chainID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) DiffID() DiffID {
|
||||||
|
return rl.diffID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) Parent() Layer {
|
||||||
|
if rl.parent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rl.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) Size() (size int64, err error) {
|
||||||
|
if rl.parent != nil {
|
||||||
|
size, err = rl.parent.Size()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size + rl.size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) DiffSize() (size int64, err error) {
|
||||||
|
return rl.size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) Metadata() (map[string]string, error) {
|
||||||
|
return rl.layerStore.driver.GetMetadata(rl.cacheID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type referencedCacheLayer struct {
|
||||||
|
*roLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) getReference() Layer {
|
||||||
|
ref := &referencedCacheLayer{
|
||||||
|
roLayer: rl,
|
||||||
|
}
|
||||||
|
rl.references[ref] = struct{}{}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) hasReference(ref Layer) bool {
|
||||||
|
_, ok := rl.references[ref]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) hasReferences() bool {
|
||||||
|
return len(rl.references) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) deleteReference(ref Layer) {
|
||||||
|
delete(rl.references, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *roLayer) depth() int {
|
||||||
|
if rl.parent == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return rl.parent.depth() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeLayer(tx MetadataTransaction, layer *roLayer) error {
|
||||||
|
if err := tx.SetDiffID(layer.diffID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.SetSize(layer.size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.SetCacheID(layer.cacheID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if layer.parent != nil {
|
||||||
|
if err := tx.SetParent(layer.parent.chainID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue