@@ -19,6 +19,8 @@ package containerd
1919import (
2020 "context"
2121 "fmt"
22+ "strings"
23+ "sync/atomic"
2224
2325 "github.com/containerd/containerd/content"
2426 "github.com/containerd/containerd/errdefs"
@@ -29,6 +31,7 @@ import (
2931 "github.com/opencontainers/image-spec/identity"
3032 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3133 "github.com/pkg/errors"
34+ "golang.org/x/sync/semaphore"
3235)
3336
3437// Image describes an image used by containers
@@ -45,6 +48,8 @@ type Image interface {
4548 RootFS (ctx context.Context ) ([]digest.Digest , error )
4649 // Size returns the total size of the image's packed resources.
4750 Size (ctx context.Context ) (int64 , error )
51+ // Usage returns a usage calculation for the image.
52+ Usage (context.Context , ... UsageOpt ) (int64 , error )
4853 // Config descriptor for the image.
4954 Config (ctx context.Context ) (ocispec.Descriptor , error )
5055 // IsUnpacked returns whether or not an image is unpacked.
@@ -53,6 +58,49 @@ type Image interface {
5358 ContentStore () content.Store
5459}
5560
61+ type usageOptions struct {
62+ manifestLimit * int
63+ manifestOnly bool
64+ snapshots bool
65+ }
66+
67+ // UsageOpt is used to configure the usage calculation
68+ type UsageOpt func (* usageOptions ) error
69+
70+ // WithUsageManifestLimit sets the limit to the number of manifests which will
71+ // be walked for usage. Setting this value to 0 will require all manifests to
72+ // be walked, returning ErrNotFound if manifests are missing.
73+ // NOTE: By default all manifests which exist will be walked
74+ // and any non-existent manifests and their subobjects will be ignored.
75+ func WithUsageManifestLimit (i int ) UsageOpt {
76+ // If 0 then don't filter any manifests
77+ // By default limits to current platform
78+ return func (o * usageOptions ) error {
79+ o .manifestLimit = & i
80+ return nil
81+ }
82+ }
83+
84+ // WithSnapshotUsage will check for referenced snapshots from the image objects
85+ // and include the snapshot size in the total usage.
86+ func WithSnapshotUsage () UsageOpt {
87+ return func (o * usageOptions ) error {
88+ o .snapshots = true
89+ return nil
90+ }
91+ }
92+
93+ // WithManifestUsage is used to get the usage for an image based on what is
94+ // reported by the manifests rather than what exists in the content store.
95+ // NOTE: This function is best used with the manifest limit set to get a
96+ // consistent value, otherwise non-existent manifests will be excluded.
97+ func WithManifestUsage () UsageOpt {
98+ return func (o * usageOptions ) error {
99+ o .manifestOnly = true
100+ return nil
101+ }
102+ }
103+
56104var _ = (Image )(& image {})
57105
58106// NewImage returns a client image object from the metadata image
@@ -98,8 +146,95 @@ func (i *image) RootFS(ctx context.Context) ([]digest.Digest, error) {
98146}
99147
100148func (i * image ) Size (ctx context.Context ) (int64 , error ) {
101- provider := i .client .ContentStore ()
102- return i .i .Size (ctx , provider , i .platform )
149+ return i .Usage (ctx , WithUsageManifestLimit (1 ), WithManifestUsage ())
150+ }
151+
152+ func (i * image ) Usage (ctx context.Context , opts ... UsageOpt ) (int64 , error ) {
153+ var config usageOptions
154+ for _ , opt := range opts {
155+ if err := opt (& config ); err != nil {
156+ return 0 , err
157+ }
158+ }
159+
160+ var (
161+ provider = i .client .ContentStore ()
162+ handler = images .ChildrenHandler (provider )
163+ size int64
164+ mustExist bool
165+ )
166+
167+ if config .manifestLimit != nil {
168+ handler = images .LimitManifests (handler , i .platform , * config .manifestLimit )
169+ mustExist = true
170+ }
171+
172+ var wh images.HandlerFunc = func (ctx context.Context , desc ocispec.Descriptor ) ([]ocispec.Descriptor , error ) {
173+ var usage int64
174+ children , err := handler (ctx , desc )
175+ if err != nil {
176+ if ! errdefs .IsNotFound (err ) || mustExist {
177+ return nil , err
178+ }
179+ if ! config .manifestOnly {
180+ // Do not count size of non-existent objects
181+ desc .Size = 0
182+ }
183+ } else if config .snapshots || ! config .manifestOnly {
184+ info , err := provider .Info (ctx , desc .Digest )
185+ if err != nil {
186+ if ! errdefs .IsNotFound (err ) {
187+ return nil , err
188+ }
189+ if ! config .manifestOnly {
190+ // Do not count size of non-existent objects
191+ desc .Size = 0
192+ }
193+ } else if info .Size > desc .Size {
194+ // Count actual usage, Size may be unset or -1
195+ desc .Size = info .Size
196+ }
197+
198+ for k , v := range info .Labels {
199+ const prefix = "containerd.io/gc.ref.snapshot."
200+ if ! strings .HasPrefix (k , prefix ) {
201+ continue
202+ }
203+
204+ sn := i .client .SnapshotService (k [len (prefix ):])
205+ if sn == nil {
206+ continue
207+ }
208+
209+ u , err := sn .Usage (ctx , v )
210+ if err != nil {
211+ if ! errdefs .IsNotFound (err ) && ! errdefs .IsInvalidArgument (err ) {
212+ return nil , err
213+ }
214+ } else {
215+ usage += u .Size
216+ }
217+ }
218+ }
219+
220+ // Ignore unknown sizes. Generally unknown sizes should
221+ // never be set in manifests, however, the usage
222+ // calculation does not need to enforce this.
223+ if desc .Size >= 0 {
224+ usage += desc .Size
225+ }
226+
227+ atomic .AddInt64 (& size , usage )
228+
229+ return children , nil
230+ }
231+
232+ l := semaphore .NewWeighted (3 )
233+ if err := images .Dispatch (ctx , wh , l , i .i .Target ); err != nil {
234+ return 0 , err
235+ }
236+
237+ return size , nil
103238}
104239
105240func (i * image ) Config (ctx context.Context ) (ocispec.Descriptor , error ) {
0 commit comments