Browse code

Add --prune-over-size-limit flag for pruning images

Maciej Szulik authored on 2016/07/01 19:56:10
Showing 5 changed files
... ...
@@ -45,14 +45,28 @@ images.`
45 45
   %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m
46 46
 
47 47
   # To actually perform the prune operation, the confirm flag must be appended
48
-  %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m --confirm`
48
+  %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m --confirm
49
+
50
+  # See, what the prune command would delete if we're interested in removing images
51
+  # exceeding currently set LimitRanges ('openshift.io/Image')
52
+  %[1]s %[2]s --prune-over-size-limit
53
+
54
+  # To actually perform the prune operation, the confirm flag must be appended
55
+  %[1]s %[2]s --prune-over-size-limit --confirm`
56
+)
57
+
58
+var (
59
+	defaultKeepYoungerThan         = 60 * time.Minute
60
+	defaultKeepTagRevisions        = 3
61
+	defaultPruneImageOverSizeLimit = false
49 62
 )
50 63
 
51 64
 // PruneImagesOptions holds all the required options for pruning images.
52 65
 type PruneImagesOptions struct {
53 66
 	Confirm             bool
54
-	KeepYoungerThan     time.Duration
55
-	KeepTagRevisions    int
67
+	KeepYoungerThan     *time.Duration
68
+	KeepTagRevisions    *int
69
+	PruneOverSizeLimit  *bool
56 70
 	CABundle            string
57 71
 	RegistryUrlOverride string
58 72
 
... ...
@@ -64,9 +78,10 @@ type PruneImagesOptions struct {
64 64
 // NewCmdPruneImages implements the OpenShift cli prune images command.
65 65
 func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
66 66
 	opts := &PruneImagesOptions{
67
-		Confirm:          false,
68
-		KeepYoungerThan:  60 * time.Minute,
69
-		KeepTagRevisions: 3,
67
+		Confirm:            false,
68
+		KeepYoungerThan:    &defaultKeepYoungerThan,
69
+		KeepTagRevisions:   &defaultKeepTagRevisions,
70
+		PruneOverSizeLimit: &defaultPruneImageOverSizeLimit,
70 71
 	}
71 72
 
72 73
 	cmd := &cobra.Command{
... ...
@@ -84,8 +99,9 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
84 84
 	}
85 85
 
86 86
 	cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "Specify that image pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
87
-	cmd.Flags().DurationVar(&opts.KeepYoungerThan, "keep-younger-than", opts.KeepYoungerThan, "Specify the minimum age of an image for it to be considered a candidate for pruning.")
88
-	cmd.Flags().IntVar(&opts.KeepTagRevisions, "keep-tag-revisions", opts.KeepTagRevisions, "Specify the number of image revisions for a tag in an image stream that will be preserved.")
87
+	cmd.Flags().DurationVar(opts.KeepYoungerThan, "keep-younger-than", *opts.KeepYoungerThan, "Specify the minimum age of an image for it to be considered a candidate for pruning.")
88
+	cmd.Flags().IntVar(opts.KeepTagRevisions, "keep-tag-revisions", *opts.KeepTagRevisions, "Specify the number of image revisions for a tag in an image stream that will be preserved.")
89
+	cmd.Flags().BoolVar(opts.PruneOverSizeLimit, "prune-over-size-limit", *opts.PruneOverSizeLimit, "Specify if images which are exceeding LimitRanges (see 'openshift.io/Image'), specified in the same namespace, should be considered for pruning. This flag cannot be combined with --keep-younger-than nor --keep-tag-revisions.")
89 90
 	cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file.")
90 91
 	cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works.")
91 92
 
... ...
@@ -99,6 +115,16 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
99 99
 		return kcmdutil.UsageError(cmd, "no arguments are allowed to this command")
100 100
 	}
101 101
 
102
+	if !cmd.Flags().Lookup("keep-younger-than").Changed {
103
+		o.KeepYoungerThan = nil
104
+	}
105
+	if !cmd.Flags().Lookup("keep-tag-revisions").Changed {
106
+		o.KeepTagRevisions = nil
107
+	}
108
+	if !cmd.Flags().Lookup("prune-over-size-limit").Changed {
109
+		o.PruneOverSizeLimit = nil
110
+	}
111
+
102 112
 	o.Out = out
103 113
 
104 114
 	osClient, kClient, registryClient, err := getClients(f, o.CABundle)
... ...
@@ -146,19 +172,36 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
146 146
 		return err
147 147
 	}
148 148
 
149
+	limitRangesList, err := kClient.LimitRanges(kapi.NamespaceAll).List(kapi.ListOptions{})
150
+	if err != nil {
151
+		return err
152
+	}
153
+	limitRangesMap := make(map[string][]*kapi.LimitRange)
154
+	for i := range limitRangesList.Items {
155
+		limit := limitRangesList.Items[i]
156
+		limits, ok := limitRangesMap[limit.Namespace]
157
+		if !ok {
158
+			limits = []*kapi.LimitRange{}
159
+		}
160
+		limits = append(limits, &limit)
161
+		limitRangesMap[limit.Namespace] = limits
162
+	}
163
+
149 164
 	options := prune.PrunerOptions{
150
-		KeepYoungerThan:  o.KeepYoungerThan,
151
-		KeepTagRevisions: o.KeepTagRevisions,
152
-		Images:           allImages,
153
-		Streams:          allStreams,
154
-		Pods:             allPods,
155
-		RCs:              allRCs,
156
-		BCs:              allBCs,
157
-		Builds:           allBuilds,
158
-		DCs:              allDCs,
159
-		DryRun:           o.Confirm == false,
160
-		RegistryClient:   registryClient,
161
-		RegistryURL:      o.RegistryUrlOverride,
165
+		KeepYoungerThan:    o.KeepYoungerThan,
166
+		KeepTagRevisions:   o.KeepTagRevisions,
167
+		PruneOverSizeLimit: o.PruneOverSizeLimit,
168
+		Images:             allImages,
169
+		Streams:            allStreams,
170
+		Pods:               allPods,
171
+		RCs:                allRCs,
172
+		BCs:                allBCs,
173
+		Builds:             allBuilds,
174
+		DCs:                allDCs,
175
+		LimitRanges:        limitRangesMap,
176
+		DryRun:             o.Confirm == false,
177
+		RegistryClient:     registryClient,
178
+		RegistryURL:        o.RegistryUrlOverride,
162 179
 	}
163 180
 
164 181
 	o.Pruner = prune.NewPruner(options)
... ...
@@ -168,10 +211,13 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
168 168
 
169 169
 // Validate ensures that a PruneImagesOptions is valid and can be used to execute pruning.
170 170
 func (o PruneImagesOptions) Validate() error {
171
-	if o.KeepYoungerThan < 0 {
171
+	if o.PruneOverSizeLimit != nil && (o.KeepYoungerThan != nil || o.KeepTagRevisions != nil) {
172
+		return fmt.Errorf("--prune-over-size-limit cannot be specified with --keep-tag-revisions nor --keep-younger-than")
173
+	}
174
+	if o.KeepYoungerThan != nil && *o.KeepYoungerThan < 0 {
172 175
 		return fmt.Errorf("--keep-younger-than must be greater than or equal to 0")
173 176
 	}
174
-	if o.KeepTagRevisions < 0 {
177
+	if o.KeepTagRevisions != nil && *o.KeepTagRevisions < 0 {
175 178
 		return fmt.Errorf("--keep-tag-revisions must be greater than or equal to 0")
176 179
 	}
177 180
 	if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
... ...
@@ -422,6 +422,7 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
422 422
 			},
423 423
 			Rules: []authorizationapi.PolicyRule{
424 424
 				authorizationapi.NewRule("get", "list").Groups(kapiGroup).Resources("pods", "replicationcontrollers").RuleOrDie(),
425
+				authorizationapi.NewRule("list").Groups(kapiGroup).Resources("limitranges").RuleOrDie(),
425 426
 				authorizationapi.NewRule("get", "list").Groups(buildGroup).Resources("buildconfigs", "builds").RuleOrDie(),
426 427
 				authorizationapi.NewRule("get", "list").Groups(deployGroup).Resources("deploymentconfigs").RuleOrDie(),
427 428
 
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	gonum "github.com/gonum/graph"
12 12
 
13 13
 	kapi "k8s.io/kubernetes/pkg/api"
14
+	"k8s.io/kubernetes/pkg/api/resource"
14 15
 	"k8s.io/kubernetes/pkg/api/unversioned"
15 16
 	kerrors "k8s.io/kubernetes/pkg/util/errors"
16 17
 	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
... ...
@@ -47,8 +48,9 @@ const (
47 47
 // pruneAlgorithm contains the various settings to use when evaluating images
48 48
 // and layers for pruning.
49 49
 type pruneAlgorithm struct {
50
-	keepYoungerThan  time.Duration
51
-	keepTagRevisions int
50
+	keepYoungerThan    time.Duration
51
+	keepTagRevisions   int
52
+	pruneOverSizeLimit bool
52 53
 }
53 54
 
54 55
 // ImageDeleter knows how to remove images from OpenShift.
... ...
@@ -90,10 +92,13 @@ type ManifestDeleter interface {
90 90
 type PrunerOptions struct {
91 91
 	// KeepYoungerThan indicates the minimum age an Image must be to be a
92 92
 	// candidate for pruning.
93
-	KeepYoungerThan time.Duration
93
+	KeepYoungerThan *time.Duration
94 94
 	// KeepTagRevisions is the minimum number of tag revisions to preserve;
95 95
 	// revisions older than this value are candidates for pruning.
96
-	KeepTagRevisions int
96
+	KeepTagRevisions *int
97
+	// PruneOverSizeLimit indicates that images exceeding defined limits (openshift.io/Image)
98
+	// will be considered as candidates for pruning.
99
+	PruneOverSizeLimit *bool
97 100
 	// Images is the entire list of images in OpenShift. An image must be in this
98 101
 	// list to be a candidate for pruning.
99 102
 	Images *imageapi.ImageList
... ...
@@ -113,6 +118,8 @@ type PrunerOptions struct {
113 113
 	// DCs is the entire list of deployment configs across all namespaces in the
114 114
 	// cluster.
115 115
 	DCs *deployapi.DeploymentConfigList
116
+	// LimitRanges is a map of LimitRanges across namespaces, being keys in this map.
117
+	LimitRanges map[string][]*kapi.LimitRange
116 118
 	// DryRun indicates that no changes will be made to the cluster and nothing
117 119
 	// will be removed.
118 120
 	DryRun bool
... ...
@@ -203,6 +210,10 @@ func (*dryRunRegistryPinger) ping(registry string) error {
203 203
 // status.tags that are preserved and ineligible for pruning. Any revision older
204 204
 // than keepTagRevisions is eligible for pruning.
205 205
 //
206
+// pruneOverSizeLimit is a boolean flag speyfing that all images exceeding limits
207
+// defined in their namespace will be considered for pruning. Important to note is
208
+// the fact that this flag does not work in any combination with the keep* flags.
209
+//
206 210
 // images, streams, pods, rcs, bcs, builds, and dcs are the resources used to run
207 211
 // the pruning algorithm. These should be the full list for each type from the
208 212
 // cluster; otherwise, the pruning algorithm might result in incorrect
... ...
@@ -230,15 +241,22 @@ func (*dryRunRegistryPinger) ping(registry string) error {
230 230
 func NewPruner(options PrunerOptions) Pruner {
231 231
 	g := graph.New()
232 232
 
233
-	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%d", options.KeepYoungerThan, options.KeepTagRevisions)
233
+	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%v, pruneOverSizeLimit=%v",
234
+		options.KeepYoungerThan, options.KeepTagRevisions, options.PruneOverSizeLimit)
234 235
 
235
-	algorithm := pruneAlgorithm{
236
-		keepYoungerThan:  options.KeepYoungerThan,
237
-		keepTagRevisions: options.KeepTagRevisions,
236
+	algorithm := pruneAlgorithm{}
237
+	if options.KeepYoungerThan != nil {
238
+		algorithm.keepYoungerThan = *options.KeepYoungerThan
239
+	}
240
+	if options.KeepTagRevisions != nil {
241
+		algorithm.keepTagRevisions = *options.KeepTagRevisions
242
+	}
243
+	if options.PruneOverSizeLimit != nil {
244
+		algorithm.pruneOverSizeLimit = *options.PruneOverSizeLimit
238 245
 	}
239 246
 
240 247
 	addImagesToGraph(g, options.Images, algorithm)
241
-	addImageStreamsToGraph(g, options.Streams, algorithm)
248
+	addImageStreamsToGraph(g, options.Streams, options.LimitRanges, algorithm)
242 249
 	addPodsToGraph(g, options.Pods, algorithm)
243 250
 	addReplicationControllersToGraph(g, options.RCs)
244 251
 	addBuildConfigsToGraph(g, options.BCs)
... ...
@@ -281,7 +299,7 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune
281 281
 		}
282 282
 
283 283
 		age := unversioned.Now().Sub(image.CreationTimestamp.Time)
284
-		if age < algorithm.keepYoungerThan {
284
+		if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
285 285
 			glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%v)", image.Name, age)
286 286
 			continue
287 287
 		}
... ...
@@ -306,13 +324,17 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune
306 306
 // addImageStreamsToGraph adds all the streams to the graph. The most recent n
307 307
 // image revisions for a tag will be preserved, where n is specified by the
308 308
 // algorithm's keepTagRevisions. Image revisions older than n are candidates
309
-// for pruning.  if the image stream's age is at least as old as the minimum
309
+// for pruning if the image stream's age is at least as old as the minimum
310 310
 // threshold in algorithm.  Otherwise, if the image stream is younger than the
311 311
 // threshold, all image revisions for that stream are ineligible for pruning.
312
+// If pruneOverSizeLimit flag is set to true, above does not matter, instead
313
+// all images size is checked against LimitRanges defined in that same namespace,
314
+// and whenever its size exceeds the smallest limit in that namespace, it will be
315
+// considered a candidate for pruning.
312 316
 //
313 317
 // addImageStreamsToGraph also adds references from each stream to all the
314 318
 // layers it references (via each image a stream references).
315
-func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, algorithm pruneAlgorithm) {
319
+func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, limits map[string][]*kapi.LimitRange, algorithm pruneAlgorithm) {
316 320
 	for i := range streams.Items {
317 321
 		stream := &streams.Items[i]
318 322
 
... ...
@@ -322,9 +344,8 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al
322 322
 		oldImageRevisionReferenceKind := WeakReferencedImageEdgeKind
323 323
 
324 324
 		age := unversioned.Now().Sub(stream.CreationTimestamp.Time)
325
-		if age < algorithm.keepYoungerThan {
325
+		if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
326 326
 			// stream's age is below threshold - use a strong reference for old image revisions instead
327
-			glog.V(4).Infof("Stream %s/%s is below age threshold - none of its images are eligible for pruning", stream.Namespace, stream.Name)
328 327
 			oldImageRevisionReferenceKind = ReferencedImageEdgeKind
329 328
 		}
330 329
 
... ...
@@ -341,12 +362,17 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al
341 341
 				}
342 342
 				imageNode := n.(*imagegraph.ImageNode)
343 343
 
344
-				var kind string
345
-				switch {
346
-				case i < algorithm.keepTagRevisions:
347
-					kind = ReferencedImageEdgeKind
348
-				default:
349
-					kind = oldImageRevisionReferenceKind
344
+				kind := oldImageRevisionReferenceKind
345
+				if algorithm.pruneOverSizeLimit {
346
+					if exceedsLimits(stream, imageNode.Image, limits) {
347
+						kind = WeakReferencedImageEdgeKind
348
+					} else {
349
+						kind = ReferencedImageEdgeKind
350
+					}
351
+				} else {
352
+					if i < algorithm.keepTagRevisions {
353
+						kind = ReferencedImageEdgeKind
354
+					}
350 355
 				}
351 356
 
352 357
 				glog.V(4).Infof("Checking for existing strong reference from stream %s/%s to image %s", stream.Namespace, stream.Name, imageNode.Image.Name)
... ...
@@ -372,6 +398,41 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al
372 372
 	}
373 373
 }
374 374
 
375
+// exceedsLimits checks if given image exceeds LimitRanges defined in ImageStream's namespace.
376
+func exceedsLimits(is *imageapi.ImageStream, image *imageapi.Image, limits map[string][]*kapi.LimitRange) bool {
377
+	limitRanges, ok := limits[is.Namespace]
378
+	if !ok {
379
+		return false
380
+	}
381
+	if len(limitRanges) == 0 {
382
+		return false
383
+	}
384
+
385
+	imageSize := resource.NewQuantity(image.DockerImageMetadata.Size, resource.BinarySI)
386
+	for _, limitRange := range limitRanges {
387
+		if limitRange == nil {
388
+			continue
389
+		}
390
+		for _, limit := range limitRange.Spec.Limits {
391
+			if limit.Type != imageapi.LimitTypeImage {
392
+				continue
393
+			}
394
+
395
+			limitQuantity, ok := limit.Max[kapi.ResourceStorage]
396
+			if !ok {
397
+				continue
398
+			}
399
+			if limitQuantity.Cmp(*imageSize) < 0 {
400
+				// image size is larger than the permitted limit range max size
401
+				glog.V(4).Infof("Image %s in stream %s/%s exceeds limit %s: %v vs %v",
402
+					image.Name, is.Namespace, is.Name, limitRange.Name, *imageSize, limitQuantity)
403
+				return true
404
+			}
405
+		}
406
+	}
407
+	return false
408
+}
409
+
375 410
 // addPodsToGraph adds pods to the graph.
376 411
 //
377 412
 // A pod is only *excluded* from being added to the graph if its phase is not
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"time"
14 14
 
15 15
 	kapi "k8s.io/kubernetes/pkg/api"
16
+	"k8s.io/kubernetes/pkg/api/resource"
16 17
 	"k8s.io/kubernetes/pkg/api/unversioned"
17 18
 	"k8s.io/kubernetes/pkg/client/unversioned/fake"
18 19
 	ktc "k8s.io/kubernetes/pkg/client/unversioned/testclient"
... ...
@@ -57,6 +58,21 @@ func agedImage(id, ref string, ageInMinutes int64) imageapi.Image {
57 57
 	return image
58 58
 }
59 59
 
60
+func sizedImage(id, ref string, size int64) imageapi.Image {
61
+	image := imageWithLayers(id, ref,
62
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
63
+		"tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0",
64
+		"tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171",
65
+		"tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd",
66
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
67
+	)
68
+
69
+	image.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1) * time.Minute))
70
+	image.DockerImageMetadata.Size = size
71
+
72
+	return image
73
+}
74
+
60 75
 func image(id, ref string) imageapi.Image {
61 76
 	return agedImage(id, ref, -1)
62 77
 }
... ...
@@ -288,6 +304,26 @@ func build(namespace, name, strategyType, fromKind, fromNamespace, fromName stri
288 288
 	}
289 289
 }
290 290
 
291
+func limitList(limits ...int64) []*kapi.LimitRange {
292
+	list := make([]*kapi.LimitRange, len(limits))
293
+	for _, limit := range limits {
294
+		quantity := resource.NewQuantity(limit, resource.BinarySI)
295
+		list = append(list, &kapi.LimitRange{
296
+			Spec: kapi.LimitRangeSpec{
297
+				Limits: []kapi.LimitRangeItem{
298
+					{
299
+						Type: imageapi.LimitTypeImage,
300
+						Max: kapi.ResourceList{
301
+							kapi.ResourceStorage: *quantity,
302
+						},
303
+					},
304
+				},
305
+			},
306
+		})
307
+	}
308
+	return list
309
+}
310
+
291 311
 func commonSpec(strategyType, fromKind, fromNamespace, fromName string) buildapi.CommonSpec {
292 312
 	spec := buildapi.CommonSpec{
293 313
 		Strategy: buildapi.BuildStrategy{},
... ...
@@ -390,6 +426,7 @@ func TestImagePruning(t *testing.T) {
390 390
 	registryURL := "registry"
391 391
 
392 392
 	tests := map[string]struct {
393
+		pruneOverSizeLimit     *bool
393 394
 		registryURLs           []string
394 395
 		images                 imageapi.ImageList
395 396
 		pods                   kapi.PodList
... ...
@@ -398,6 +435,7 @@ func TestImagePruning(t *testing.T) {
398 398
 		bcs                    buildapi.BuildConfigList
399 399
 		builds                 buildapi.BuildList
400 400
 		dcs                    deployapi.DeploymentConfigList
401
+		limits                 map[string][]*kapi.LimitRange
401 402
 		expectedDeletions      []string
402 403
 		expectedUpdatedStreams []string
403 404
 	}{
... ...
@@ -663,6 +701,79 @@ func TestImagePruning(t *testing.T) {
663 663
 			expectedDeletions:      []string{"id"},
664 664
 			expectedUpdatedStreams: []string{},
665 665
 		},
666
+		"image exceeding limits": {
667
+			pruneOverSizeLimit: newBool(true),
668
+			images: imageList(
669
+				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
670
+				sizedImage("id2", registryURL+"/foo/bar@id2", 100),
671
+				sizedImage("id3", registryURL+"/foo/bar@id3", 200),
672
+			),
673
+			streams: streamList(
674
+				stream(registryURL, "foo", "bar", tags(
675
+					tag("latest",
676
+						tagEvent("id", "otherregistry/foo/bar@id"),
677
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
678
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
679
+					),
680
+				)),
681
+			),
682
+			limits: map[string][]*kapi.LimitRange{
683
+				"foo": limitList(100, 200),
684
+			},
685
+			expectedDeletions:      []string{"id3"},
686
+			expectedUpdatedStreams: []string{"foo/bar|id3"},
687
+		},
688
+		"multiple images in different namespaces exceeding different limits": {
689
+			pruneOverSizeLimit: newBool(true),
690
+			images: imageList(
691
+				sizedImage("id1", registryURL+"/foo/bar@id1", 100),
692
+				sizedImage("id2", registryURL+"/foo/bar@id2", 200),
693
+				sizedImage("id3", registryURL+"/bar/foo@id3", 500),
694
+				sizedImage("id4", registryURL+"/bar/foo@id4", 600),
695
+			),
696
+			streams: streamList(
697
+				stream(registryURL, "foo", "bar", tags(
698
+					tag("latest",
699
+						tagEvent("id1", registryURL+"/foo/bar@id1"),
700
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
701
+					),
702
+				)),
703
+				stream(registryURL, "bar", "foo", tags(
704
+					tag("latest",
705
+						tagEvent("id3", registryURL+"/bar/foo@id3"),
706
+						tagEvent("id4", registryURL+"/bar/foo@id4"),
707
+					),
708
+				)),
709
+			),
710
+			limits: map[string][]*kapi.LimitRange{
711
+				"foo": limitList(150),
712
+				"bar": limitList(550),
713
+			},
714
+			expectedDeletions:      []string{"id2", "id4"},
715
+			expectedUpdatedStreams: []string{"foo/bar|id2", "bar/foo|id4"},
716
+		},
717
+		"image within allowed limits": {
718
+			pruneOverSizeLimit: newBool(true),
719
+			images: imageList(
720
+				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
721
+				sizedImage("id2", registryURL+"/foo/bar@id2", 100),
722
+				sizedImage("id3", registryURL+"/foo/bar@id3", 200),
723
+			),
724
+			streams: streamList(
725
+				stream(registryURL, "foo", "bar", tags(
726
+					tag("latest",
727
+						tagEvent("id", "otherregistry/foo/bar@id"),
728
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
729
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
730
+					),
731
+				)),
732
+			),
733
+			limits: map[string][]*kapi.LimitRange{
734
+				"foo": limitList(300),
735
+			},
736
+			expectedDeletions:      []string{},
737
+			expectedUpdatedStreams: []string{},
738
+		},
666 739
 	}
667 740
 
668 741
 	for name, test := range tests {
... ...
@@ -672,15 +783,22 @@ func TestImagePruning(t *testing.T) {
672 672
 		}
673 673
 
674 674
 		options := PrunerOptions{
675
-			KeepYoungerThan:  60 * time.Minute,
676
-			KeepTagRevisions: 3,
677
-			Images:           &test.images,
678
-			Streams:          &test.streams,
679
-			Pods:             &test.pods,
680
-			RCs:              &test.rcs,
681
-			BCs:              &test.bcs,
682
-			Builds:           &test.builds,
683
-			DCs:              &test.dcs,
675
+			Images:      &test.images,
676
+			Streams:     &test.streams,
677
+			Pods:        &test.pods,
678
+			RCs:         &test.rcs,
679
+			BCs:         &test.bcs,
680
+			Builds:      &test.builds,
681
+			DCs:         &test.dcs,
682
+			LimitRanges: test.limits,
683
+		}
684
+		if test.pruneOverSizeLimit != nil {
685
+			options.PruneOverSizeLimit = test.pruneOverSizeLimit
686
+		} else {
687
+			keepYoungerThan := 60 * time.Minute
688
+			keepTagRevisions := 3
689
+			options.KeepYoungerThan = &keepYoungerThan
690
+			options.KeepTagRevisions = &keepTagRevisions
684 691
 		}
685 692
 		p := NewPruner(options)
686 693
 		p.(*pruner).registryPinger = &fakeRegistryPinger{}
... ...
@@ -880,9 +998,11 @@ func TestRegistryPruning(t *testing.T) {
880 880
 
881 881
 		t.Logf("Running test case %s", name)
882 882
 
883
+		keepYoungerThan := 60 * time.Minute
884
+		keepTagRevisions := 1
883 885
 		options := PrunerOptions{
884
-			KeepYoungerThan:  60 * time.Minute,
885
-			KeepTagRevisions: 1,
886
+			KeepYoungerThan:  &keepYoungerThan,
887
+			KeepTagRevisions: &keepTagRevisions,
886 888
 			Images:           &test.images,
887 889
 			Streams:          &test.streams,
888 890
 			Pods:             &kapi.PodList{},
... ...
@@ -913,3 +1033,9 @@ func TestRegistryPruning(t *testing.T) {
913 913
 		}
914 914
 	}
915 915
 }
916
+
917
+func newBool(a bool) *bool {
918
+	r := new(bool)
919
+	*r = a
920
+	return r
921
+}
... ...
@@ -1394,6 +1394,13 @@ items:
1394 1394
     - ""
1395 1395
     attributeRestrictions: null
1396 1396
     resources:
1397
+    - limitranges
1398
+    verbs:
1399
+    - list
1400
+  - apiGroups:
1401
+    - ""
1402
+    attributeRestrictions: null
1403
+    resources:
1397 1404
     - buildconfigs
1398 1405
     - builds
1399 1406
     verbs: