Initial commit
This commit is contained in:
184
vendor/github.com/hyperhq/hypercli/image/fs.go
generated
vendored
Normal file
184
vendor/github.com/hyperhq/hypercli/image/fs.go
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
// IDWalkFunc is function called by StoreBackend.Walk
|
||||
type IDWalkFunc func(id ID) error
|
||||
|
||||
// StoreBackend provides interface for image.Store persistence
|
||||
type StoreBackend interface {
|
||||
Walk(f IDWalkFunc) error
|
||||
Get(id ID) ([]byte, error)
|
||||
Set(data []byte) (ID, error)
|
||||
Delete(id ID) error
|
||||
SetMetadata(id ID, key string, data []byte) error
|
||||
GetMetadata(id ID, key string) ([]byte, error)
|
||||
DeleteMetadata(id ID, key string) error
|
||||
}
|
||||
|
||||
// fs implements StoreBackend using the filesystem.
|
||||
type fs struct {
|
||||
sync.RWMutex
|
||||
root string
|
||||
}
|
||||
|
||||
const (
|
||||
contentDirName = "content"
|
||||
metadataDirName = "metadata"
|
||||
)
|
||||
|
||||
// NewFSStoreBackend returns new filesystem based backend for image.Store
|
||||
func NewFSStoreBackend(root string) (StoreBackend, error) {
|
||||
return newFSStore(root)
|
||||
}
|
||||
|
||||
func newFSStore(root string) (*fs, error) {
|
||||
s := &fs{
|
||||
root: root,
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *fs) contentFile(id ID) string {
|
||||
dgst := digest.Digest(id)
|
||||
return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Hex())
|
||||
}
|
||||
|
||||
func (s *fs) metadataDir(id ID) string {
|
||||
dgst := digest.Digest(id)
|
||||
return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Hex())
|
||||
}
|
||||
|
||||
// Walk calls the supplied callback for each image ID in the storage backend.
|
||||
func (s *fs) Walk(f IDWalkFunc) error {
|
||||
// Only Canonical digest (sha256) is currently supported
|
||||
s.RLock()
|
||||
dir, err := ioutil.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
|
||||
s.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range dir {
|
||||
dgst := digest.NewDigestFromHex(string(digest.Canonical), v.Name())
|
||||
if err := dgst.Validate(); err != nil {
|
||||
logrus.Debugf("Skipping invalid digest %s: %s", dgst, err)
|
||||
continue
|
||||
}
|
||||
if err := f(ID(dgst)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the content stored under a given ID.
|
||||
func (s *fs) Get(id ID) ([]byte, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.get(id)
|
||||
}
|
||||
|
||||
func (s *fs) get(id ID) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(s.contentFile(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// todo: maybe optional
|
||||
if ID(digest.FromBytes(content)) != id {
|
||||
return nil, fmt.Errorf("failed to verify image: %v", id)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Set stores content under a given ID.
|
||||
func (s *fs) Set(data []byte) (ID, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", fmt.Errorf("Invalid empty data")
|
||||
}
|
||||
|
||||
id := ID(digest.FromBytes(data))
|
||||
filePath := s.contentFile(id)
|
||||
tempFilePath := s.contentFile(id) + ".tmp"
|
||||
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(tempFilePath, filePath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Delete removes content and metadata files associated with the ID.
|
||||
func (s *fs) Delete(id ID) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if err := os.RemoveAll(s.metadataDir(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(s.contentFile(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMetadata sets metadata for a given ID. It fails if there's no base file.
|
||||
func (s *fs) SetMetadata(id ID, key string, data []byte) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if _, err := s.get(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := filepath.Join(s.metadataDir(id))
|
||||
if err := os.MkdirAll(baseDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(s.metadataDir(id), key)
|
||||
tempFilePath := filePath + ".tmp"
|
||||
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempFilePath, filePath)
|
||||
}
|
||||
|
||||
// GetMetadata returns metadata for a given ID.
|
||||
func (s *fs) GetMetadata(id ID, key string) ([]byte, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
if _, err := s.get(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadFile(filepath.Join(s.metadataDir(id), key))
|
||||
}
|
||||
|
||||
// DeleteMetadata removes the metadata associated with an ID.
|
||||
func (s *fs) DeleteMetadata(id ID, key string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
return os.RemoveAll(filepath.Join(s.metadataDir(id), key))
|
||||
}
|
||||
384
vendor/github.com/hyperhq/hypercli/image/fs_test.go
generated
vendored
Normal file
384
vendor/github.com/hyperhq/hypercli/image/fs_test.go
generated
vendored
Normal file
@@ -0,0 +1,384 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
func TestFSGetSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testGetSet(t, fs)
|
||||
}
|
||||
|
||||
func TestFSGetInvalidData(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := fs.Set([]byte("foobar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dgst := digest.Digest(id)
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, string(dgst.Algorithm()), dgst.Hex()), []byte("foobar2"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fs.Get(id)
|
||||
if err == nil {
|
||||
t.Fatal("Expected get to fail after data modification.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSInvalidSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id := digest.FromBytes([]byte("foobar"))
|
||||
err = os.Mkdir(filepath.Join(tmpdir, contentDirName, string(id.Algorithm()), id.Hex()), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fs.Set([]byte("foobar"))
|
||||
if err == nil {
|
||||
t.Fatal("Expecting error from invalid filesystem data.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSInvalidRoot(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
tcases := []struct {
|
||||
root, invalidFile string
|
||||
}{
|
||||
{"root", "root"},
|
||||
{"root", "root/content"},
|
||||
{"root", "root/metadata"},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
root := filepath.Join(tmpdir, tc.root)
|
||||
filePath := filepath.Join(tmpdir, tc.invalidFile)
|
||||
err := os.MkdirAll(filepath.Dir(filePath), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
_, err = NewFSStoreBackend(root)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error from root %q and invlid file %q", tc.root, tc.invalidFile)
|
||||
}
|
||||
|
||||
os.RemoveAll(root)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testMetadataGetSet(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcases := []struct {
|
||||
id ID
|
||||
key string
|
||||
value []byte
|
||||
}{
|
||||
{id, "tkey", []byte("tval1")},
|
||||
{id, "tkey2", []byte("tval2")},
|
||||
{id2, "tkey", []byte("tval3")},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
err = store.SetMetadata(tc.id, tc.key, tc.value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := store.GetMetadata(tc.id, tc.key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(actual, tc.value) != 0 {
|
||||
t.Fatalf("Metadata expected %q, got %q", tc.value, actual)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = store.GetMetadata(id2, "tkey2")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for getting metadata for unknown key")
|
||||
}
|
||||
|
||||
id3 := digest.FromBytes([]byte("baz"))
|
||||
err = store.SetMetadata(ID(id3), "tkey", []byte("tval"))
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for setting metadata for unknown ID.")
|
||||
}
|
||||
|
||||
_, err = store.GetMetadata(ID(id3), "tkey")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for getting metadata for unknown ID.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSMetadataGetSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMetadataGetSet(t, fs)
|
||||
}
|
||||
|
||||
func TestFSDelete(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testDelete(t, fs)
|
||||
}
|
||||
|
||||
func TestFSWalker(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testWalker(t, fs)
|
||||
}
|
||||
|
||||
func TestFSInvalidWalker(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fooID, err := fs.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, "sha256/foobar"), []byte("foobar"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := 0
|
||||
err = fs.Walk(func(id ID) error {
|
||||
if id != fooID {
|
||||
t.Fatalf("Invalid walker ID %q, expected %q", id, fooID)
|
||||
}
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid data should not have caused walker error, got %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("Expected 1 walk initialization, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetSet(t *testing.T, store StoreBackend) {
|
||||
type tcase struct {
|
||||
input []byte
|
||||
expected ID
|
||||
}
|
||||
tcases := []tcase{
|
||||
{[]byte("foobar"), ID("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
|
||||
}
|
||||
|
||||
randomInput := make([]byte, 8*1024)
|
||||
_, err := rand.Read(randomInput)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// skipping use of digest pkg because its used by the implementation
|
||||
h := sha256.New()
|
||||
_, err = h.Write(randomInput)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tcases = append(tcases, tcase{
|
||||
input: randomInput,
|
||||
expected: ID("sha256:" + hex.EncodeToString(h.Sum(nil))),
|
||||
})
|
||||
|
||||
for _, tc := range tcases {
|
||||
id, err := store.Set([]byte(tc.input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != tc.expected {
|
||||
t.Fatalf("Expected ID %q, got %q", tc.expected, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, emptyData := range [][]byte{nil, {}} {
|
||||
_, err := store.Set(emptyData)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for nil input.")
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
data, err := store.Get(tc.expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(data, tc.input) != 0 {
|
||||
t.Fatalf("Expected data %q, got %q", tc.input, data)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range []ID{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
|
||||
_, err := store.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for ID %q.", key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testDelete(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Delete(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = store.Get(id)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected getting deleted item %q to fail", id)
|
||||
}
|
||||
_, err = store.Get(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Delete(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.Get(id2)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected getting deleted item %q to fail", id2)
|
||||
}
|
||||
}
|
||||
|
||||
func testWalker(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcases := make(map[ID]struct{})
|
||||
tcases[id] = struct{}{}
|
||||
tcases[id2] = struct{}{}
|
||||
n := 0
|
||||
err = store.Walk(func(id ID) error {
|
||||
delete(tcases, id)
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n != 2 {
|
||||
t.Fatalf("Expected 2 walk initializations, got %d", n)
|
||||
}
|
||||
if len(tcases) != 0 {
|
||||
t.Fatalf("Expected empty unwalked set, got %+v", tcases)
|
||||
}
|
||||
|
||||
// stop on error
|
||||
tcases = make(map[ID]struct{})
|
||||
tcases[id] = struct{}{}
|
||||
err = store.Walk(func(id ID) error {
|
||||
return errors.New("")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("Exected error from walker.")
|
||||
}
|
||||
}
|
||||
138
vendor/github.com/hyperhq/hypercli/image/image.go
generated
vendored
Normal file
138
vendor/github.com/hyperhq/hypercli/image/image.go
generated
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/engine-api/types/container"
|
||||
)
|
||||
|
||||
// ID is the content-addressable ID of an image.
|
||||
type ID digest.Digest
|
||||
|
||||
func (id ID) String() string {
|
||||
return digest.Digest(id).String()
|
||||
}
|
||||
|
||||
// V1Image stores the V1 image configuration.
|
||||
type V1Image struct {
|
||||
// ID a unique 64 character identifier of the image
|
||||
ID string `json:"id,omitempty"`
|
||||
// Parent id of the image
|
||||
Parent string `json:"parent,omitempty"`
|
||||
// Comment user added comment
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Created timestamp when image was created
|
||||
Created time.Time `json:"created"`
|
||||
// Container is the id of the container used to commit
|
||||
Container string `json:"container,omitempty"`
|
||||
// ContainerConfig is the configuration of the container that is committed into the image
|
||||
ContainerConfig container.Config `json:"container_config,omitempty"`
|
||||
// DockerVersion specifies version on which image is built
|
||||
DockerVersion string `json:"docker_version,omitempty"`
|
||||
// Author of the image
|
||||
Author string `json:"author,omitempty"`
|
||||
// Config is the configuration of the container received from the client
|
||||
Config *container.Config `json:"config,omitempty"`
|
||||
// Architecture is the hardware that the image is build and runs on
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
// OS is the operating system used to build and run the image
|
||||
OS string `json:"os,omitempty"`
|
||||
// Size is the total size of the image including all layers it is composed of
|
||||
Size int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Image stores the image configuration
|
||||
type Image struct {
|
||||
V1Image
|
||||
Parent ID `json:"parent,omitempty"`
|
||||
RootFS *RootFS `json:"rootfs,omitempty"`
|
||||
History []History `json:"history,omitempty"`
|
||||
|
||||
// rawJSON caches the immutable JSON associated with this image.
|
||||
rawJSON []byte
|
||||
|
||||
// computedID is the ID computed from the hash of the image config.
|
||||
// Not to be confused with the legacy V1 ID in V1Image.
|
||||
computedID ID
|
||||
}
|
||||
|
||||
// RawJSON returns the immutable JSON associated with the image.
|
||||
func (img *Image) RawJSON() []byte {
|
||||
return img.rawJSON
|
||||
}
|
||||
|
||||
// ID returns the image's content-addressable ID.
|
||||
func (img *Image) ID() ID {
|
||||
return img.computedID
|
||||
}
|
||||
|
||||
// ImageID stringizes ID.
|
||||
func (img *Image) ImageID() string {
|
||||
return string(img.ID())
|
||||
}
|
||||
|
||||
// RunConfig returns the image's container config.
|
||||
func (img *Image) RunConfig() *container.Config {
|
||||
return img.Config
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
|
||||
// that JSON that's been manipulated by a push/pull cycle with a legacy
|
||||
// registry won't end up with a different key order.
|
||||
func (img *Image) MarshalJSON() ([]byte, error) {
|
||||
type MarshalImage Image
|
||||
|
||||
pass1, err := json.Marshal(MarshalImage(*img))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c map[string]*json.RawMessage
|
||||
if err := json.Unmarshal(pass1, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// History stores build commands that were used to create an image
|
||||
type History struct {
|
||||
// Created timestamp for build point
|
||||
Created time.Time `json:"created"`
|
||||
// Author of the build point
|
||||
Author string `json:"author,omitempty"`
|
||||
// CreatedBy keeps the Dockerfile command used while building image.
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
// Comment is custom message set by the user when creating the image.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// EmptyLayer is set to true if this history item did not generate a
|
||||
// layer. Otherwise, the history item is associated with the next
|
||||
// layer in the RootFS section.
|
||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
||||
}
|
||||
|
||||
// Exporter provides interface for exporting and importing images
|
||||
type Exporter interface {
|
||||
Load(io.ReadCloser, io.Writer) error
|
||||
// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
|
||||
Save([]string, io.Writer) error
|
||||
}
|
||||
|
||||
// NewFromJSON creates an Image configuration from json.
|
||||
func NewFromJSON(src []byte) (*Image, error) {
|
||||
img := &Image{}
|
||||
|
||||
if err := json.Unmarshal(src, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if img.RootFS == nil {
|
||||
return nil, errors.New("Invalid image JSON, no RootFS key.")
|
||||
}
|
||||
|
||||
img.rawJSON = src
|
||||
|
||||
return img, nil
|
||||
}
|
||||
59
vendor/github.com/hyperhq/hypercli/image/image_test.go
generated
vendored
Normal file
59
vendor/github.com/hyperhq/hypercli/image/image_test.go
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleImageJSON = `{
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"config": {},
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": []
|
||||
}
|
||||
}`
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
img, err := NewFromJSON([]byte(sampleImageJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawJSON := img.RawJSON()
|
||||
if string(rawJSON) != sampleImageJSON {
|
||||
t.Fatalf("Raw JSON of config didn't match: expected %+v, got %v", sampleImageJSON, rawJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidJSON(t *testing.T) {
|
||||
_, err := NewFromJSON([]byte("{}"))
|
||||
if err == nil {
|
||||
t.Fatal("Expected JSON parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalKeyOrder(t *testing.T) {
|
||||
b, err := json.Marshal(&Image{
|
||||
V1Image: V1Image{
|
||||
Comment: "a",
|
||||
Author: "b",
|
||||
Architecture: "c",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedOrder := []string{"architecture", "author", "comment"}
|
||||
var indexes []int
|
||||
for _, k := range expectedOrder {
|
||||
indexes = append(indexes, strings.Index(string(b), k))
|
||||
}
|
||||
|
||||
if !sort.IntsAreSorted(indexes) {
|
||||
t.Fatal("invalid key order in JSON: ", string(b))
|
||||
}
|
||||
}
|
||||
8
vendor/github.com/hyperhq/hypercli/image/rootfs.go
generated
vendored
Normal file
8
vendor/github.com/hyperhq/hypercli/image/rootfs.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
package image
|
||||
|
||||
import "github.com/hyperhq/hypercli/layer"
|
||||
|
||||
// Append appends a new diffID to rootfs
|
||||
func (r *RootFS) Append(id layer.DiffID) {
|
||||
r.DiffIDs = append(r.DiffIDs, id)
|
||||
}
|
||||
23
vendor/github.com/hyperhq/hypercli/image/rootfs_unix.go
generated
vendored
Normal file
23
vendor/github.com/hyperhq/hypercli/image/rootfs_unix.go
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// +build !windows
|
||||
|
||||
package image
|
||||
|
||||
import "github.com/hyperhq/hypercli/layer"
|
||||
|
||||
// RootFS describes images root filesystem
|
||||
// This is currently a placeholder that only supports layers. In the future
|
||||
// this can be made into a interface that supports different implementations.
|
||||
type RootFS struct {
|
||||
Type string `json:"type"`
|
||||
DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
|
||||
}
|
||||
|
||||
// ChainID returns the ChainID for the top layer in RootFS.
|
||||
func (r *RootFS) ChainID() layer.ChainID {
|
||||
return layer.CreateChainID(r.DiffIDs)
|
||||
}
|
||||
|
||||
// NewRootFS returns empty RootFS struct
|
||||
func NewRootFS() *RootFS {
|
||||
return &RootFS{Type: "layers"}
|
||||
}
|
||||
37
vendor/github.com/hyperhq/hypercli/image/rootfs_windows.go
generated
vendored
Normal file
37
vendor/github.com/hyperhq/hypercli/image/rootfs_windows.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// +build windows
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
)
|
||||
|
||||
// RootFS describes images root filesystem
|
||||
// This is currently a placeholder that only supports layers. In the future
|
||||
// this can be made into a interface that supports different implementations.
|
||||
type RootFS struct {
|
||||
Type string `json:"type"`
|
||||
DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
|
||||
BaseLayer string `json:"base_layer,omitempty"`
|
||||
}
|
||||
|
||||
// BaseLayerID returns the 64 byte hex ID for the baselayer name.
|
||||
func (r *RootFS) BaseLayerID() string {
|
||||
baseID := sha512.Sum384([]byte(r.BaseLayer))
|
||||
return fmt.Sprintf("%x", baseID[:32])
|
||||
}
|
||||
|
||||
// ChainID returns the ChainID for the top layer in RootFS.
|
||||
func (r *RootFS) ChainID() layer.ChainID {
|
||||
baseDiffID := digest.FromBytes([]byte(r.BaseLayerID()))
|
||||
return layer.CreateChainID(append([]layer.DiffID{layer.DiffID(baseDiffID)}, r.DiffIDs...))
|
||||
}
|
||||
|
||||
// NewRootFS returns empty RootFS struct
|
||||
func NewRootFS() *RootFS {
|
||||
return &RootFS{Type: "layers+base"}
|
||||
}
|
||||
573
vendor/github.com/hyperhq/hypercli/image/spec/v1.md
generated
vendored
Normal file
573
vendor/github.com/hyperhq/hypercli/image/spec/v1.md
generated
vendored
Normal file
@@ -0,0 +1,573 @@
|
||||
# Docker Image Specification v1.0.0
|
||||
|
||||
An *Image* is an ordered collection of root filesystem changes and the
|
||||
corresponding execution parameters for use within a container runtime. This
|
||||
specification outlines the format of these filesystem changes and corresponding
|
||||
parameters and describes how to create and use them for use with a container
|
||||
runtime and execution tool.
|
||||
|
||||
## Terminology
|
||||
|
||||
This specification uses the following terms:
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
Layer
|
||||
</dt>
|
||||
<dd>
|
||||
Images are composed of <i>layers</i>. <i>Image layer</i> is a general
|
||||
term which may be used to refer to one or both of the following:
|
||||
|
||||
<ol>
|
||||
<li>The metadata for the layer, described in the JSON format.</li>
|
||||
<li>The filesystem changes described by a layer.</li>
|
||||
</ol>
|
||||
|
||||
To refer to the former you may use the term <i>Layer JSON</i> or
|
||||
<i>Layer Metadata</i>. To refer to the latter you may use the term
|
||||
<i>Image Filesystem Changeset</i> or <i>Image Diff</i>.
|
||||
</dd>
|
||||
<dt>
|
||||
Image JSON
|
||||
</dt>
|
||||
<dd>
|
||||
Each layer has an associated JSON structure which describes some
|
||||
basic information about the image such as date created, author, and the
|
||||
ID of its parent image as well as execution/runtime configuration like
|
||||
its entry point, default arguments, CPU/memory shares, networking, and
|
||||
volumes.
|
||||
</dd>
|
||||
<dt>
|
||||
Image Filesystem Changeset
|
||||
</dt>
|
||||
<dd>
|
||||
Each layer has an archive of the files which have been added, changed,
|
||||
or deleted relative to its parent layer. Using a layer-based or union
|
||||
filesystem such as AUFS, or by computing the diff from filesystem
|
||||
snapshots, the filesystem changeset can be used to present a series of
|
||||
image layers as if they were one cohesive filesystem.
|
||||
</dd>
|
||||
<dt>
|
||||
Image ID <a name="id_desc"></a>
|
||||
</dt>
|
||||
<dd>
|
||||
Each layer is given an ID upon its creation. It is
|
||||
represented as a hexadecimal encoding of 256 bits, e.g.,
|
||||
<code>a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9</code>.
|
||||
Image IDs should be sufficiently random so as to be globally unique.
|
||||
32 bytes read from <code>/dev/urandom</code> is sufficient for all
|
||||
practical purposes. Alternatively, an image ID may be derived as a
|
||||
cryptographic hash of image contents as the result is considered
|
||||
indistinguishable from random. The choice is left up to implementors.
|
||||
</dd>
|
||||
<dt>
|
||||
Image Parent
|
||||
</dt>
|
||||
<dd>
|
||||
Most layer metadata structs contain a <code>parent</code> field which
|
||||
refers to the Image from which another directly descends. An image
|
||||
contains a separate JSON metadata file and set of changes relative to
|
||||
the filesystem of its parent image. <i>Image Ancestor</i> and
|
||||
<i>Image Descendant</i> are also common terms.
|
||||
</dd>
|
||||
<dt>
|
||||
Image Checksum
|
||||
</dt>
|
||||
<dd>
|
||||
Layer metadata structs contain a cryptographic hash of the contents of
|
||||
the layer's filesystem changeset. Though the set of changes exists as a
|
||||
simple Tar archive, two archives with identical filenames and content
|
||||
will have different SHA digests if the last-access or last-modified
|
||||
times of any entries differ. For this reason, image checksums are
|
||||
generated using the TarSum algorithm which produces a cryptographic
|
||||
hash of file contents and selected headers only. Details of this
|
||||
algorithm are described in the separate <a href="https://github.com/docker/docker/blob/master/pkg/tarsum/tarsum_spec.md">TarSum specification</a>.
|
||||
</dd>
|
||||
<dt>
|
||||
Tag
|
||||
</dt>
|
||||
<dd>
|
||||
A tag serves to map a descriptive, user-given name to any single image
|
||||
ID. An image name suffix (the name component after <code>:</code>) is
|
||||
often referred to as a tag as well, though it strictly refers to the
|
||||
full name of an image. Acceptable values for a tag suffix are
|
||||
implementation specific, but they SHOULD be limited to the set of
|
||||
alphanumeric characters <code>[a-zA-z0-9]</code>, punctuation
|
||||
characters <code>[._-]</code>, and MUST NOT contain a <code>:</code>
|
||||
character.
|
||||
</dd>
|
||||
<dt>
|
||||
Repository
|
||||
</dt>
|
||||
<dd>
|
||||
A collection of tags grouped under a common prefix (the name component
|
||||
before <code>:</code>). For example, in an image tagged with the name
|
||||
<code>my-app:3.1.4</code>, <code>my-app</code> is the <i>Repository</i>
|
||||
component of the name. Acceptable values for repository name are
|
||||
implementation specific, but they SHOULD be limited to the set of
|
||||
alphanumeric characters <code>[a-zA-z0-9]</code>, and punctuation
|
||||
characters <code>[._-]</code>, however it MAY contain additional
|
||||
<code>/</code> and <code>:</code> characters for organizational
|
||||
purposes, with the last <code>:</code> character being interpreted
|
||||
dividing the repository component of the name from the tag suffix
|
||||
component.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
## Image JSON Description
|
||||
|
||||
Here is an example image JSON file:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9",
|
||||
"parent": "c6e3cedcda2e3982a1a6760e178355e8e65f7b80e4e5248743fa3549d284e024",
|
||||
"checksum": "tarsum.v1+sha256:e58fcf7418d2390dec8e8fb69d88c06ec07039d651fedc3aa72af9972e7d046b",
|
||||
"created": "2014-10-13T21:19:18.674353812Z",
|
||||
"author": "Alyssa P. Hacker <alyspdev@example.com>",
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"Size": 271828,
|
||||
"config": {
|
||||
"User": "alice",
|
||||
"Memory": 2048,
|
||||
"MemorySwap": 4096,
|
||||
"CpuShares": 8,
|
||||
"ExposedPorts": {
|
||||
"8080/tcp": {}
|
||||
},
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"FOO=docker_is_a_really",
|
||||
"BAR=great_tool_you_know"
|
||||
],
|
||||
"Entrypoint": [
|
||||
"/bin/my-app-binary"
|
||||
],
|
||||
"Cmd": [
|
||||
"--foreground",
|
||||
"--config",
|
||||
"/etc/my-app.d/default.cfg"
|
||||
],
|
||||
"Volumes": {
|
||||
"/var/job-result-data": {},
|
||||
"/var/log/my-app-logs": {},
|
||||
},
|
||||
"WorkingDir": "/home/alice",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Image JSON Field Descriptions
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
id <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Randomly generated, 256-bit, hexadecimal encoded. Uniquely identifies
|
||||
the image.
|
||||
</dd>
|
||||
<dt>
|
||||
parent <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
ID of the parent image. If there is no parent image then this field
|
||||
should be omitted. A collection of images may share many of the same
|
||||
ancestor layers. This organizational structure is strictly a tree with
|
||||
any one layer having either no parent or a single parent and zero or
|
||||
more descendent layers. Cycles are not allowed and implementations
|
||||
should be careful to avoid creating them or iterating through a cycle
|
||||
indefinitely.
|
||||
</dd>
|
||||
<dt>
|
||||
created <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
ISO-8601 formatted combined date and time at which the image was
|
||||
created.
|
||||
</dd>
|
||||
<dt>
|
||||
author <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Gives the name and/or email address of the person or entity which
|
||||
created and is responsible for maintaining the image.
|
||||
</dd>
|
||||
<dt>
|
||||
architecture <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The CPU architecture which the binaries in this image are built to run
|
||||
on. Possible values include:
|
||||
<ul>
|
||||
<li>386</li>
|
||||
<li>amd64</li>
|
||||
<li>arm</li>
|
||||
</ul>
|
||||
More values may be supported in the future and any of these may or may
|
||||
not be supported by a given container runtime implementation.
|
||||
</dd>
|
||||
<dt>
|
||||
os <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The name of the operating system which the image is built to run on.
|
||||
Possible values include:
|
||||
<ul>
|
||||
<li>darwin</li>
|
||||
<li>freebsd</li>
|
||||
<li>linux</li>
|
||||
</ul>
|
||||
More values may be supported in the future and any of these may or may
|
||||
not be supported by a given container runtime implementation.
|
||||
</dd>
|
||||
<dt>
|
||||
checksum <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Image Checksum of the filesystem changeset associated with the image
|
||||
layer.
|
||||
</dd>
|
||||
<dt>
|
||||
Size <code>integer</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The size in bytes of the filesystem changeset associated with the image
|
||||
layer.
|
||||
</dd>
|
||||
<dt>
|
||||
config <code>struct</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The execution parameters which should be used as a base when running a
|
||||
container using the image. This field can be <code>null</code>, in
|
||||
which case any execution parameters should be specified at creation of
|
||||
the container.
|
||||
|
||||
<h4>Container RunConfig Field Descriptions</h4>
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
User <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
<p>The username or UID which the process in the container should
|
||||
run as. This acts as a default value to use when the value is
|
||||
not specified when creating a container.</p>
|
||||
|
||||
<p>All of the following are valid:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>user</code></li>
|
||||
<li><code>uid</code></li>
|
||||
<li><code>user:group</code></li>
|
||||
<li><code>uid:gid</code></li>
|
||||
<li><code>uid:group</code></li>
|
||||
<li><code>user:gid</code></li>
|
||||
</ul>
|
||||
|
||||
<p>If <code>group</code>/<code>gid</code> is not specified, the
|
||||
default group and supplementary groups of the given
|
||||
<code>user</code>/<code>uid</code> in <code>/etc/passwd</code>
|
||||
from the container are applied.</p>
|
||||
</dd>
|
||||
<dt>
|
||||
Memory <code>integer</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Memory limit (in bytes). This acts as a default value to use
|
||||
when the value is not specified when creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
MemorySwap <code>integer</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Total memory usage (memory + swap); set to <code>-1</code> to
|
||||
disable swap. This acts as a default value to use when the
|
||||
value is not specified when creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
CpuShares <code>integer</code>
|
||||
</dt>
|
||||
<dd>
|
||||
CPU shares (relative weight vs. other containers). This acts as
|
||||
a default value to use when the value is not specified when
|
||||
creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
ExposedPorts <code>struct</code>
|
||||
</dt>
|
||||
<dd>
|
||||
A set of ports to expose from a container running this image.
|
||||
This JSON structure value is unusual because it is a direct
|
||||
JSON serialization of the Go type
|
||||
<code>map[string]struct{}</code> and is represented in JSON as
|
||||
an object mapping its keys to an empty object. Here is an
|
||||
example:
|
||||
|
||||
<pre>{
|
||||
"8080": {},
|
||||
"53/udp": {},
|
||||
"2356/tcp": {}
|
||||
}</pre>
|
||||
|
||||
Its keys can be in the format of:
|
||||
<ul>
|
||||
<li>
|
||||
<code>"port/tcp"</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>"port/udp"</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>"port"</code>
|
||||
</li>
|
||||
</ul>
|
||||
with the default protocol being <code>"tcp"</code> if not
|
||||
specified.
|
||||
|
||||
These values act as defaults and are merged with any specified
|
||||
when creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
Env <code>array of strings</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Entries are in the format of <code>VARNAME="var value"</code>.
|
||||
These values act as defaults and are merged with any specified
|
||||
when creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
Entrypoint <code>array of strings</code>
|
||||
</dt>
|
||||
<dd>
|
||||
A list of arguments to use as the command to execute when the
|
||||
container starts. This value acts as a default and is replaced
|
||||
by an entrypoint specified when creating a container.
|
||||
</dd>
|
||||
<dt>
|
||||
Cmd <code>array of strings</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Default arguments to the entry point of the container. These
|
||||
values act as defaults and are replaced with any specified when
|
||||
creating a container. If an <code>Entrypoint</code> value is
|
||||
not specified, then the first entry of the <code>Cmd</code>
|
||||
array should be interpreted as the executable to run.
|
||||
</dd>
|
||||
<dt>
|
||||
Volumes <code>struct</code>
|
||||
</dt>
|
||||
<dd>
|
||||
A set of directories which should be created as data volumes in
|
||||
a container running this image. This JSON structure value is
|
||||
unusual because it is a direct JSON serialization of the Go
|
||||
type <code>map[string]struct{}</code> and is represented in
|
||||
JSON as an object mapping its keys to an empty object. Here is
|
||||
an example:
|
||||
<pre>{
|
||||
"/var/my-app-data/": {},
|
||||
"/etc/some-config.d/": {},
|
||||
}</pre>
|
||||
</dd>
|
||||
<dt>
|
||||
WorkingDir <code>string</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Sets the current working directory of the entry point process
|
||||
in the container. This value acts as a default and is replaced
|
||||
by a working directory specified when creating a container.
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
Any extra fields in the Image JSON struct are considered implementation
|
||||
specific and should be ignored by any implementations which are unable to
|
||||
interpret them.
|
||||
|
||||
## Creating an Image Filesystem Changeset
|
||||
|
||||
An example of creating an Image Filesystem Changeset follows.
|
||||
|
||||
An image root filesystem is first created as an empty directory named with the
|
||||
ID of the image being created. Here is the initial empty directory structure
|
||||
for the changeset for an image with ID `c3167915dc9d` ([real IDs are much
|
||||
longer](#id_desc), but this example use a truncated one here for brevity.
|
||||
Implementations need not name the rootfs directory in this way but it may be
|
||||
convenient for keeping record of a large number of image layers.):
|
||||
|
||||
```
|
||||
c3167915dc9d/
|
||||
```
|
||||
|
||||
Files and directories are then created:
|
||||
|
||||
```
|
||||
c3167915dc9d/
|
||||
etc/
|
||||
my-app-config
|
||||
bin/
|
||||
my-app-binary
|
||||
my-app-tools
|
||||
```
|
||||
|
||||
The `c3167915dc9d` directory is then committed as a plain Tar archive with
|
||||
entries for the following files:
|
||||
|
||||
```
|
||||
etc/my-app-config
|
||||
bin/my-app-binary
|
||||
bin/my-app-tools
|
||||
```
|
||||
|
||||
The TarSum checksum for the archive file is then computed and placed in the
|
||||
JSON metadata along with the execution parameters.
|
||||
|
||||
To make changes to the filesystem of this container image, create a new
|
||||
directory named with a new ID, such as `f60c56784b83`, and initialize it with
|
||||
a snapshot of the parent image's root filesystem, so that the directory is
|
||||
identical to that of `c3167915dc9d`. NOTE: a copy-on-write or union filesystem
|
||||
can make this very efficient:
|
||||
|
||||
```
|
||||
f60c56784b83/
|
||||
etc/
|
||||
my-app-config
|
||||
bin/
|
||||
my-app-binary
|
||||
my-app-tools
|
||||
```
|
||||
|
||||
This example change is going add a configuration directory at `/etc/my-app.d`
|
||||
which contains a default config file. There's also a change to the
|
||||
`my-app-tools` binary to handle the config layout change. The `f60c56784b83`
|
||||
directory then looks like this:
|
||||
|
||||
```
|
||||
f60c56784b83/
|
||||
etc/
|
||||
my-app.d/
|
||||
default.cfg
|
||||
bin/
|
||||
my-app-binary
|
||||
my-app-tools
|
||||
```
|
||||
|
||||
This reflects the removal of `/etc/my-app-config` and creation of a file and
|
||||
directory at `/etc/my-app.d/default.cfg`. `/bin/my-app-tools` has also been
|
||||
replaced with an updated version. Before committing this directory to a
|
||||
changeset, because it has a parent image, it is first compared with the
|
||||
directory tree of the parent snapshot, `f60c56784b83`, looking for files and
|
||||
directories that have been added, modified, or removed. The following changeset
|
||||
is found:
|
||||
|
||||
```
|
||||
Added: /etc/my-app.d/default.cfg
|
||||
Modified: /bin/my-app-tools
|
||||
Deleted: /etc/my-app-config
|
||||
```
|
||||
|
||||
A Tar Archive is then created which contains *only* this changeset: The added
|
||||
and modified files and directories in their entirety, and for each deleted item
|
||||
an entry for an empty file at the same location but with the basename of the
|
||||
deleted file or directory prefixed with `.wh.`. The filenames prefixed with
|
||||
`.wh.` are known as "whiteout" files. NOTE: For this reason, it is not possible
|
||||
to create an image root filesystem which contains a file or directory with a
|
||||
name beginning with `.wh.`. The resulting Tar archive for `f60c56784b83` has
|
||||
the following entries:
|
||||
|
||||
```
|
||||
/etc/my-app.d/default.cfg
|
||||
/bin/my-app-tools
|
||||
/etc/.wh.my-app-config
|
||||
```
|
||||
|
||||
Any given image is likely to be composed of several of these Image Filesystem
|
||||
Changeset tar archives.
|
||||
|
||||
## Combined Image JSON + Filesystem Changeset Format
|
||||
|
||||
There is also a format for a single archive which contains complete information
|
||||
about an image, including:
|
||||
|
||||
- repository names/tags
|
||||
- all image layer JSON files
|
||||
- all tar archives of each layer filesystem changesets
|
||||
|
||||
For example, here's what the full archive of `library/busybox` is (displayed in
|
||||
`tree` format):
|
||||
|
||||
```
|
||||
.
|
||||
├── 5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e
|
||||
│ ├── VERSION
|
||||
│ ├── json
|
||||
│ └── layer.tar
|
||||
├── a7b8b41220991bfc754d7ad445ad27b7f272ab8b4a2c175b9512b97471d02a8a
|
||||
│ ├── VERSION
|
||||
│ ├── json
|
||||
│ └── layer.tar
|
||||
├── a936027c5ca8bf8f517923169a233e391cbb38469a75de8383b5228dc2d26ceb
|
||||
│ ├── VERSION
|
||||
│ ├── json
|
||||
│ └── layer.tar
|
||||
├── f60c56784b832dd990022afc120b8136ab3da9528094752ae13fe63a2d28dc8c
|
||||
│ ├── VERSION
|
||||
│ ├── json
|
||||
│ └── layer.tar
|
||||
└── repositories
|
||||
```
|
||||
|
||||
There are one or more directories named with the ID for each layer in a full
|
||||
image. Each of these directories contains 3 files:
|
||||
|
||||
* `VERSION` - The schema version of the `json` file
|
||||
* `json` - The JSON metadata for an image layer
|
||||
* `layer.tar` - The Tar archive of the filesystem changeset for an image
|
||||
layer.
|
||||
|
||||
The content of the `VERSION` files is simply the semantic version of the JSON
|
||||
metadata schema:
|
||||
|
||||
```
|
||||
1.0
|
||||
```
|
||||
|
||||
And the `repositories` file is another JSON file which describes names/tags:
|
||||
|
||||
```
|
||||
{
|
||||
"busybox":{
|
||||
"latest":"5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every key in this object is the name of a repository, and maps to a collection
|
||||
of tag suffixes. Each tag maps to the ID of the image represented by that tag.
|
||||
|
||||
## Loading an Image Filesystem Changeset
|
||||
|
||||
Unpacking a bundle of image layer JSON files and their corresponding filesystem
|
||||
changesets can be done using a series of steps:
|
||||
|
||||
1. Follow the parent IDs of image layers to find the root ancestor (an image
|
||||
with no parent ID specified).
|
||||
2. For every image layer, in order from root ancestor and descending down,
|
||||
extract the contents of that layer's filesystem changeset archive into a
|
||||
directory which will be used as the root of a container filesystem.
|
||||
|
||||
- Extract all contents of each archive.
|
||||
- Walk the directory tree once more, removing any files with the prefix
|
||||
`.wh.` and the corresponding file or directory named without this prefix.
|
||||
|
||||
|
||||
## Implementations
|
||||
|
||||
This specification is an admittedly imperfect description of an
|
||||
imperfectly-understood problem. The Docker project is, in turn, an attempt to
|
||||
implement this specification. Our goal and our execution toward it will evolve
|
||||
over time, but our primary concern in this specification and in our
|
||||
implementation is compatibility and interoperability.
|
||||
289
vendor/github.com/hyperhq/hypercli/image/store.go
generated
vendored
Normal file
289
vendor/github.com/hyperhq/hypercli/image/store.go
generated
vendored
Normal file
@@ -0,0 +1,289 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
)
|
||||
|
||||
// Store is an interface for creating and accessing images
|
||||
type Store interface {
|
||||
Create(config []byte) (ID, error)
|
||||
Get(id ID) (*Image, error)
|
||||
Delete(id ID) ([]layer.Metadata, error)
|
||||
Search(partialID string) (ID, error)
|
||||
SetParent(id ID, parent ID) error
|
||||
GetParent(id ID) (ID, error)
|
||||
Children(id ID) []ID
|
||||
Map() map[ID]*Image
|
||||
Heads() map[ID]*Image
|
||||
}
|
||||
|
||||
// LayerGetReleaser is a minimal interface for getting and releasing images.
|
||||
type LayerGetReleaser interface {
|
||||
Get(layer.ChainID) (layer.Layer, error)
|
||||
Release(layer.Layer) ([]layer.Metadata, error)
|
||||
}
|
||||
|
||||
type imageMeta struct {
|
||||
layer layer.Layer
|
||||
children map[ID]struct{}
|
||||
}
|
||||
|
||||
type store struct {
|
||||
sync.Mutex
|
||||
ls LayerGetReleaser
|
||||
images map[ID]*imageMeta
|
||||
fs StoreBackend
|
||||
digestSet *digest.Set
|
||||
}
|
||||
|
||||
// NewImageStore returns new store object for given layer store
|
||||
func NewImageStore(fs StoreBackend, ls LayerGetReleaser) (Store, error) {
|
||||
is := &store{
|
||||
ls: ls,
|
||||
images: make(map[ID]*imageMeta),
|
||||
fs: fs,
|
||||
digestSet: digest.NewSet(),
|
||||
}
|
||||
|
||||
// load all current images and retain layers
|
||||
if err := is.restore(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return is, nil
|
||||
}
|
||||
|
||||
func (is *store) restore() error {
|
||||
err := is.fs.Walk(func(id ID) error {
|
||||
img, err := is.Get(id)
|
||||
if err != nil {
|
||||
logrus.Errorf("invalid image %v, %v", id, err)
|
||||
return nil
|
||||
}
|
||||
var l layer.Layer
|
||||
if chainID := img.RootFS.ChainID(); chainID != "" {
|
||||
l, err = is.ls.Get(chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := is.digestSet.Add(digest.Digest(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageMeta := &imageMeta{
|
||||
layer: l,
|
||||
children: make(map[ID]struct{}),
|
||||
}
|
||||
|
||||
is.images[ID(id)] = imageMeta
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Second pass to fill in children maps
|
||||
for id := range is.images {
|
||||
if parent, err := is.GetParent(id); err == nil {
|
||||
if parentMeta := is.images[parent]; parentMeta != nil {
|
||||
parentMeta.children[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *store) Create(config []byte) (ID, error) {
|
||||
var img Image
|
||||
err := json.Unmarshal(config, &img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Must reject any config that references diffIDs from the history
|
||||
// which aren't among the rootfs layers.
|
||||
rootFSLayers := make(map[layer.DiffID]struct{})
|
||||
for _, diffID := range img.RootFS.DiffIDs {
|
||||
rootFSLayers[diffID] = struct{}{}
|
||||
}
|
||||
|
||||
layerCounter := 0
|
||||
for _, h := range img.History {
|
||||
if !h.EmptyLayer {
|
||||
layerCounter++
|
||||
}
|
||||
}
|
||||
if layerCounter > len(img.RootFS.DiffIDs) {
|
||||
return "", errors.New("too many non-empty layers in History section")
|
||||
}
|
||||
|
||||
dgst, err := is.fs.Set(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imageID := ID(dgst)
|
||||
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
|
||||
if _, exists := is.images[imageID]; exists {
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
layerID := img.RootFS.ChainID()
|
||||
|
||||
var l layer.Layer
|
||||
if layerID != "" {
|
||||
l, err = is.ls.Get(layerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
imageMeta := &imageMeta{
|
||||
layer: l,
|
||||
children: make(map[ID]struct{}),
|
||||
}
|
||||
|
||||
is.images[imageID] = imageMeta
|
||||
if err := is.digestSet.Add(digest.Digest(imageID)); err != nil {
|
||||
delete(is.images, imageID)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
func (is *store) Search(term string) (ID, error) {
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
|
||||
dgst, err := is.digestSet.Lookup(term)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ID(dgst), nil
|
||||
}
|
||||
|
||||
func (is *store) Get(id ID) (*Image, error) {
|
||||
// todo: Check if image is in images
|
||||
// todo: Detect manual insertions and start using them
|
||||
config, err := is.fs.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img, err := NewFromJSON(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.computedID = id
|
||||
|
||||
img.Parent, err = is.GetParent(id)
|
||||
if err != nil {
|
||||
img.Parent = ""
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (is *store) Delete(id ID) ([]layer.Metadata, error) {
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
|
||||
imageMeta := is.images[id]
|
||||
if imageMeta == nil {
|
||||
return nil, fmt.Errorf("unrecognized image ID %s", id.String())
|
||||
}
|
||||
for id := range imageMeta.children {
|
||||
is.fs.DeleteMetadata(id, "parent")
|
||||
}
|
||||
if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
|
||||
delete(is.images[parent].children, id)
|
||||
}
|
||||
|
||||
if err := is.digestSet.Remove(digest.Digest(id)); err != nil {
|
||||
logrus.Errorf("error removing %s from digest set: %q", id, err)
|
||||
}
|
||||
delete(is.images, id)
|
||||
is.fs.Delete(id)
|
||||
|
||||
if imageMeta.layer != nil {
|
||||
return is.ls.Release(imageMeta.layer)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (is *store) SetParent(id, parent ID) error {
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
parentMeta := is.images[parent]
|
||||
if parentMeta == nil {
|
||||
return fmt.Errorf("unknown parent image ID %s", parent.String())
|
||||
}
|
||||
parentMeta.children[id] = struct{}{}
|
||||
return is.fs.SetMetadata(id, "parent", []byte(parent))
|
||||
}
|
||||
|
||||
func (is *store) GetParent(id ID) (ID, error) {
|
||||
d, err := is.fs.GetMetadata(id, "parent")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ID(d), nil // todo: validate?
|
||||
}
|
||||
|
||||
func (is *store) Children(id ID) []ID {
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
|
||||
return is.children(id)
|
||||
}
|
||||
|
||||
func (is *store) children(id ID) []ID {
|
||||
var ids []ID
|
||||
if is.images[id] != nil {
|
||||
for id := range is.images[id].children {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (is *store) Heads() map[ID]*Image {
|
||||
return is.imagesMap(false)
|
||||
}
|
||||
|
||||
func (is *store) Map() map[ID]*Image {
|
||||
return is.imagesMap(true)
|
||||
}
|
||||
|
||||
func (is *store) imagesMap(all bool) map[ID]*Image {
|
||||
is.Lock()
|
||||
defer is.Unlock()
|
||||
|
||||
images := make(map[ID]*Image)
|
||||
|
||||
for id := range is.images {
|
||||
if !all && len(is.children(id)) > 0 {
|
||||
continue
|
||||
}
|
||||
img, err := is.Get(id)
|
||||
if err != nil {
|
||||
logrus.Errorf("invalid image access: %q, error: %q", id, err)
|
||||
continue
|
||||
}
|
||||
images[id] = img
|
||||
}
|
||||
return images
|
||||
}
|
||||
244
vendor/github.com/hyperhq/hypercli/image/store_test.go
generated
vendored
Normal file
244
vendor/github.com/hyperhq/hypercli/image/store_test.go
generated
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
)
|
||||
|
||||
func TestRestore(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id1, err := fs.Set([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = fs.Set([]byte(`invalid`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := fs.Set([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = fs.SetMetadata(id2, "parent", []byte(id1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
is, err := NewImageStore(fs, &mockLayerGetReleaser{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
imgs := is.Map()
|
||||
if actual, expected := len(imgs), 2; actual != expected {
|
||||
t.Fatalf("invalid images length, expected 2, got %q", len(imgs))
|
||||
}
|
||||
|
||||
img1, err := is.Get(ID(id1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, expected := img1.computedID, ID(id1); actual != expected {
|
||||
t.Fatalf("invalid image ID: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
if actual, expected := img1.computedID.String(), string(id1); actual != expected {
|
||||
t.Fatalf("invalid image ID string: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
img2, err := is.Get(ID(id2))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, expected := img1.Comment, "abc"; actual != expected {
|
||||
t.Fatalf("invalid comment for image1: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
if actual, expected := img2.Comment, "def"; actual != expected {
|
||||
t.Fatalf("invalid comment for image2: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
p, err := is.GetParent(ID(id1))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for getting parent")
|
||||
}
|
||||
|
||||
p, err = is.GetParent(ID(id2))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual, expected := p, ID(id1); actual != expected {
|
||||
t.Fatalf("invalid parent: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
children := is.Children(ID(id1))
|
||||
if len(children) != 1 {
|
||||
t.Fatalf("invalid children length: %q", len(children))
|
||||
}
|
||||
if actual, expected := children[0], ID(id2); actual != expected {
|
||||
t.Fatalf("invalid child for id1: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
heads := is.Heads()
|
||||
if actual, expected := len(heads), 1; actual != expected {
|
||||
t.Fatalf("invalid images length: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
sid1, err := is.Search(string(id1)[:10])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual, expected := sid1, ID(id1); actual != expected {
|
||||
t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
sid1, err = is.Search(digest.Digest(id1).Hex()[:6])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual, expected := sid1, ID(id1); actual != expected {
|
||||
t.Fatalf("searched ID mismatch: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
invalidPattern := digest.Digest(id1).Hex()[1:6]
|
||||
_, err = is.Search(invalidPattern)
|
||||
if err == nil {
|
||||
t.Fatalf("expected search for %q to fail", invalidPattern)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddDelete(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
is, err := NewImageStore(fs, &mockLayerGetReleaser{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id1, err := is.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, expected := id1, ID("sha256:8d25a9c45df515f9d0fe8e4a6b1c64dd3b965a84790ddbcc7954bb9bc89eb993"); actual != expected {
|
||||
t.Fatalf("create ID mismatch: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
img, err := is.Get(id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, expected := img.Comment, "abc"; actual != expected {
|
||||
t.Fatalf("invalid comment in image: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
id2, err := is.Create([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = is.SetParent(id2, id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pid1, err := is.GetParent(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual, expected := pid1, id1; actual != expected {
|
||||
t.Fatalf("invalid parent for image: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
_, err = is.Delete(id1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = is.Get(id1)
|
||||
if err == nil {
|
||||
t.Fatalf("expected get for deleted image %q to fail", id1)
|
||||
}
|
||||
_, err = is.Get(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pid1, err = is.GetParent(id2)
|
||||
if err == nil {
|
||||
t.Fatalf("expected parent check for image %q to fail, got %q", id2, pid1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSearchAfterDelete(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
is, err := NewImageStore(fs, &mockLayerGetReleaser{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := is.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id1, err := is.Search(string(id)[:15])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, expected := id1, id; expected != actual {
|
||||
t.Fatalf("wrong id returned from search: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
if _, err := is.Delete(id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := is.Search(string(id)[:15]); err == nil {
|
||||
t.Fatal("expected search after deletion to fail")
|
||||
}
|
||||
}
|
||||
|
||||
type mockLayerGetReleaser struct{}
|
||||
|
||||
func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ls *mockLayerGetReleaser) Release(layer.Layer) ([]layer.Metadata, error) {
|
||||
return nil, nil
|
||||
}
|
||||
289
vendor/github.com/hyperhq/hypercli/image/tarexport/load.go
generated
vendored
Normal file
289
vendor/github.com/hyperhq/hypercli/image/tarexport/load.go
generated
vendored
Normal file
@@ -0,0 +1,289 @@
|
||||
package tarexport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/image"
|
||||
"github.com/hyperhq/hypercli/image/v1"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
"github.com/hyperhq/hypercli/pkg/archive"
|
||||
"github.com/hyperhq/hypercli/pkg/chrootarchive"
|
||||
"github.com/hyperhq/hypercli/pkg/symlink"
|
||||
"github.com/hyperhq/hypercli/reference"
|
||||
)
|
||||
|
||||
func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer) error {
|
||||
tmpDir, err := ioutil.TempDir("", "docker-import-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := chrootarchive.Untar(inTar, tmpDir, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// read manifest, if no file then load in legacy mode
|
||||
manifestPath, err := safePath(tmpDir, manifestFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifestFile, err := os.Open(manifestPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return l.legacyLoad(tmpDir, outStream)
|
||||
}
|
||||
return manifestFile.Close()
|
||||
}
|
||||
defer manifestFile.Close()
|
||||
|
||||
var manifest []manifestItem
|
||||
if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range manifest {
|
||||
configPath, err := safePath(tmpDir, m.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, err := image.NewFromJSON(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var rootFS image.RootFS
|
||||
rootFS = *img.RootFS
|
||||
rootFS.DiffIDs = nil
|
||||
|
||||
if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
|
||||
return fmt.Errorf("invalid manifest, layers length mismatch: expected %q, got %q", expected, actual)
|
||||
}
|
||||
|
||||
for i, diffID := range img.RootFS.DiffIDs {
|
||||
layerPath, err := safePath(tmpDir, m.Layers[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := rootFS
|
||||
r.Append(diffID)
|
||||
newLayer, err := l.ls.Get(r.ChainID())
|
||||
if err != nil {
|
||||
newLayer, err = l.loadLayer(layerPath, rootFS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer layer.ReleaseAndLog(l.ls, newLayer)
|
||||
if expected, actual := diffID, newLayer.DiffID(); expected != actual {
|
||||
return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
|
||||
}
|
||||
rootFS.Append(diffID)
|
||||
}
|
||||
|
||||
imgID, err := l.is.Create(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, repoTag := range m.RepoTags {
|
||||
named, err := reference.ParseNamed(repoTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref, ok := named.(reference.NamedTagged)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid tag %q", repoTag)
|
||||
}
|
||||
l.setLoadedTag(ref, imgID, outStream)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS) (layer.Layer, error) {
|
||||
rawTar, err := os.Open(filename)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error reading embedded tar: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer rawTar.Close()
|
||||
|
||||
inflatedLayerData, err := archive.DecompressStream(rawTar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer inflatedLayerData.Close()
|
||||
|
||||
return l.ls.Register(inflatedLayerData, rootFS.ChainID())
|
||||
}
|
||||
|
||||
func (l *tarexporter) setLoadedTag(ref reference.NamedTagged, imgID image.ID, outStream io.Writer) error {
|
||||
if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID {
|
||||
fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", ref.String(), string(prevID)) // todo: this message is wrong in case of multiple tags
|
||||
}
|
||||
|
||||
if err := l.rs.AddTag(ref, imgID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer) error {
|
||||
legacyLoadedMap := make(map[string]image.ID)
|
||||
|
||||
dirs, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// every dir represents an image
|
||||
for _, d := range dirs {
|
||||
if d.IsDir() {
|
||||
if err := l.legacyLoadImage(d.Name(), tmpDir, legacyLoadedMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load tags from repositories file
|
||||
repositoriesPath, err := safePath(tmpDir, legacyRepositoriesFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repositoriesFile, err := os.Open(repositoriesPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return repositoriesFile.Close()
|
||||
}
|
||||
defer repositoriesFile.Close()
|
||||
|
||||
repositories := make(map[string]map[string]string)
|
||||
if err := json.NewDecoder(repositoriesFile).Decode(&repositories); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, tagMap := range repositories {
|
||||
for tag, oldID := range tagMap {
|
||||
imgID, ok := legacyLoadedMap[oldID]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid target ID: %v", oldID)
|
||||
}
|
||||
named, err := reference.WithName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref, err := reference.WithTag(named, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.setLoadedTag(ref, imgID, outStream)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[string]image.ID) error {
|
||||
if _, loaded := loadedMap[oldID]; loaded {
|
||||
return nil
|
||||
}
|
||||
configPath, err := safePath(sourceDir, filepath.Join(oldID, legacyConfigFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageJSON, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error reading json: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var img struct{ Parent string }
|
||||
if err := json.Unmarshal(imageJSON, &img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parentID image.ID
|
||||
if img.Parent != "" {
|
||||
for {
|
||||
var loaded bool
|
||||
if parentID, loaded = loadedMap[img.Parent]; !loaded {
|
||||
if err := l.legacyLoadImage(img.Parent, sourceDir, loadedMap); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: try to connect with migrate code
|
||||
rootFS := image.NewRootFS()
|
||||
var history []image.History
|
||||
|
||||
if parentID != "" {
|
||||
parentImg, err := l.is.Get(parentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootFS = parentImg.RootFS
|
||||
history = parentImg.History
|
||||
}
|
||||
|
||||
layerPath, err := safePath(sourceDir, filepath.Join(oldID, legacyLayerFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newLayer, err := l.loadLayer(layerPath, *rootFS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootFS.Append(newLayer.DiffID())
|
||||
|
||||
h, err := v1.HistoryFromConfig(imageJSON, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
history = append(history, h)
|
||||
|
||||
config, err := v1.MakeConfigFromV1Config(imageJSON, rootFS, history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imgID, err := l.is.Create(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metadata, err := l.ls.Release(newLayer)
|
||||
layer.LogReleaseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parentID != "" {
|
||||
if err := l.is.SetParent(imgID, parentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
loadedMap[oldID] = imgID
|
||||
return nil
|
||||
}
|
||||
|
||||
func safePath(base, path string) (string, error) {
|
||||
return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
|
||||
}
|
||||
300
vendor/github.com/hyperhq/hypercli/image/tarexport/save.go
generated
vendored
Normal file
300
vendor/github.com/hyperhq/hypercli/image/tarexport/save.go
generated
vendored
Normal file
@@ -0,0 +1,300 @@
|
||||
package tarexport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/hyperhq/hypercli/image"
|
||||
"github.com/hyperhq/hypercli/image/v1"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
"github.com/hyperhq/hypercli/pkg/archive"
|
||||
"github.com/hyperhq/hypercli/reference"
|
||||
)
|
||||
|
||||
type imageDescriptor struct {
|
||||
refs []reference.NamedTagged
|
||||
layers []string
|
||||
}
|
||||
|
||||
type saveSession struct {
|
||||
*tarexporter
|
||||
outDir string
|
||||
images map[image.ID]*imageDescriptor
|
||||
savedLayers map[string]struct{}
|
||||
}
|
||||
|
||||
func (l *tarexporter) Save(names []string, outStream io.Writer) error {
|
||||
images, err := l.parseNames(names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return (&saveSession{tarexporter: l, images: images}).save(outStream)
|
||||
}
|
||||
|
||||
func (l *tarexporter) parseNames(names []string) (map[image.ID]*imageDescriptor, error) {
|
||||
imgDescr := make(map[image.ID]*imageDescriptor)
|
||||
|
||||
addAssoc := func(id image.ID, ref reference.Named) {
|
||||
if _, ok := imgDescr[id]; !ok {
|
||||
imgDescr[id] = &imageDescriptor{}
|
||||
}
|
||||
|
||||
if ref != nil {
|
||||
var tagged reference.NamedTagged
|
||||
if _, ok := ref.(reference.Canonical); ok {
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
if tagged, ok = ref.(reference.NamedTagged); !ok {
|
||||
var err error
|
||||
if tagged, err = reference.WithTag(ref, reference.DefaultTag); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range imgDescr[id].refs {
|
||||
if tagged.String() == t.String() {
|
||||
return
|
||||
}
|
||||
}
|
||||
imgDescr[id].refs = append(imgDescr[id].refs, tagged)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
ref, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ref.Name() == string(digest.Canonical) {
|
||||
imgID, err := l.is.Search(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAssoc(imgID, nil)
|
||||
continue
|
||||
}
|
||||
if reference.IsNameOnly(ref) {
|
||||
assocs := l.rs.ReferencesByName(ref)
|
||||
for _, assoc := range assocs {
|
||||
addAssoc(assoc.ImageID, assoc.Ref)
|
||||
}
|
||||
if len(assocs) == 0 {
|
||||
imgID, err := l.is.Search(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAssoc(imgID, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var imgID image.ID
|
||||
if imgID, err = l.rs.Get(ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAssoc(imgID, ref)
|
||||
|
||||
}
|
||||
return imgDescr, nil
|
||||
}
|
||||
|
||||
func (s *saveSession) save(outStream io.Writer) error {
|
||||
s.savedLayers = make(map[string]struct{})
|
||||
|
||||
// get image json
|
||||
tempDir, err := ioutil.TempDir("", "docker-export-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
s.outDir = tempDir
|
||||
reposLegacy := make(map[string]map[string]string)
|
||||
|
||||
var manifest []manifestItem
|
||||
|
||||
for id, imageDescr := range s.images {
|
||||
if err = s.saveImage(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var repoTags []string
|
||||
var layers []string
|
||||
|
||||
for _, ref := range imageDescr.refs {
|
||||
if _, ok := reposLegacy[ref.Name()]; !ok {
|
||||
reposLegacy[ref.Name()] = make(map[string]string)
|
||||
}
|
||||
reposLegacy[ref.Name()][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1]
|
||||
repoTags = append(repoTags, ref.String())
|
||||
}
|
||||
|
||||
for _, l := range imageDescr.layers {
|
||||
layers = append(layers, filepath.Join(l, legacyLayerFileName))
|
||||
}
|
||||
|
||||
manifest = append(manifest, manifestItem{
|
||||
Config: digest.Digest(id).Hex() + ".json",
|
||||
RepoTags: repoTags,
|
||||
Layers: layers,
|
||||
})
|
||||
}
|
||||
|
||||
if len(reposLegacy) > 0 {
|
||||
reposFile := filepath.Join(tempDir, legacyRepositoriesFileName)
|
||||
f, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(reposLegacy); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
manifestFileName := filepath.Join(tempDir, manifestFileName)
|
||||
f, err := os.OpenFile(manifestFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chtimes(manifestFileName, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs, err := archive.Tar(tempDir, archive.Uncompressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
if _, err := io.Copy(outStream, fs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *saveSession) saveImage(id image.ID) error {
|
||||
img, err := s.is.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(img.RootFS.DiffIDs) == 0 {
|
||||
return fmt.Errorf("empty export - not implemented")
|
||||
}
|
||||
|
||||
var parent digest.Digest
|
||||
var layers []string
|
||||
for i := range img.RootFS.DiffIDs {
|
||||
v1Img := image.V1Image{}
|
||||
if i == len(img.RootFS.DiffIDs)-1 {
|
||||
v1Img = img.V1Image
|
||||
}
|
||||
rootFS := *img.RootFS
|
||||
rootFS.DiffIDs = rootFS.DiffIDs[:i+1]
|
||||
v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v1Img.ID = v1ID.Hex()
|
||||
if parent != "" {
|
||||
v1Img.Parent = parent.Hex()
|
||||
}
|
||||
|
||||
if err := s.saveLayer(rootFS.ChainID(), v1Img, img.Created); err != nil {
|
||||
return err
|
||||
}
|
||||
layers = append(layers, v1Img.ID)
|
||||
parent = v1ID
|
||||
}
|
||||
|
||||
configFile := filepath.Join(s.outDir, digest.Digest(id).Hex()+".json")
|
||||
if err := ioutil.WriteFile(configFile, img.RawJSON(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chtimes(configFile, img.Created, img.Created); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.images[id].layers = layers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, createdTime time.Time) error {
|
||||
if _, exists := s.savedLayers[legacyImg.ID]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
outDir := filepath.Join(s.outDir, legacyImg.ID)
|
||||
if err := os.Mkdir(outDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// todo: why is this version file here?
|
||||
if err := ioutil.WriteFile(filepath.Join(outDir, legacyVersionFileName), []byte("1.0"), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageConfig, err := json.Marshal(legacyImg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(outDir, legacyConfigFileName), imageConfig, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// serialize filesystem
|
||||
tarFile, err := os.Create(filepath.Join(outDir, legacyLayerFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tarFile.Close()
|
||||
|
||||
l, err := s.ls.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer layer.ReleaseAndLog(s.ls, l)
|
||||
|
||||
arch, err := l.TarStream()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer arch.Close()
|
||||
|
||||
if _, err := io.Copy(tarFile, arch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, fname := range []string{"", legacyVersionFileName, legacyConfigFileName, legacyLayerFileName} {
|
||||
// todo: maybe save layer created timestamp?
|
||||
if err := os.Chtimes(filepath.Join(outDir, fname), createdTime, createdTime); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.savedLayers[legacyImg.ID] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
36
vendor/github.com/hyperhq/hypercli/image/tarexport/tarexport.go
generated
vendored
Normal file
36
vendor/github.com/hyperhq/hypercli/image/tarexport/tarexport.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
package tarexport
|
||||
|
||||
import (
|
||||
"github.com/hyperhq/hypercli/image"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
"github.com/hyperhq/hypercli/reference"
|
||||
)
|
||||
|
||||
const (
|
||||
manifestFileName = "manifest.json"
|
||||
legacyLayerFileName = "layer.tar"
|
||||
legacyConfigFileName = "json"
|
||||
legacyVersionFileName = "VERSION"
|
||||
legacyRepositoriesFileName = "repositories"
|
||||
)
|
||||
|
||||
type manifestItem struct {
|
||||
Config string
|
||||
RepoTags []string
|
||||
Layers []string
|
||||
}
|
||||
|
||||
type tarexporter struct {
|
||||
is image.Store
|
||||
ls layer.Store
|
||||
rs reference.Store
|
||||
}
|
||||
|
||||
// NewTarExporter returns new ImageExporter for tar packages
|
||||
func NewTarExporter(is image.Store, ls layer.Store, rs reference.Store) image.Exporter {
|
||||
return &tarexporter{
|
||||
is: is,
|
||||
ls: ls,
|
||||
rs: rs,
|
||||
}
|
||||
}
|
||||
148
vendor/github.com/hyperhq/hypercli/image/v1/imagev1.go
generated
vendored
Normal file
148
vendor/github.com/hyperhq/hypercli/image/v1/imagev1.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/hyperhq/hypercli/image"
|
||||
"github.com/hyperhq/hypercli/layer"
|
||||
"github.com/hyperhq/hypercli/pkg/version"
|
||||
)
|
||||
|
||||
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||
|
||||
// noFallbackMinVersion is the minimum version for which v1compatibility
|
||||
// information will not be marshaled through the Image struct to remove
|
||||
// blank fields.
|
||||
var noFallbackMinVersion = version.Version("1.8.3")
|
||||
|
||||
// HistoryFromConfig creates a History struct from v1 configuration JSON
|
||||
func HistoryFromConfig(imageJSON []byte, emptyLayer bool) (image.History, error) {
|
||||
h := image.History{}
|
||||
var v1Image image.V1Image
|
||||
if err := json.Unmarshal(imageJSON, &v1Image); err != nil {
|
||||
return h, err
|
||||
}
|
||||
|
||||
return image.History{
|
||||
Author: v1Image.Author,
|
||||
Created: v1Image.Created,
|
||||
CreatedBy: strings.Join(v1Image.ContainerConfig.Cmd, " "),
|
||||
Comment: v1Image.Comment,
|
||||
EmptyLayer: emptyLayer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateID creates an ID from v1 image, layerID and parent ID.
|
||||
// Used for backwards compatibility with old clients.
|
||||
func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest) (digest.Digest, error) {
|
||||
v1Image.ID = ""
|
||||
v1JSON, err := json.Marshal(v1Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var config map[string]*json.RawMessage
|
||||
if err := json.Unmarshal(v1JSON, &config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// FIXME: note that this is slightly incompatible with RootFS logic
|
||||
config["layer_id"] = rawJSON(layerID)
|
||||
if parent != "" {
|
||||
config["parent"] = rawJSON(parent)
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logrus.Debugf("CreateV1ID %s", configJSON)
|
||||
|
||||
return digest.FromBytes(configJSON), nil
|
||||
}
|
||||
|
||||
// MakeConfigFromV1Config creates an image config from the legacy V1 config format.
|
||||
func MakeConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) ([]byte, error) {
|
||||
var dver struct {
|
||||
DockerVersion string `json:"docker_version"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(imageJSON, &dver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useFallback := version.Version(dver.DockerVersion).LessThan(noFallbackMinVersion)
|
||||
|
||||
if useFallback {
|
||||
var v1Image image.V1Image
|
||||
err := json.Unmarshal(imageJSON, &v1Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageJSON, err = json.Marshal(v1Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var c map[string]*json.RawMessage
|
||||
if err := json.Unmarshal(imageJSON, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(c, "id")
|
||||
delete(c, "parent")
|
||||
delete(c, "Size") // Size is calculated from data on disk and is inconsitent
|
||||
delete(c, "parent_id")
|
||||
delete(c, "layer_id")
|
||||
delete(c, "throwaway")
|
||||
|
||||
c["rootfs"] = rawJSON(rootfs)
|
||||
c["history"] = rawJSON(history)
|
||||
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// MakeV1ConfigFromConfig creates an legacy V1 image config from an Image struct
|
||||
func MakeV1ConfigFromConfig(img *image.Image, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
|
||||
// Top-level v1compatibility string should be a modified version of the
|
||||
// image config.
|
||||
var configAsMap map[string]*json.RawMessage
|
||||
if err := json.Unmarshal(img.RawJSON(), &configAsMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete fields that didn't exist in old manifest
|
||||
delete(configAsMap, "rootfs")
|
||||
delete(configAsMap, "history")
|
||||
configAsMap["id"] = rawJSON(v1ID)
|
||||
if parentV1ID != "" {
|
||||
configAsMap["parent"] = rawJSON(parentV1ID)
|
||||
}
|
||||
if throwaway {
|
||||
configAsMap["throwaway"] = rawJSON(true)
|
||||
}
|
||||
|
||||
return json.Marshal(configAsMap)
|
||||
}
|
||||
|
||||
func rawJSON(value interface{}) *json.RawMessage {
|
||||
jsonval, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return (*json.RawMessage)(&jsonval)
|
||||
}
|
||||
|
||||
// ValidateID checks whether an ID string is a valid image ID.
|
||||
func ValidateID(id string) error {
|
||||
if ok := validHex.MatchString(id); !ok {
|
||||
return fmt.Errorf("image ID '%s' is invalid ", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user