Skip to content

Commit 2d9691d

Browse files
author
Michal Minář
committed
image-pruning: delete istag references to absent images
Images can manually removed using `oc delete`. Image stream tags having references to these images become obsolete - we may delete them. To be sure that we don't remove reference to image that has just been created (and we don't know about it), make sure to honor --keep-younger-than. Signed-off-by: Michal Minář <[email protected]>
1 parent 86084a0 commit 2d9691d

File tree

2 files changed

+157
-47
lines changed

2 files changed

+157
-47
lines changed

pkg/image/prune/prune.go

+77-44
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const (
5757
// pruneAlgorithm contains the various settings to use when evaluating images
5858
// and layers for pruning.
5959
type pruneAlgorithm struct {
60-
keepYoungerThan time.Duration
60+
keepYoungerThan time.Time
6161
keepTagRevisions int
6262
pruneOverSizeLimit bool
6363
namespace string
@@ -217,7 +217,7 @@ func NewPruner(options PrunerOptions) (Pruner, error) {
217217

218218
algorithm := pruneAlgorithm{}
219219
if options.KeepYoungerThan != nil {
220-
algorithm.keepYoungerThan = *options.KeepYoungerThan
220+
algorithm.keepYoungerThan = metav1.Now().Add(-*options.KeepYoungerThan)
221221
}
222222
if options.KeepTagRevisions != nil {
223223
algorithm.keepTagRevisions = *options.KeepTagRevisions
@@ -307,8 +307,7 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, li
307307
// use a weak reference for old image revisions by default
308308
oldImageRevisionReferenceKind := WeakReferencedImageEdgeKind
309309

310-
age := metav1.Now().Sub(stream.CreationTimestamp.Time)
311-
if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
310+
if !algorithm.pruneOverSizeLimit && stream.CreationTimestamp.Time.After(algorithm.keepYoungerThan) {
312311
// stream's age is below threshold - use a strong reference for old image revisions instead
313312
oldImageRevisionReferenceKind = ReferencedImageEdgeKind
314313
}
@@ -415,9 +414,8 @@ func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm)
415414
// pending or running. Additionally, it has to be at least as old as the minimum
416415
// age threshold defined by the algorithm.
417416
if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending {
418-
age := metav1.Now().Sub(pod.CreationTimestamp.Time)
419-
if age >= algorithm.keepYoungerThan {
420-
glog.V(4).Infof("Pod %s is not running nor pending and age exceeds keepYoungerThan (%v) - skipping", getName(pod), age)
417+
if !pod.CreationTimestamp.Time.After(algorithm.keepYoungerThan) {
418+
glog.V(4).Infof("Pod %s is neither running nor pending and is too old", getName(pod))
421419
continue
422420
}
423421
}
@@ -598,9 +596,8 @@ func imageIsPrunable(g graph.Graph, imageNode *imagegraph.ImageNode, algorithm p
598596
}
599597
}
600598

601-
age := metav1.Now().Sub(imageNode.Image.CreationTimestamp.Time)
602-
if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
603-
glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%v)", imageNode.Image.Name, age)
599+
if !algorithm.pruneOverSizeLimit && imageNode.Image.CreationTimestamp.Time.After(algorithm.keepYoungerThan) {
600+
glog.V(4).Infof("Image %q is younger than minimum pruning age", imageNode.Image.Name)
604601
return false
605602
}
606603

@@ -690,23 +687,15 @@ func pruneStreams(
690687
g graph.Graph,
691688
prunableImageNodes map[string]*imagegraph.ImageNode,
692689
streamPruner ImageStreamDeleter,
690+
keepYoungerThan time.Time,
693691
) error {
694-
prunableStreams := make(map[string]*imagegraph.ImageStreamNode)
695-
696692
glog.V(4).Infof("Removing pruned image references from streams")
697-
for _, imageNode := range prunableImageNodes {
698-
for _, n := range g.To(imageNode) {
699-
streamNode, ok := n.(*imagegraph.ImageStreamNode)
700-
if !ok {
701-
continue
702-
}
703-
704-
streamName := getName(streamNode.ImageStream)
705-
prunableStreams[streamName] = streamNode
693+
for _, node := range g.Nodes() {
694+
streamNode, ok := node.(*imagegraph.ImageStreamNode)
695+
if !ok {
696+
continue
706697
}
707-
}
708-
709-
for streamName, streamNode := range prunableStreams {
698+
streamName := getName(streamNode.ImageStream)
710699
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
711700
stream, err := streamPruner.GetImageStream(streamNode.ImageStream)
712701
if err != nil {
@@ -720,26 +709,11 @@ func pruneStreams(
720709
updatedTags := sets.NewString()
721710
deletedTags := sets.NewString()
722711

723-
for tag, history := range stream.Status.Tags {
724-
newHistory := imageapi.TagEventList{}
725-
for i, tagEvent := range history.Items {
726-
glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image)
727-
728-
if _, ok := prunableImageNodes[tagEvent.Image]; ok {
729-
glog.V(4).Infof("Image stream tag %s:%s revision %d - removing because image %q matches deleted image", streamName, tag, i, tagEvent.Image)
730-
updatedTags.Insert(tag)
731-
} else {
732-
glog.V(4).Infof("Image stream tag %s:%s revision %d - keeping because image %q is not deleted", streamName, tag, i, tagEvent.Image)
733-
newHistory.Items = append(newHistory.Items, tagEvent)
734-
}
735-
}
736-
737-
if len(newHistory.Items) == 0 {
738-
glog.V(4).Infof("Image stream tag %s:%s - removing empty tag", streamName, tag)
739-
delete(stream.Status.Tags, tag)
712+
for tag := range stream.Status.Tags {
713+
if updated, deleted := pruneISTagHistory(g, prunableImageNodes, keepYoungerThan, streamName, stream, tag); deleted {
740714
deletedTags.Insert(tag)
741-
} else {
742-
stream.Status.Tags[tag] = newHistory
715+
} else if updated {
716+
updatedTags.Insert(tag)
743717
}
744718
}
745719

@@ -770,6 +744,65 @@ func pruneStreams(
770744
return nil
771745
}
772746

747+
// pruneISTagHistory processes tag event list of the given image stream tag. It removes references to images
748+
// that are going to be removed or are missing in the graph.
749+
func pruneISTagHistory(
750+
g graph.Graph,
751+
prunableImageNodes map[string]*imagegraph.ImageNode,
752+
keepYoungerThan time.Time,
753+
streamName string,
754+
imageStream *imageapi.ImageStream,
755+
tag string,
756+
) (tagUpdated, tagDeleted bool) {
757+
history := imageStream.Status.Tags[tag]
758+
newHistory := imageapi.TagEventList{}
759+
760+
for i, tagEvent := range history.Items {
761+
glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image)
762+
763+
if ok, reason := tagEventIsPrunable(tagEvent, g, prunableImageNodes, keepYoungerThan); ok {
764+
glog.V(4).Infof("Image stream tag %s:%s revision %d - removing because %s", streamName, tag, i, reason)
765+
tagUpdated = true
766+
} else {
767+
glog.V(4).Infof("Image stream tag %s:%s revision %d - keeping because %s", streamName, tag, i, reason)
768+
newHistory.Items = append(newHistory.Items, tagEvent)
769+
}
770+
}
771+
772+
if len(newHistory.Items) == 0 {
773+
glog.V(4).Infof("Image stream tag %s:%s - removing empty tag", streamName, tag)
774+
delete(imageStream.Status.Tags, tag)
775+
tagDeleted = true
776+
tagUpdated = false
777+
} else if tagUpdated {
778+
imageStream.Status.Tags[tag] = newHistory
779+
}
780+
781+
return
782+
}
783+
784+
func tagEventIsPrunable(
785+
tagEvent imageapi.TagEvent,
786+
g graph.Graph,
787+
prunableImageNodes map[string]*imagegraph.ImageNode,
788+
keepYoungerThan time.Time,
789+
) (ok bool, reason string) {
790+
if _, ok := prunableImageNodes[tagEvent.Image]; ok {
791+
return true, fmt.Sprintf("image %q matches deleted image", tagEvent.Image)
792+
}
793+
794+
n := imagegraph.FindImage(g, tagEvent.Image)
795+
if n != nil {
796+
return false, fmt.Sprintf("image %q is not deleted", tagEvent.Image)
797+
}
798+
799+
if n == nil && !tagEvent.Created.After(keepYoungerThan) {
800+
return true, fmt.Sprintf("image %q is absent", tagEvent.Image)
801+
}
802+
803+
return false, "the tag event is younger than threshold"
804+
}
805+
773806
// pruneImages invokes imagePruner.DeleteImage with each image that is prunable.
774807
func pruneImages(g graph.Graph, imageNodes map[string]*imagegraph.ImageNode, imagePruner ImageDeleter) []error {
775808
errs := []error{}
@@ -802,7 +835,7 @@ func (p *pruner) Prune(
802835

803836
prunableImageNodes, prunableImageIDs := calculatePrunableImages(p.g, imageNodes, p.algorithm)
804837

805-
err := pruneStreams(p.g, prunableImageNodes, streamPruner)
838+
err := pruneStreams(p.g, prunableImageNodes, streamPruner, p.algorithm.keepYoungerThan)
806839
// if namespace is specified prune only ImageStreams and nothing more
807840
// if we have any errors after ImageStreams pruning this may mean that
808841
// we still have references to images.

pkg/image/prune/prune_test.go

+80-3
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ func tagEvent(id, ref string) imageapi.TagEvent {
216216
}
217217
}
218218

219+
func youngTagEvent(id, ref string, created metav1.Time) imageapi.TagEvent {
220+
return imageapi.TagEvent{
221+
Image: id,
222+
Created: created,
223+
DockerImageReference: ref,
224+
}
225+
}
226+
219227
func rcList(rcs ...kapi.ReplicationController) kapi.ReplicationControllerList {
220228
return kapi.ReplicationControllerList{
221229
Items: rcs,
@@ -362,6 +370,7 @@ type fakeImageStreamDeleter struct {
362370
invocations sets.String
363371
err error
364372
streamImages map[string][]string
373+
streamTags map[string][]string
365374
}
366375

367376
var _ ImageStreamDeleter = &fakeImageStreamDeleter{}
@@ -370,9 +379,14 @@ func (p *fakeImageStreamDeleter) GetImageStream(stream *imageapi.ImageStream) (*
370379
if p.streamImages == nil {
371380
p.streamImages = make(map[string][]string)
372381
}
373-
for _, history := range stream.Status.Tags {
382+
if p.streamTags == nil {
383+
p.streamTags = make(map[string][]string)
384+
}
385+
for tag, history := range stream.Status.Tags {
386+
streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
387+
p.streamTags[streamName] = append(p.streamTags[streamName], tag)
388+
374389
for _, tagEvent := range history.Items {
375-
streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
376390
p.streamImages[streamName] = append(p.streamImages[streamName], tagEvent.Image)
377391
}
378392
}
@@ -381,15 +395,23 @@ func (p *fakeImageStreamDeleter) GetImageStream(stream *imageapi.ImageStream) (*
381395

382396
func (p *fakeImageStreamDeleter) UpdateImageStream(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) {
383397
streamImages := make(map[string]struct{})
398+
streamTags := make(map[string]struct{})
384399

385-
for _, history := range stream.Status.Tags {
400+
for tag, history := range stream.Status.Tags {
401+
streamTags[tag] = struct{}{}
386402
for _, tagEvent := range history.Items {
387403
streamImages[tagEvent.Image] = struct{}{}
388404
}
389405
}
390406

391407
streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
392408

409+
for _, tag := range p.streamTags[streamName] {
410+
if _, ok := streamTags[tag]; !ok {
411+
p.invocations.Insert(fmt.Sprintf("%s:%s", streamName, tag))
412+
}
413+
}
414+
393415
for _, imageName := range p.streamImages[streamName] {
394416
if _, ok := streamImages[imageName]; !ok {
395417
p.invocations.Insert(fmt.Sprintf("%s|%s", streamName, imageName))
@@ -743,6 +765,61 @@ func TestImagePruning(t *testing.T) {
743765
expectedStreamUpdates: []string{},
744766
},
745767

768+
"image stream - unreference absent image": {
769+
images: imageList(
770+
image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"),
771+
),
772+
streams: streamList(
773+
stream(registryHost, "foo", "bar", tags(
774+
tag("latest",
775+
tagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
776+
tagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"),
777+
),
778+
)),
779+
),
780+
expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000"},
781+
},
782+
783+
"image stream with dangling references - delete tags": {
784+
images: imageList(
785+
imageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", nil, "layer1"),
786+
),
787+
streams: streamList(
788+
stream(registryHost, "foo", "bar", tags(
789+
tag("latest",
790+
tagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"),
791+
),
792+
tag("tag",
793+
tagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"),
794+
),
795+
)),
796+
),
797+
expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"},
798+
expectedStreamUpdates: []string{
799+
"foo/bar:latest",
800+
"foo/bar:tag",
801+
"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000",
802+
"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002",
803+
},
804+
expectedBlobDeletions: []string{registryURL + "|layer1"},
805+
},
806+
807+
"image stream - keep reference to a young absent image": {
808+
images: imageList(
809+
image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"),
810+
imageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", nil),
811+
),
812+
streams: streamList(
813+
stream(registryHost, "foo", "bar", tags(
814+
tag("latest",
815+
youngTagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", metav1.Now()),
816+
tagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"),
817+
),
818+
)),
819+
),
820+
expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000002"},
821+
},
822+
746823
"multiple resources pointing to image - don't prune": {
747824
images: imageList(
748825
image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"),

0 commit comments

Comments
 (0)