@@ -19,10 +19,15 @@ package snapshot
1919import (
2020 "bytes"
2121 "context"
22+ "crypto/rand"
23+ "encoding/base64"
2224 "fmt"
25+ "io"
2326 "os"
2427 "os/exec"
28+ "path"
2529 "path/filepath"
30+ "slices"
2631 "strings"
2732 "sync"
2833 "syscall"
@@ -32,6 +37,8 @@ import (
3237 "github.com/containerd/accelerated-container-image/pkg/label"
3338 "github.com/containerd/accelerated-container-image/pkg/snapshot/diskquota"
3439 "github.com/containerd/accelerated-container-image/pkg/tracing"
40+ "github.com/containerd/accelerated-container-image/pkg/utils"
41+ "github.com/containerd/accelerated-container-image/pkg/version"
3542 "go.opentelemetry.io/otel/attribute"
3643 "go.opentelemetry.io/otel/trace"
3744
@@ -40,14 +47,20 @@ import (
4047 "github.com/data-accelerator/zdfs"
4148 "github.com/sirupsen/logrus"
4249
43- "github.com/containerd/containerd/v2/core/mount"
50+ "github.com/containerd/containerd/v2/client"
51+ "github.com/containerd/containerd/v2/core/content"
4452 "github.com/containerd/containerd/v2/core/snapshots"
4553 "github.com/containerd/containerd/v2/core/snapshots/storage"
54+ "github.com/containerd/containerd/v2/pkg/namespaces"
4655 "github.com/containerd/continuity/fs"
4756 "github.com/containerd/errdefs"
4857 "github.com/containerd/log"
4958 "github.com/moby/locker"
5059 "github.com/pkg/errors"
60+
61+ "github.com/containerd/containerd/v2/core/diff"
62+ "github.com/containerd/containerd/v2/core/mount"
63+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
5164)
5265
5366// storageType is used to indicate that what kind of the snapshot it is.
@@ -215,8 +228,13 @@ type snapshotter struct {
215228 quotaSize string
216229}
217230
231+ type SnapshotterAndComparer interface {
232+ snapshots.Snapshotter
233+ diff.Comparer
234+ }
235+
218236// NewSnapshotter returns a Snapshotter which uses block device based on overlayFS.
219- func NewSnapshotter (bootConfig * BootConfig , opts ... Opt ) (snapshots. Snapshotter , error ) {
237+ func NewSnapshotter (bootConfig * BootConfig , opts ... Opt ) (SnapshotterAndComparer , error ) {
220238 config := defaultConfig
221239 for _ , opt := range opts {
222240 if err := opt (& config ); err != nil {
@@ -1198,6 +1216,261 @@ func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
11981216 return nil
11991217}
12001218
1219+ // Reverses a map, returning a map that maps value to key. This is lossy, in that if
1220+ // multiple keys map to the same value, the returned map will map that value to the
1221+ // last key in iteration order.
1222+ func ReverseMapLossy [K comparable , V comparable ](src map [K ]V ) map [V ]K {
1223+ reversed := make (map [V ]K )
1224+ for k , v := range src {
1225+ reversed [v ] = k
1226+ }
1227+ return reversed
1228+ }
1229+
1230+ type snapshotIdAndInfo struct {
1231+ snapshots.Info
1232+ Id string
1233+ }
1234+
1235+ // Returns a map of block device path (`/dev/XXX`) to snapshot Info.
1236+ func (o * snapshotter ) resolveBlockDevices (ctx context.Context ) (map [string ]* snapshotIdAndInfo , error ) {
1237+ ctx , t , err := o .ms .TransactionContext (ctx , false )
1238+ if err != nil {
1239+ return nil , err
1240+ }
1241+ defer t .Rollback ()
1242+
1243+ // Build a reverse-lookup map so that we can recover the ID from the snapshot Name
1244+ ids , err := storage .IDMap (ctx )
1245+ if err != nil {
1246+ return nil , err
1247+ }
1248+ keyToId := ReverseMapLossy (ids )
1249+
1250+ // Construct a lookup map of block device path to info by walking the snapshots
1251+ results := make (map [string ]* snapshotIdAndInfo )
1252+ storage .WalkInfo (ctx , func (ctx context.Context , info snapshots.Info ) error {
1253+ id , ok := keyToId [info .Name ]
1254+ if ! ok {
1255+ return nil
1256+ }
1257+ // Read the backstore mark file, which containts the `/dev/XXX` device path.
1258+ // If the read fails, this is likely not an overlaybd snapshot, so skip it.
1259+ blockDev , err := os .ReadFile (o .overlaybdBackstoreMarkFile (id ))
1260+ if err != nil {
1261+ return nil
1262+ }
1263+ results [strings .TrimSpace (string (blockDev ))] = & snapshotIdAndInfo {
1264+ Id : id ,
1265+ Info : info ,
1266+ }
1267+ return nil
1268+ })
1269+ return results , nil
1270+ }
1271+
1272+ func getBlockDeviceMount (m []mount.Mount , mode string ) (string , bool ) {
1273+ if len (m ) == 1 && strings .HasPrefix (m [0 ].Source , "/dev/" ) && slices .Contains (m [0 ].Options , mode ) {
1274+ return m [0 ].Source , true
1275+ }
1276+ return "" , false
1277+ }
1278+
1279+ // Validates that the lower and upper mounts are compatible with this Differ and returns the upper mount info if they are.
1280+ func (o * snapshotter ) validateDiffCompatibility (ctx context.Context , lower , upper []mount.Mount ) (* snapshotIdAndInfo , bool ) {
1281+ // Only handle requests whose upper mount is a RW block device.
1282+ upperDev , ok := getBlockDeviceMount (upper , "rw" )
1283+ if ! ok {
1284+ return nil , false
1285+ }
1286+
1287+ // Only handle requests whose lower mount is a RO block device.
1288+ lowerDev , ok := getBlockDeviceMount (lower , "ro" )
1289+ if ! ok {
1290+ return nil , false
1291+ }
1292+
1293+ // Create a block device path to info lookup map.
1294+ lookup , err := o .resolveBlockDevices (ctx )
1295+ if err != nil {
1296+ logrus .Errorln ("failed to create block device lookup map:" , err )
1297+ return nil , false
1298+ }
1299+
1300+ // Validate the upper device properties.
1301+ upperInfo , ok := lookup [upperDev ]
1302+ if ! ok {
1303+ logrus .Warnf ("upper dev %q is not managed by overlaybd" , upperDev )
1304+ return nil , false
1305+ }
1306+ if upperInfo .Kind != snapshots .KindActive {
1307+ logrus .Warnf ("upper snapshot %s must be active" , upperInfo .Name )
1308+ return nil , false
1309+ }
1310+
1311+ // Validate the lower device properties.
1312+ lowerInfo , ok := lookup [lowerDev ]
1313+ if ! ok {
1314+ logrus .Warnf ("lower dev %q is not managed by overlaybd" , lowerDev )
1315+ return nil , false
1316+ }
1317+ switch lowerInfo .Kind {
1318+ case snapshots .KindCommitted :
1319+ if lowerInfo .Name != upperInfo .Parent {
1320+ logrus .Warnf ("upper snapshot %s must be direct child of lower snapshot %s" , upperInfo .Name , lowerInfo .Name )
1321+ return nil , false
1322+ }
1323+ case snapshots .KindView :
1324+ // A View is a RO snapshot on top of a committed snapshot, so we compare parents to determine ancestry.
1325+ if lowerInfo .Parent != upperInfo .Parent {
1326+ logrus .Warnf ("upper snapshot %s must be direct child of lower snapshot %s" , upperInfo .Name , lowerInfo .Name )
1327+ return nil , false
1328+ }
1329+ default :
1330+ logrus .Warnf ("lower snapshot %s must be a RO layer" , lowerInfo .Name )
1331+ return nil , false
1332+ }
1333+
1334+ return upperInfo , true
1335+ }
1336+
1337+ const uncompressedOciLayerMediaType = "application/vnd.oci.image.layer.v1.tar"
1338+
1339+ func (o * snapshotter ) Compare (ctx context.Context , lower , upper []mount.Mount , opts ... diff.Opt ) (ocispec.Descriptor , error ) {
1340+ // Inflate our config from the passed in options.
1341+ var config diff.Config
1342+ for _ , opt := range opts {
1343+ if err := opt (& config ); err != nil {
1344+ return ocispec.Descriptor {}, err
1345+ }
1346+ }
1347+
1348+ logrus .Infoln ("Compare" )
1349+ logrus .Infof ("-> lower is %+v" , lower )
1350+ logrus .Infof ("-> upper is %+v" , upper )
1351+ logrus .Infof ("-> config is %+v" , config )
1352+
1353+ // MediaType must be uncompressed OCI.
1354+ if config .MediaType == "" {
1355+ config .MediaType = uncompressedOciLayerMediaType
1356+ } else if config .MediaType != uncompressedOciLayerMediaType {
1357+ return ocispec.Descriptor {}, errdefs .ErrNotImplemented
1358+ }
1359+
1360+ // If we are not given a reference ID, make our own.
1361+ newReference := false
1362+ if config .Reference == "" {
1363+ config .Reference = uniqueRef ()
1364+ newReference = true
1365+ }
1366+
1367+ // Make sure we can handle this Diff, as we only want to handle the case where
1368+ // the Diff is between an overlaybd writable layer and its parent.
1369+ upperInfo , compatible := o .validateDiffCompatibility (ctx , lower , upper )
1370+ if ! compatible {
1371+ // ErrNotImplemented instructs containerd to try the next Differ.
1372+ return ocispec.Descriptor {}, errdefs .ErrNotImplemented
1373+ }
1374+
1375+ // Create a temp directory to store the results of overlaybd-commit.
1376+ tempDir , err := os .MkdirTemp ("" , fmt .Sprintf ("%s-*" , upperInfo .Id ))
1377+ if err != nil {
1378+ logrus .Errorln ("failed to create tmpdir:" , err )
1379+ return ocispec.Descriptor {}, err
1380+ }
1381+ defer os .RemoveAll (tempDir )
1382+
1383+ // Call overlaybd-commit to create the overlaybd.commit file. Use zfile compression
1384+ // and wrap it in a TAR file for compatibility.
1385+ blockPath := o .blockPath (upperInfo .Id )
1386+ err = utils .Commit (ctx , blockPath , tempDir , false , "-z" , "-t" )
1387+ if err != nil {
1388+ logrus .Errorf ("failed to commit %q for snapshot %s: %v" , blockPath , upperInfo .Name , err )
1389+ return ocispec.Descriptor {}, err
1390+ }
1391+
1392+ overlaybdCommitFile := path .Join (tempDir , "overlaybd.commit" )
1393+ logrus .Infof ("overlaybd-commit of snapshot %s succeeded: %s" , upperInfo .Name , overlaybdCommitFile )
1394+
1395+ // Write the commit file into containerd's content store.
1396+
1397+ containerdClient , err := client .New ("/run/containerd/containerd.sock" )
1398+ if err != nil {
1399+ logrus .Errorln ("failed to create client to containerd:" , err )
1400+ return ocispec.Descriptor {}, err
1401+ }
1402+ defer containerdClient .Close ()
1403+
1404+ cs := containerdClient .ContentStore ()
1405+
1406+ // HACK: Hardcode the namespace, for some reason it is not propagated into our DiffService.
1407+ ctx = namespaces .WithNamespace (ctx , "k8s.io" )
1408+
1409+ // Grab a new content store writer using a unique ref to ensure it is new.
1410+ writer , err := cs .Writer (ctx , content .WithRef (config .Reference ), content .WithDescriptor (ocispec.Descriptor {MediaType : config .MediaType }))
1411+ if err != nil {
1412+ logrus .Errorln ("failed to get content store writer:" , err )
1413+ return ocispec.Descriptor {}, err
1414+ }
1415+ var errWrite error
1416+ defer func () {
1417+ writer .Close ()
1418+ // Only abort on an error and if we owned this reference.
1419+ if errWrite != nil && newReference {
1420+ if errAbort := cs .Abort (ctx , config .Reference ); errAbort != nil {
1421+ logrus .Errorf ("failed to abort content store write %q: %v" , config .Reference , errAbort )
1422+ }
1423+ }
1424+ }()
1425+
1426+ if ! newReference {
1427+ // If we are given a reference, make sure we rewind to the beginning.
1428+ if errWrite = writer .Truncate (0 ); errWrite != nil {
1429+ logrus .Errorf ("failed to truncate existing writer with ref %s: %v" , config .Reference , errWrite )
1430+ return ocispec.Descriptor {}, errWrite
1431+ }
1432+ }
1433+
1434+ // Write the commit file into the content store.
1435+ src , errWrite := os .Open (overlaybdCommitFile )
1436+ if errWrite != nil {
1437+ logrus .Errorf ("failed to open %q: %v" , overlaybdCommitFile , errWrite )
1438+ return ocispec.Descriptor {}, errWrite
1439+ }
1440+ defer src .Close ()
1441+ size , errWrite := io .Copy (writer , src )
1442+ if errWrite != nil {
1443+ logrus .Errorln ("failed to write overlaybd commit file to content store:" , errWrite )
1444+ return ocispec.Descriptor {}, errWrite
1445+ }
1446+ var commitopts []content.Opt
1447+ if config .Labels != nil {
1448+ commitopts = append (commitopts , content .WithLabels (config .Labels ))
1449+ }
1450+ if errWrite = writer .Commit (ctx , size , "" , commitopts ... ); errWrite != nil {
1451+ if ! errdefs .IsAlreadyExists (errWrite ) {
1452+ logrus .Errorln ("failed to commit overlaybd layer to content store:" , errWrite )
1453+ return ocispec.Descriptor {}, errWrite
1454+ }
1455+ errWrite = nil
1456+ }
1457+
1458+ digest := writer .Digest ()
1459+ desc := ocispec.Descriptor {
1460+ MediaType : config .MediaType ,
1461+ Digest : digest ,
1462+ Size : size ,
1463+ Annotations : map [string ]string {
1464+ label .OverlayBDVersion : version .OverlayBDVersionNumber ,
1465+ label .OverlayBDBlobDigest : digest .String (),
1466+ label .OverlayBDBlobSize : fmt .Sprintf ("%d" , size ),
1467+ label .OverlayBDBlobFsType : "ext4" ,
1468+ },
1469+ }
1470+ logrus .Infof ("Diff created with spec %+v" , desc )
1471+ return desc , nil
1472+ }
1473+
12011474// Walk the snapshots.
12021475func (o * snapshotter ) Walk (ctx context.Context , fn snapshots.WalkFunc , fs ... string ) (err error ) {
12031476 log .G (ctx ).Infof ("Walk (fs: %s)" , fs )
@@ -1746,3 +2019,11 @@ func isOverlaybdFileHeader(header []byte) bool {
17462019
17472020 return zfileFormat || lsmtFormat
17482021}
2022+
2023+ func uniqueRef () string {
2024+ t := time .Now ()
2025+ var b [3 ]byte
2026+ // Ignore read failures, just decreases uniqueness
2027+ rand .Read (b [:])
2028+ return fmt .Sprintf ("%d-%s" , t .UnixNano (), base64 .URLEncoding .EncodeToString (b [:]))
2029+ }
0 commit comments