Skip to content

Commit 0c386e2

Browse files
committed
Add blockfile snapshotter
Signed-off-by: Derek McGowan <[email protected]>
1 parent 0de40f4 commit 0c386e2

5 files changed

Lines changed: 615 additions & 0 deletions

File tree

snapshots/blockfile/blockfile.go

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package blockfile
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
25+
"github.com/containerd/containerd/log"
26+
"github.com/containerd/containerd/mount"
27+
"github.com/containerd/containerd/plugin"
28+
"github.com/containerd/containerd/snapshots"
29+
"github.com/containerd/containerd/snapshots/storage"
30+
31+
"github.com/containerd/continuity/fs"
32+
)
33+
34+
// SnapshotterConfig holds the configurable properties for the blockfile snapshotter
35+
type SnapshotterConfig struct {
36+
// recreateScratch is whether scratch should be recreated even
37+
// if already exists
38+
recreateScratch bool
39+
40+
scratchGenerator func(string) error
41+
42+
// fsType is the filesystem type for the mount (defaults to ext4)
43+
fsType string
44+
}
45+
46+
// Opt is an option to configure the overlay snapshotter
47+
type Opt func(string, *SnapshotterConfig)
48+
49+
// WithScratchFile provides a scratch file which will get copied on startup
50+
// if the scratch file needs to be generated.
51+
func WithScratchFile(src string) Opt {
52+
return func(root string, config *SnapshotterConfig) {
53+
config.scratchGenerator = func(dst string) error {
54+
// Copy src to dst
55+
scratch := filepath.Join(root, "scratch")
56+
if err := fs.CopyFile(scratch, src); err != nil {
57+
return fmt.Errorf("failed to copy scratch: %w", err)
58+
}
59+
return nil
60+
}
61+
}
62+
}
63+
64+
// WithFSType defines the filesystem type to apply to mounts of the blockfile
65+
func WithFSType(fsType string) Opt {
66+
return func(root string, config *SnapshotterConfig) {
67+
config.fsType = fsType
68+
}
69+
}
70+
71+
type snapshotter struct {
72+
root string
73+
scratch string
74+
fsType string
75+
ms *storage.MetaStore
76+
}
77+
78+
// NewSnapshotter returns a Snapshotter which copies layers on the underlying
79+
// file system. A metadata file is stored under the root.
80+
func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
81+
var config SnapshotterConfig
82+
if err := os.MkdirAll(root, 0700); err != nil {
83+
return nil, err
84+
}
85+
86+
for _, opt := range opts {
87+
opt(root, &config)
88+
}
89+
90+
scratch := filepath.Join(root, "scratch")
91+
createScratch := config.recreateScratch
92+
if !createScratch {
93+
if _, err := os.Stat(scratch); err != nil {
94+
if !os.IsNotExist(err) {
95+
return nil, fmt.Errorf("unable to stat scratch file: %w", err)
96+
}
97+
createScratch = true
98+
}
99+
}
100+
if createScratch {
101+
if config.scratchGenerator == nil {
102+
return nil, fmt.Errorf("no scratch file generator: %w", plugin.ErrSkipPlugin)
103+
}
104+
if err := config.scratchGenerator(scratch); err != nil {
105+
return nil, fmt.Errorf("failed to generate scratch file: %w", err)
106+
}
107+
}
108+
109+
if config.fsType == "" {
110+
config.fsType = "ext4"
111+
}
112+
113+
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
119+
return nil, err
120+
}
121+
122+
return &snapshotter{
123+
root: root,
124+
scratch: scratch,
125+
fsType: config.fsType,
126+
ms: ms,
127+
}, nil
128+
}
129+
130+
// Stat returns the info for an active or committed snapshot by name or
131+
// key.
132+
//
133+
// Should be used for parent resolution, existence checks and to discern
134+
// the kind of snapshot.
135+
func (o *snapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
136+
err = o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
137+
_, info, _, err = storage.GetInfo(ctx, key)
138+
return err
139+
})
140+
if err != nil {
141+
return snapshots.Info{}, err
142+
}
143+
144+
return info, nil
145+
}
146+
147+
func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (_ snapshots.Info, err error) {
148+
err = o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
149+
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
150+
return err
151+
})
152+
if err != nil {
153+
return snapshots.Info{}, err
154+
}
155+
156+
return info, nil
157+
}
158+
159+
func (o *snapshotter) Usage(ctx context.Context, key string) (usage snapshots.Usage, err error) {
160+
var (
161+
id string
162+
info snapshots.Info
163+
)
164+
165+
err = o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
166+
id, info, usage, err = storage.GetInfo(ctx, key)
167+
if err != nil {
168+
return err
169+
}
170+
171+
// Current usage calculation is an approximation based on the size
172+
// of the block file - the size of its parent. This does not consider
173+
// that the filesystem may not support shared extents between the block
174+
// file and its parents, in which case the accurate calculation would just
175+
// be the size of the block file. Additionally, this does not take into
176+
// consideration that file may have been removed before being adding,
177+
// making the number of shared extents between the parent and the block
178+
// file smaller than the parent, under reporting actual usage.
179+
//
180+
// A more ideal calculation would look like:
181+
// size(block) - usage(extent_intersection(block,parent))
182+
// OR
183+
// usage(extent_union(block,parent)) - size(parent)
184+
185+
if info.Kind == snapshots.KindActive {
186+
// TODO: Use size calculator from fs package
187+
st, err := os.Stat(o.getBlockFile(id))
188+
if err != nil {
189+
return err
190+
}
191+
usage.Size = st.Size()
192+
usage.Inodes = 1
193+
}
194+
195+
if info.Parent != "" {
196+
// GetInfo returns total number of bytes used by a snapshot (including parent).
197+
// So subtract parent usage in order to get delta consumed by layer itself.
198+
_, _, parentUsage, err := storage.GetInfo(ctx, info.Parent)
199+
if err != nil {
200+
return err
201+
}
202+
203+
usage.Size -= parentUsage.Size
204+
}
205+
206+
return err
207+
})
208+
if err != nil {
209+
return snapshots.Usage{}, err
210+
}
211+
212+
return usage, nil
213+
}
214+
215+
func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
216+
return o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
217+
}
218+
219+
func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
220+
return o.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
221+
}
222+
223+
// Mounts returns the mounts for the transaction identified by key. Can be
224+
// called on an read-write or readonly transaction.
225+
//
226+
// This can be used to recover mounts after calling View or Prepare.
227+
func (o *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
228+
var s storage.Snapshot
229+
err = o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
230+
s, err = storage.GetSnapshot(ctx, key)
231+
if err != nil {
232+
return fmt.Errorf("failed to get snapshot mount: %w", err)
233+
}
234+
235+
return nil
236+
})
237+
if err != nil {
238+
return nil, err
239+
}
240+
241+
return o.mounts(s), nil
242+
}
243+
244+
func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
245+
return o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
246+
id, _, _, err := storage.GetInfo(ctx, key)
247+
if err != nil {
248+
return err
249+
}
250+
251+
st, err := os.Stat(o.getBlockFile(id))
252+
if err != nil {
253+
return err
254+
}
255+
256+
usage := snapshots.Usage{
257+
Size: st.Size(),
258+
Inodes: 1,
259+
}
260+
261+
if _, err = storage.CommitActive(ctx, key, name, usage, opts...); err != nil {
262+
return fmt.Errorf("failed to commit snapshot: %w", err)
263+
}
264+
return nil
265+
})
266+
}
267+
268+
// Remove abandons the transaction identified by key. All resources
269+
// associated with the key will be removed.
270+
func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
271+
var (
272+
renamed, path string
273+
restore bool
274+
)
275+
276+
err = o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
277+
id, _, err := storage.Remove(ctx, key)
278+
if err != nil {
279+
return fmt.Errorf("failed to remove: %w", err)
280+
}
281+
282+
path = o.getBlockFile(id)
283+
renamed = filepath.Join(o.root, "snapshots", "rm-"+id)
284+
if err = os.Rename(path, renamed); err != nil {
285+
if !os.IsNotExist(err) {
286+
return fmt.Errorf("failed to rename: %w", err)
287+
}
288+
renamed = ""
289+
}
290+
291+
restore = true
292+
return nil
293+
})
294+
295+
if err != nil {
296+
if renamed != "" && restore {
297+
if err1 := os.Rename(renamed, path); err1 != nil {
298+
// May cause inconsistent data on disk
299+
log.G(ctx).WithError(err1).WithField("path", renamed).Error("failed to rename after failed commit")
300+
}
301+
}
302+
return err
303+
}
304+
if renamed != "" {
305+
if err := os.Remove(renamed); err != nil {
306+
// Must be cleaned up, any "rm-*" could be removed if no active transactions
307+
log.G(ctx).WithError(err).WithField("path", renamed).Warnf("failed to remove root filesystem")
308+
}
309+
}
310+
311+
return nil
312+
}
313+
314+
// Walk the committed snapshots.
315+
func (o *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
316+
return o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
317+
return storage.WalkInfo(ctx, fn, fs...)
318+
})
319+
}
320+
321+
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
322+
var s storage.Snapshot
323+
324+
err = o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
325+
s, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
326+
if err != nil {
327+
return fmt.Errorf("failed to create snapshot: %w", err)
328+
}
329+
330+
if len(s.ParentIDs) == 0 || s.Kind == snapshots.KindActive {
331+
path := o.getBlockFile(s.ID)
332+
333+
if len(s.ParentIDs) > 0 {
334+
if err = fs.CopyFile(path, o.getBlockFile(s.ParentIDs[0])); err != nil {
335+
return fmt.Errorf("copying of parent failed: %w", err)
336+
}
337+
} else {
338+
if err = fs.CopyFile(path, o.scratch); err != nil {
339+
return fmt.Errorf("copying of scratch failed: %w", err)
340+
}
341+
}
342+
}
343+
344+
return nil
345+
})
346+
if err != nil {
347+
return nil, err
348+
}
349+
350+
return o.mounts(s), nil
351+
}
352+
353+
func (o *snapshotter) getBlockFile(id string) string {
354+
return filepath.Join(o.root, "snapshots", id)
355+
}
356+
357+
func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount {
358+
var (
359+
mountOptions = []string{
360+
"loop",
361+
}
362+
source string
363+
)
364+
365+
if s.Kind == snapshots.KindView {
366+
mountOptions = append(mountOptions, "ro")
367+
} else {
368+
mountOptions = append(mountOptions, "rw")
369+
}
370+
371+
if len(s.ParentIDs) == 0 || s.Kind == snapshots.KindActive {
372+
source = o.getBlockFile(s.ID)
373+
} else {
374+
source = o.getBlockFile(s.ParentIDs[0])
375+
}
376+
377+
return []mount.Mount{
378+
{
379+
Source: source,
380+
Type: o.fsType,
381+
Options: mountOptions,
382+
},
383+
}
384+
}
385+
386+
// Close closes the snapshotter
387+
func (o *snapshotter) Close() error {
388+
return o.ms.Close()
389+
}

0 commit comments

Comments
 (0)