X Tutup
Skip to content

Commit 68fd252

Browse files
committed
snapshot: define the snapshot driver interface
We now define the `snapshot.Driver` interface based on earlier work. Many details of the model are worked out, such as snapshot lifecycle and parentage of commits against "Active" snapshots. The impetus of this change is to provide a snapshot POC that does a complete push/pull workflow. The beginnings of a test suite for snapshot drivers is included that we can use to verify the assumptions of drivers. The intent is to port the existing tests over to this test suite and start scaling contributions and test to the snapshot driver subsystem. There are still some details that need to be worked out, such as listing and metadata access. We can do this activity as we further integrate with tooling. Signed-off-by: Stephen J Day <stephen.day@docker.com>
1 parent 78b55a1 commit 68fd252

File tree

9 files changed

+338
-428
lines changed

9 files changed

+338
-428
lines changed

content/content_test.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import (
1616
"testing"
1717
"time"
1818

19+
"github.com/docker/containerd/testutil"
1920
"github.com/opencontainers/go-digest"
2021
)
2122

2223
func TestContentWriter(t *testing.T) {
2324
tmpdir, cs, cleanup := contentStoreEnv(t)
2425
defer cleanup()
26+
defer testutil.DumpDir(t, tmpdir)
2527

2628
if _, err := os.Stat(filepath.Join(tmpdir, "ingest")); os.IsNotExist(err) {
2729
t.Fatal("ingest dir should be created", err)
@@ -114,7 +116,6 @@ func TestContentWriter(t *testing.T) {
114116
t.Fatal("mismatched data written to disk")
115117
}
116118

117-
dumpDir(tmpdir)
118119
}
119120

120121
func TestWalkBlobs(t *testing.T) {
@@ -279,14 +280,3 @@ func checkWrite(t checker, cs *Store, dgst digest.Digest, p []byte) digest.Diges
279280

280281
return dgst
281282
}
282-
283-
func dumpDir(root string) error {
284-
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
285-
if err != nil {
286-
return err
287-
}
288-
289-
fmt.Println(fi.Mode(), path)
290-
return nil
291-
})
292-
}

mount.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package containerd
33
import (
44
"strings"
55
"syscall"
6+
7+
"github.com/Sirupsen/logrus"
8+
"github.com/docker/containerd/log"
69
)
710

811
// Mount is the lingua franca of containerd. A mount represents a
@@ -20,6 +23,13 @@ type Mount struct {
2023

2124
func (m *Mount) Mount(target string) error {
2225
flags, data := parseMountOptions(m.Options)
26+
27+
lfields := logrus.Fields{"target": target, "source": m.Source}
28+
if data != "" {
29+
lfields["data"] = data
30+
}
31+
log.L.WithFields(lfields).Debug("syscall.Mount")
32+
2333
return syscall.Mount(m.Source, target, m.Type, uintptr(flags), data)
2434
}
2535

snapshot/btrfs/btrfs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"testing"
1111

1212
"github.com/docker/containerd"
13-
"github.com/docker/containerd/snapshot/testutil"
13+
"github.com/docker/containerd/testutil"
1414
btrfs "github.com/stevvooe/go-btrfs"
1515
)
1616

snapshot/driver.go

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
package snapshot
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/docker/containerd"
10+
"github.com/docker/containerd/testutil"
11+
)
12+
13+
// Driver defines the methods required to implement a snapshot driver for
14+
// allocating, snapshotting and mounting abstract filesystems. The model works
15+
// by building up sets of changes with parent-child relationships.
16+
//
17+
// These differ from the concept of the graphdriver in that the Manager has no
18+
// knowledge of images, layers or containers. Users simply prepare and commit
19+
// directories. We also avoid the integration between graph driver's and the
20+
// tar format used to represent the changesets.
21+
//
22+
// A snapshot represents a filesystem state. Every snapshot has a parent, where
23+
// the empty parent is represented by the empty string. A diff can be taken
24+
// between a parent and its snapshot to generate a classic layer.
25+
//
26+
// For convention, we define the following terms to be used throughout this
27+
// interface for driver implementations:
28+
//
29+
// `name` - refers to a forkable snapshot, typically read only
30+
// `key` - refers to an active transaction, either a prepare or view
31+
// `parent` - refers to the parent in relation to a name
32+
//
33+
// TODO(stevvooe): Update this description when things settle.
34+
//
35+
// Importing a Layer
36+
//
37+
// To import a layer, we simply have the Manager provide a list of
38+
// mounts to be applied such that our dst will capture a changeset. We start
39+
// out by getting a path to the layer tar file and creating a temp location to
40+
// unpack it to:
41+
//
42+
// layerPath, tmpLocation := getLayerPath(), mkTmpDir() // just a path to layer tar file.
43+
//
44+
// We then use a Manager to prepare the temporary location as a
45+
// snapshot point:
46+
//
47+
// sm := NewManager()
48+
// mounts, err := sm.Prepare(tmpLocation, "")
49+
// if err != nil { ... }
50+
//
51+
// Note that we provide "" as the parent, since we are applying the diff to an
52+
// empty directory. We get back a list of mounts from Manager.Prepare.
53+
// Before proceeding, we perform all these mounts:
54+
//
55+
// if err := MountAll(mounts); err != nil { ... }
56+
//
57+
// Once the mounts are performed, our temporary location is ready to capture
58+
// a diff. In practice, this works similar to a filesystem transaction. The
59+
// next step is to unpack the layer. We have a special function unpackLayer
60+
// that applies the contents of the layer to target location and calculates the
61+
// DiffID of the unpacked layer (this is a requirement for docker
62+
// implementation):
63+
//
64+
// layer, err := os.Open(layerPath)
65+
// if err != nil { ... }
66+
// digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location
67+
// if err != nil { ... }
68+
//
69+
// When the above completes, we should have a filesystem the represents the
70+
// contents of the layer. Careful implementations should verify that digest
71+
// matches the expected DiffID. When completed, we unmount the mounts:
72+
//
73+
// unmount(mounts) // optional, for now
74+
//
75+
// Now that we've verified and unpacked our layer, we create a location to
76+
// commit the actual diff. For this example, we are just going to use the layer
77+
// digest, but in practice, this will probably be the ChainID:
78+
//
79+
// diffPath := filepath.Join("/layers", digest) // name location for the uncompressed layer digest
80+
// if err := sm.Commit(diffPath, tmpLocation); err != nil { ... }
81+
//
82+
// Now, we have a layer in the Manager that can be accessed with the
83+
// opaque diffPath provided during commit.
84+
//
85+
// Importing the Next Layer
86+
//
87+
// Making a layer depend on the above is identical to the process described
88+
// above except that the parent is provided as diffPath when calling
89+
// Manager.Prepare:
90+
//
91+
// mounts, err := sm.Prepare(tmpLocation, parentDiffPath)
92+
//
93+
// The diff will be captured at tmpLocation, as the layer is applied.
94+
//
95+
// Running a Container
96+
//
97+
// To run a container, we simply provide Manager.Prepare the diffPath
98+
// of the image we want to start the container from. After mounting, the
99+
// prepared path can be used directly as the container's filesystem:
100+
//
101+
// mounts, err := sm.Prepare(containerRootFS, imageDiffPath)
102+
//
103+
// The returned mounts can then be passed directly to the container runtime. If
104+
// one would like to create a new image from the filesystem,
105+
// Manager.Commit is called:
106+
//
107+
// if err := sm.Commit(newImageDiff, containerRootFS); err != nil { ... }
108+
//
109+
// Alternatively, for most container runs, Manager.Rollback will be
110+
// called to signal Manager to abandon the changes.
111+
type Driver interface {
112+
// Prepare returns a set of mounts corresponding to an active snapshot
113+
// transaction, identified by the provided transaction key.
114+
//
115+
// If a parent is provided, after performing the mounts, the destination
116+
// will start with the content of the parent. Changes to the mounted
117+
// destination will be captured in relation to the provided parent. The
118+
// default parent, "", is an empty directory.
119+
//
120+
// The changes may be saved to a new snapshot by calling Commit. When one
121+
// is done with the transaction, Remove should be called on the key.
122+
//
123+
// Multiple calls to Prepare or View with the same key should fail.
124+
Prepare(key, parent string) ([]containerd.Mount, error)
125+
126+
// View behaves identically to Prepare except the result may not be committed
127+
// back to the snapshot manager. View returns a readonly view on the
128+
// parent, with the transaction tracked by the given key.
129+
//
130+
// This method operates identically to Prepare, except that Mounts returned
131+
// may have the readonly flag set. Any modifications to the underlying
132+
// filesystem will be ignored.
133+
//
134+
// Commit may not be called on the provided key. To collect the resources
135+
// associated with key, Remove must be called with key as the argument.
136+
View(key, parent string) ([]containerd.Mount, error)
137+
138+
// Commit captures the changes between key and its parent into a snapshot
139+
// identified by name. The name can then be used with the driver's other
140+
// methods to create subsequent snapshots.
141+
//
142+
// A snapshot will be created under name with the parent that started the
143+
// transaction.
144+
//
145+
// Commit may be called multiple times on the same key. Snapshots created
146+
// in this manner will all reference the parent used to start the
147+
// transaction.
148+
Commit(name, key string) error
149+
150+
// Mounts returns the mounts for the transaction identified by key. Can be
151+
// called on an read-write or readonly transaction.
152+
//
153+
// This can be used to recover mounts after calling View or Prepare.
154+
Mounts(key string) ([]containerd.Mount, error)
155+
156+
// Remove abandons the transaction identified by key. All resources
157+
// associated with the key will be removed.
158+
Remove(key string) error
159+
160+
// Parent returns the parent of snapshot identified by name.
161+
Parent(name string) (string, error)
162+
163+
// Exists returns true if the snapshot with name exists.
164+
Exists(name string) bool
165+
166+
// Delete the snapshot idenfitied by name.
167+
//
168+
// If name has children, the operation will fail.
169+
Delete(name string) error
170+
171+
// TODO(stevvooe): The methods below are still in flux. We'll need to work
172+
// out the roles of active and committed snapshots for this to become more
173+
// clear.
174+
175+
// Walk the committed snapshots.
176+
Walk(fn func(name string) error) error
177+
178+
// Active will call fn for each active transaction.
179+
Active(fn func(key string) error) error
180+
}
181+
182+
// DriverSuite runs a test suite on the driver given a factory function.
183+
func DriverSuite(t *testing.T, name string, driverFn func(root string) (Driver, func(), error)) {
184+
t.Run("Basic", makeTest(t, name, driverFn, checkDriverBasic))
185+
}
186+
187+
func makeTest(t *testing.T, name string, driverFn func(root string) (Driver, func(), error), fn func(t *testing.T, driver Driver, work string)) func(t *testing.T) {
188+
return func(t *testing.T) {
189+
// Make two directories: a driver root and a play area for the tests:
190+
//
191+
// /tmp
192+
// work/ -> passed to test functions
193+
// root/ -> passed to driver
194+
//
195+
tmpDir, err := ioutil.TempDir("", "snapshot-suite-"+name+"-")
196+
if err != nil {
197+
t.Fatal(err)
198+
}
199+
defer os.RemoveAll(tmpDir)
200+
201+
root := filepath.Join(tmpDir, "root")
202+
if err := os.MkdirAll(root, 0777); err != nil {
203+
t.Fatal(err)
204+
}
205+
206+
driver, cleanup, err := driverFn(root)
207+
if err != nil {
208+
t.Fatal(err)
209+
}
210+
defer cleanup()
211+
212+
work := filepath.Join(tmpDir, "work")
213+
if err := os.MkdirAll(work, 0777); err != nil {
214+
t.Fatal(err)
215+
}
216+
217+
defer testutil.DumpDir(t, tmpDir)
218+
fn(t, driver, work)
219+
}
220+
}
221+
222+
// checkDriverBasic tests the basic workflow of a snapshot driver.
223+
func checkDriverBasic(t *testing.T, driver Driver, work string) {
224+
preparing := filepath.Join(work, "preparing")
225+
if err := os.MkdirAll(preparing, 0777); err != nil {
226+
t.Fatal(err)
227+
}
228+
229+
mounts, err := driver.Prepare(preparing, "")
230+
if err != nil {
231+
t.Fatal(err)
232+
}
233+
234+
if len(mounts) < 1 {
235+
t.Fatal("expected mounts to have entries")
236+
}
237+
238+
if err := containerd.MountAll(mounts, preparing); err != nil {
239+
t.Fatal(err)
240+
}
241+
defer testutil.Unmount(t, preparing)
242+
243+
if err := ioutil.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
244+
t.Fatal(err)
245+
}
246+
247+
if err := os.MkdirAll(filepath.Join(preparing, "a", "b", "c"), 0755); err != nil {
248+
t.Fatal(err)
249+
}
250+
251+
committed := filepath.Join(work, "committed")
252+
if err := driver.Commit(committed, preparing); err != nil {
253+
t.Fatal(err)
254+
}
255+
256+
parent, err := driver.Parent(committed)
257+
if err != nil {
258+
t.Fatal(err)
259+
}
260+
261+
if parent != "" {
262+
t.Fatalf("parent of new layer should be empty, got driver.Parent(%q) == %q", committed, parent)
263+
}
264+
265+
next := filepath.Join(work, "nextlayer")
266+
if err := os.MkdirAll(next, 0777); err != nil {
267+
t.Fatal(err)
268+
}
269+
270+
mounts, err = driver.Prepare(next, committed)
271+
if err != nil {
272+
t.Fatal(err)
273+
}
274+
if err := containerd.MountAll(mounts, next); err != nil {
275+
t.Fatal(err)
276+
}
277+
defer testutil.Unmount(t, next)
278+
279+
if err := ioutil.WriteFile(filepath.Join(next, "bar"), []byte("bar\n"), 0777); err != nil {
280+
t.Fatal(err)
281+
}
282+
283+
// also, change content of foo to bar
284+
if err := ioutil.WriteFile(filepath.Join(next, "foo"), []byte("bar\n"), 0777); err != nil {
285+
t.Fatal(err)
286+
}
287+
288+
if err := os.RemoveAll(filepath.Join(next, "a", "b")); err != nil {
289+
t.Log(err)
290+
}
291+
292+
nextCommitted := filepath.Join(work, "committed-next")
293+
if err := driver.Commit(nextCommitted, next); err != nil {
294+
t.Fatal(err)
295+
}
296+
297+
parent, err = driver.Parent(nextCommitted)
298+
if parent != committed {
299+
t.Fatalf("parent of new layer should be %q, got driver.Parent(%q) == %q", committed, next, parent)
300+
}
301+
}

0 commit comments

Comments
 (0)
X Tutup