diff --git a/fs/layer/layer.go b/fs/layer/layer.go index 0397f2e9e..b88e2ec46 100644 --- a/fs/layer/layer.go +++ b/fs/layer/layer.go @@ -612,7 +612,7 @@ func (l *layer) RootNode(baseInode uint32) (fusefs.InodeEmbedder, error) { if l.r == nil { return nil, fmt.Errorf("layer hasn't been verified yet") } - return newNode(l.desc.Digest, l.r, l.blob, baseInode, l.resolver.overlayOpaqueType, l.passThrough) + return newNode(l.desc.Digest, l.r, l.blob, baseInode, l.resolver.overlayOpaqueType, l.passThrough, l.resolver.rootDir) } func (l *layer) ReadAt(p []byte, offset int64, opts ...remote.Option) (int, error) { diff --git a/fs/layer/node.go b/fs/layer/node.go index e33bee9ba..ece5d4fed 100644 --- a/fs/layer/node.go +++ b/fs/layer/node.go @@ -76,7 +76,8 @@ var opaqueXattrs = map[OverlayOpaqueType][]string{ OverlayOpaqueUser: {"user.overlay.opaque"}, } -func newNode(layerDgst digest.Digest, r reader.Reader, blob remote.Blob, baseInode uint32, opaque OverlayOpaqueType, pth passThroughConfig) (fusefs.InodeEmbedder, error) { +func newNode(layerDgst digest.Digest, r reader.Reader, blob remote.Blob, baseInode uint32, opaque OverlayOpaqueType, + pth passThroughConfig, statfsBase string) (fusefs.InodeEmbedder, error) { rootID := r.Metadata().RootID() rootAttr, err := r.Metadata().GetAttr(rootID) if err != nil { @@ -93,6 +94,7 @@ func newNode(layerDgst digest.Digest, r reader.Reader, blob remote.Blob, baseIno rootID: rootID, opaqueXattrs: opq, passThrough: pth, + statfsBase: statfsBase, } ffs.s = ffs.newState(layerDgst, blob) return &node{ @@ -111,6 +113,7 @@ type fs struct { rootID uint32 opaqueXattrs []string passThrough passThroughConfig + statfsBase string } func (fs *fs) inodeOfState() uint64 { @@ -434,7 +437,7 @@ func (n *node) Readlink(ctx context.Context) ([]byte, syscall.Errno) { var _ = (fusefs.NodeStatfser)((*node)(nil)) func (n *node) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { - defaultStatfs(out) + n.fs.fillStatfs(out) return 0 } @@ -507,7 +510,7 @@ func (w *whiteout) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse.A var _ = (fusefs.NodeStatfser)((*whiteout)(nil)) func (w *whiteout) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { - defaultStatfs(out) + w.fs.fillStatfs(out) return 0 } @@ -573,7 +576,7 @@ func (s *state) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse.Attr var _ = (fusefs.NodeStatfser)((*state)(nil)) func (s *state) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { - defaultStatfs(out) + s.fs.fillStatfs(out) return 0 } @@ -635,7 +638,7 @@ func (sf *statFile) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse. var _ = (fusefs.NodeStatfser)((*statFile)(nil)) func (sf *statFile) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { - defaultStatfs(out) + sf.fs.fillStatfs(out) return 0 } @@ -837,3 +840,23 @@ func defaultStatfs(stat *fuse.StatfsOut) { stat.Padding = 0 stat.Spare = [6]uint32{} } + +func (fs *fs) fillStatfs(out *fuse.StatfsOut) { + if fs.statfsBase != "" { + var s unix.Statfs_t + if err := unix.Statfs(fs.statfsBase, &s); err == nil { + out.Blocks = s.Blocks + out.Bfree = s.Bfree + out.Bavail = s.Bavail + out.Files = s.Files + out.Ffree = s.Ffree + out.Bsize = uint32(s.Bsize) + out.Frsize = uint32(s.Frsize) + out.NameLen = 255 + out.Padding = 0 + out.Spare = [6]uint32{} + return + } + } + defaultStatfs(out) +} diff --git a/fs/layer/testutil.go b/fs/layer/testutil.go index ac2356af7..aa0e68416 100644 --- a/fs/layer/testutil.go +++ b/fs/layer/testutil.go @@ -137,6 +137,7 @@ func TestSuiteLayer(t *TestRunner, store metadata.Store) { testPrefetch(t, store, lc) testNodeRead(t, store, lc) testNodes(t, store, lc) + testStatfs(t, store, lc) } } @@ -814,7 +815,7 @@ func getRootNode(t TestingT, r metadata.Reader, opaque OverlayOpaqueType, tocDgs if err != nil { t.Fatalf("failed to verify reader: %v", err) } - rootNode, err := newNode(testStateLayerDigest, rr, &testBlobState{10, 5}, 100, opaque, lc.passThroughConfig) + rootNode, err := newNode(testStateLayerDigest, rr, &testBlobState{10, 5}, 100, opaque, lc.passThroughConfig, "") if err != nil { t.Fatalf("failed to get root node: %v", err) } @@ -822,6 +823,64 @@ func getRootNode(t TestingT, r metadata.Reader, opaque OverlayOpaqueType, tocDgs return rootNode.(*node) } +func getRootNodeWithStatfsBase(t TestingT, r metadata.Reader, opaque OverlayOpaqueType, tocDgst digest.Digest, cc cache.BlobCache, lc layerConfig, statfsBase string) *node { + vr, err := reader.NewReader(r, cc, digest.FromString("")) + if err != nil { + t.Fatalf("failed to create reader: %v", err) + } + rr, err := vr.VerifyTOC(tocDgst) + if err != nil { + t.Fatalf("failed to verify reader: %v", err) + } + rootNode, err := newNode(testStateLayerDigest, rr, &testBlobState{10, 5}, 100, opaque, lc.passThroughConfig, statfsBase) + if err != nil { + t.Fatalf("failed to get root node: %v", err) + } + fusefs.NewNodeFS(rootNode, &fusefs.Options{}) + return rootNode.(*node) +} + +func testStatfs(t *TestRunner, factory metadata.Store, lc layerConfig) { + // Build a minimal layer + sr, tocDgst, err := tutil.BuildEStargz( + []tutil.TarEntry{tutil.File("foo.txt", sampleData1)}, + tutil.WithEStargzOptions(estargz.WithCompression(tutil.ZstdCompressionWithLevel(zstd.SpeedFastest)())), + ) + if err != nil { + t.Fatalf("failed to build eStargz: %v", err) + } + r, err := factory(sr, metadata.WithDecompressors(tutil.ZstdCompressionWithLevel(zstd.SpeedFastest)())) + if err != nil { + t.Fatalf("failed to create metadata reader: %v", err) + } + defer r.Close() + cc := cache.NewMemoryCache() + + // Case1: empty statfsBase -> zero stats + rootEmpty := getRootNode(t, r, OverlayOpaqueAll, tocDgst, cc, lc) + var out fuse.StatfsOut + if errno := rootEmpty.Operations().(fusefs.NodeStatfser).Statfs(context.Background(), &out); errno != 0 { + t.Fatalf("Statfs failed: %v", errno) + } + if out.Blocks != 0 || out.Files != 0 { + t.Fatalf("expected zero stats for empty base, got Blocks=%d Files=%d", out.Blocks, out.Files) + } + + // Case2: valid statfsBase -> non-zero stats + dir, err := os.MkdirTemp("", "statfsbase") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + rootWith := getRootNodeWithStatfsBase(t, r, OverlayOpaqueAll, tocDgst, cc, lc, dir) + var out2 fuse.StatfsOut + if errno := rootWith.Operations().(fusefs.NodeStatfser).Statfs(context.Background(), &out2); errno != 0 { + t.Fatalf("Statfs with base failed: %v", errno) + } + if out2.Blocks == 0 || out2.Bsize == 0 { + t.Fatalf("expected non-zero stats for valid base, got Blocks=%d Bsize=%d", out2.Blocks, out2.Bsize) + } +} + type testBlobState struct { size int64 fetchedSize int64