Skip to content

Commit 75771c4

Browse files
committed
Add usage function to client
The usage function allows more configurable and accurate calculations of the usage for an image. Signed-off-by: Derek McGowan <[email protected]>
1 parent 225cc7d commit 75771c4

2 files changed

Lines changed: 235 additions & 2 deletions

File tree

image.go

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package containerd
1919
import (
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+
56104
var _ = (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

100148
func (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

105240
func (i *image) Config(ctx context.Context) (ocispec.Descriptor, error) {

image_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,101 @@ func TestImagePullWithDistSourceLabel(t *testing.T) {
135135
t.Fatal(err)
136136
}
137137
}
138+
139+
func TestImageUsage(t *testing.T) {
140+
if testing.Short() || runtime.GOOS == "windows" {
141+
t.Skip()
142+
}
143+
144+
imageName := "docker.io/library/busybox:latest"
145+
ctx, cancel := testContext(t)
146+
defer cancel()
147+
148+
client, err := newClient(t, address)
149+
if err != nil {
150+
t.Fatal(err)
151+
}
152+
defer client.Close()
153+
154+
// Cleanup
155+
err = client.ImageService().Delete(ctx, imageName)
156+
if err != nil && !errdefs.IsNotFound(err) {
157+
t.Fatal(err)
158+
}
159+
160+
testPlatform := platforms.Only(ocispec.Platform{
161+
OS: "linux",
162+
Architecture: "amd64",
163+
})
164+
165+
// Pull single platform, do not unpack
166+
image, err := client.Pull(ctx, imageName, WithPlatformMatcher(testPlatform))
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
171+
s1, err := image.Usage(ctx, WithUsageManifestLimit(1))
172+
if err != nil {
173+
t.Fatal(err)
174+
}
175+
176+
if _, err := image.Usage(ctx, WithUsageManifestLimit(0), WithManifestUsage()); err == nil {
177+
t.Fatal("expected NotFound with missing manifests")
178+
} else if !errdefs.IsNotFound(err) {
179+
t.Fatalf("unexpected error: %+v", err)
180+
}
181+
182+
// Pin image name to specific version for future fetches
183+
imageName = imageName + "@" + image.Target().Digest.String()
184+
185+
// Fetch single platforms, but all manifests pulled
186+
if _, err := client.Fetch(ctx, imageName, WithPlatformMatcher(testPlatform)); err != nil {
187+
t.Fatal(err)
188+
}
189+
190+
if s, err := image.Usage(ctx, WithUsageManifestLimit(1)); err != nil {
191+
t.Fatal(err)
192+
} else if s != s1 {
193+
t.Fatalf("unexpected usage %d, expected %d", s, s1)
194+
}
195+
196+
s2, err := image.Usage(ctx, WithUsageManifestLimit(0))
197+
if err != nil {
198+
t.Fatal(err)
199+
}
200+
201+
if s2 <= s1 {
202+
t.Fatalf("Expected larger usage counting all manifests: %d <= %d", s2, s1)
203+
}
204+
205+
s3, err := image.Usage(ctx, WithUsageManifestLimit(0), WithManifestUsage())
206+
if err != nil {
207+
t.Fatal(err)
208+
}
209+
210+
if s3 <= s2 {
211+
t.Fatalf("Expected larger usage counting all manifest reported sizes: %d <= %d", s3, s2)
212+
}
213+
214+
// Fetch everything
215+
if _, err = client.Fetch(ctx, imageName); err != nil {
216+
t.Fatal(err)
217+
}
218+
219+
if s, err := image.Usage(ctx); err != nil {
220+
t.Fatal(err)
221+
} else if s != s3 {
222+
t.Fatalf("Expected actual usage to equal manifest reported usage of %d: got %d", s3, s)
223+
}
224+
225+
err = image.Unpack(ctx, DefaultSnapshotter)
226+
if err != nil {
227+
t.Fatal(err)
228+
}
229+
230+
if s, err := image.Usage(ctx, WithSnapshotUsage()); err != nil {
231+
t.Fatal(err)
232+
} else if s <= s3 {
233+
t.Fatalf("Expected actual usage with snapshots to be greater: %d <= %d", s, s3)
234+
}
235+
}

0 commit comments

Comments
 (0)