From 59abafcba532a78cb87ce2762056404136785245 Mon Sep 17 00:00:00 2001 From: Oleg Bulatov Date: Tue, 15 Sep 2020 12:28:49 +0200 Subject: [PATCH] Reimplement image pruner The new implementation of the image pruner does not use graphs. The graph was replaced by two maps. The pruner works in two steps: The first step deletes image stream items (aka revisions) that are not used by the cluster components (pods, builds, etc.) and are not protected by the pruner options (keep-younger-than, keep-tag-revisions, etc.) The second step deletes image objects that are not used by image streams. --- pkg/cli/admin/prune/imageprune/helper.go | 101 +- pkg/cli/admin/prune/imageprune/prune.go | 2118 ++++++++---------- pkg/cli/admin/prune/imageprune/prune_test.go | 1826 ++++++++------- pkg/cli/admin/prune/imageprune/worker.go | 359 --- pkg/cli/admin/prune/images/images.go | 284 +-- pkg/cli/admin/prune/images/images_test.go | 6 +- pkg/helpers/image/test/util.go | 15 +- 7 files changed, 1871 insertions(+), 2838 deletions(-) delete mode 100644 pkg/cli/admin/prune/imageprune/worker.go diff --git a/pkg/cli/admin/prune/imageprune/helper.go b/pkg/cli/admin/prune/imageprune/helper.go index c19313fb49..7758be6385 100644 --- a/pkg/cli/admin/prune/imageprune/helper.go +++ b/pkg/cli/admin/prune/imageprune/helper.go @@ -10,12 +10,7 @@ import ( "github.com/docker/distribution/registry/api/errcode" "k8s.io/klog/v2" - corev1 "k8s.io/api/core/v1" - kmeta "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" kerrors "k8s.io/apimachinery/pkg/util/errors" - ref "k8s.io/client-go/tools/reference" - "k8s.io/kubectl/pkg/scheme" imagev1 "github.com/openshift/api/image/v1" "github.com/openshift/library-go/pkg/image/reference" @@ -42,13 +37,12 @@ func (ba isByAge) Less(i, j int) bool { // DetermineRegistryHost returns registry host embedded in a pull-spec of the latest unmanaged image or the // latest imagestream from the provided lists. If no such pull-spec is found, error is returned. -func DetermineRegistryHost(images *imagev1.ImageList, imageStreams *imagev1.ImageStreamList) (string, error) { +func DetermineRegistryHost(images map[string]*imagev1.Image, imageStreams *imagev1.ImageStreamList) (string, error) { var pullSpec string var managedImages []*imagev1.Image // 1st try to determine registry url from a pull spec of the youngest managed image - for i := range images.Items { - image := &images.Items[i] + for _, image := range images { if image.Annotations[imagev1.ManagedByOpenShiftAnnotation] != "true" { continue } @@ -216,89 +210,28 @@ func (rp *retryPath) Error() string { return rp.err.Error() } // ErrBadReference denotes an invalid reference to image, imagestreamtag or imagestreamimage stored in a // particular object. The object is identified by kind, namespace and name. type ErrBadReference struct { - kind string - namespace string - name string - targetKind string - reference string - reason string + referrer resourceReference + subreferrer string + targetKind string + reference string + err error } -func newErrBadReferenceToImage(reference string, obj *corev1.ObjectReference, reason string) error { - kind := "" - namespace := "" - name := "" - if obj != nil { - kind = obj.Kind - namespace = obj.Namespace - name = obj.Name - } - - return &ErrBadReference{ - kind: kind, - namespace: namespace, - name: name, - reference: reference, - reason: reason, - } -} - -func newErrBadReferenceTo(targetKind, reference string, obj *corev1.ObjectReference, reason string) error { +func newErrBadReferenceTo(referrer resourceReference, subreferrer string, targetKind string, reference string, err error) error { return &ErrBadReference{ - kind: obj.Kind, - namespace: obj.Namespace, - name: obj.Name, - targetKind: targetKind, - reference: reference, - reason: reason, + referrer: referrer, + subreferrer: subreferrer, + targetKind: targetKind, + reference: reference, + err: err, } } func (e *ErrBadReference) Error() string { - return e.String() -} - -func (e *ErrBadReference) String() string { - name := e.name - if len(e.namespace) > 0 { - name = e.namespace + "/" + name - } - targetKind := "container image" - if len(e.targetKind) > 0 { - targetKind = e.targetKind + r := e.referrer.String() + if e.subreferrer != "" { + r += ": " + e.subreferrer } - return fmt.Sprintf("%s[%s]: invalid %s reference %q: %s", e.kind, name, targetKind, e.reference, e.reason) -} -func getName(obj runtime.Object) string { - accessor, err := kmeta.Accessor(obj) - if err != nil { - klog.V(4).Infof("Error getting accessor for %#v", obj) - return "" - } - ns := accessor.GetNamespace() - if len(ns) == 0 { - return accessor.GetName() - } - return fmt.Sprintf("%s/%s", ns, accessor.GetName()) -} - -func getKindName(obj *corev1.ObjectReference) string { - if obj == nil { - return "unknown object" - } - name := obj.Name - if len(obj.Namespace) > 0 { - name = obj.Namespace + "/" + name - } - return fmt.Sprintf("%s[%s]", obj.Kind, name) -} - -func getRef(obj runtime.Object) *corev1.ObjectReference { - ref, err := ref.GetReference(scheme.Scheme, obj) - if err != nil { - klog.Errorf("failed to get reference to object %T: %v", obj, err) - return nil - } - return ref + return fmt.Sprintf("%s: invalid %s reference %q: %v", r, e.targetKind, e.reference, e.err) } diff --git a/pkg/cli/admin/prune/imageprune/prune.go b/pkg/cli/admin/prune/imageprune/prune.go index e9c9be54bf..61d636c9be 100644 --- a/pkg/cli/admin/prune/imageprune/prune.go +++ b/pkg/cli/admin/prune/imageprune/prune.go @@ -3,18 +3,16 @@ package imageprune import ( "context" "encoding/json" - "errors" "fmt" "net/http" "net/url" "reflect" - "sort" "strings" + "sync" "time" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/registry/api/errcode" - gonum "github.com/gonum/graph" "k8s.io/klog/v2" kappsv1 "k8s.io/api/apps/v1" @@ -23,9 +21,6 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kerrors "k8s.io/apimachinery/pkg/util/errors" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/util/retry" appsv1 "github.com/openshift/api/apps/v1" @@ -36,48 +31,128 @@ import ( "github.com/openshift/library-go/pkg/build/buildutil" "github.com/openshift/library-go/pkg/image/imageutil" "github.com/openshift/library-go/pkg/image/reference" - appsgraph "github.com/openshift/oc/pkg/helpers/graph/appsgraph/nodes" - buildgraph "github.com/openshift/oc/pkg/helpers/graph/buildgraph/nodes" - "github.com/openshift/oc/pkg/helpers/graph/genericgraph" - imagegraph "github.com/openshift/oc/pkg/helpers/graph/imagegraph/nodes" - kubegraph "github.com/openshift/oc/pkg/helpers/graph/kubegraph/nodes" ) -// TODO these edges should probably have an `Add***Edges` method in images/graph and be moved there -const ( - // ReferencedImageEdgeKind defines a "strong" edge where the tail is an - // ImageNode, with strong indicating that the ImageNode tail is not a - // candidate for pruning. - ReferencedImageEdgeKind = "ReferencedImage" - // WeakReferencedImageEdgeKind defines a "weak" edge where the tail is - // an ImageNode, with weak indicating that this particular edge does - // not keep an ImageNode from being a candidate for pruning. - WeakReferencedImageEdgeKind = "WeakReferencedImage" +const defaultPruneImageWorkerCount = 5 - // ReferencedImageConfigEdgeKind defines an edge from an ImageStreamNode or an - // ImageNode to an ImageComponentNode. - ReferencedImageConfigEdgeKind = "ReferencedImageConfig" +type imageStreamTagReference struct { + Namespace string + Name string + Tag string +} - // ReferencedImageLayerEdgeKind defines an edge from an ImageStreamNode or an - // ImageNode to an ImageComponentNode. - ReferencedImageLayerEdgeKind = "ReferencedImageLayer" +func (r imageStreamTagReference) String() string { + return fmt.Sprintf("%s/%s:%s", r.Namespace, r.Name, r.Tag) +} - // ReferencedImageManifestEdgeKind defines an edge from an ImageStreamNode or an - // ImageNode to an ImageComponentNode. - ReferencedImageManifestEdgeKind = "ReferencedImageManifest" +type imageStreamImageReference struct { + Namespace string + Name string + Digest string +} - defaultPruneImageWorkerCount = 5 -) +func (r imageStreamImageReference) String() string { + return fmt.Sprintf("%s/%s@%s", r.Namespace, r.Name, r.Digest) +} + +type resourceReference struct { + Resource string + Namespace string + Name string +} + +func (r resourceReference) String() string { + if r.Namespace == "" { + return fmt.Sprintf("%s/%s", r.Resource, r.Name) + } + return fmt.Sprintf("%s/%s namespace=%s", r.Resource, r.Name, r.Namespace) +} + +func referencesSample(refs []resourceReference) string { + if len(refs) == 0 { + return "" + } -// RegistryClientFactoryFunc is a factory function returning a registry client for use in a worker. -type RegistryClientFactoryFunc func() (*http.Client, error) + result := "" + suffix := "" + limit := len(refs) + if limit >= 5 { + limit = 3 + suffix = fmt.Sprintf(", and %d more", len(refs)-limit) + } + result += refs[0].String() + for i := 1; i < limit; i++ { + result += ", " + result += refs[i].String() + } + return result + suffix +} + +type PruneStats struct { + mutex sync.Mutex + DeletedImages int + DeletedImageStreamTagItems int + UpdatedImageStreams int + DeletedLayerLinks int + DeletedManifestLinks int + DeletedBlobs int +} + +func (s *PruneStats) Copy() *PruneStats { + s.mutex.Lock() + defer s.mutex.Unlock() + + return &PruneStats{ + DeletedImages: s.DeletedImages, + DeletedImageStreamTagItems: s.DeletedImageStreamTagItems, + UpdatedImageStreams: s.UpdatedImageStreams, + DeletedLayerLinks: s.DeletedLayerLinks, + DeletedManifestLinks: s.DeletedManifestLinks, + DeletedBlobs: s.DeletedBlobs, + } +} + +func (s *PruneStats) Add(other *PruneStats) { + s.mutex.Lock() + defer s.mutex.Unlock() + + other = other.Copy() // make a local copy to avoid races + + s.DeletedImages += other.DeletedImages + s.DeletedImageStreamTagItems += other.DeletedImageStreamTagItems + s.UpdatedImageStreams += other.UpdatedImageStreams + s.DeletedLayerLinks += other.DeletedLayerLinks + s.DeletedManifestLinks += other.DeletedManifestLinks + s.DeletedBlobs += other.DeletedBlobs +} -//ImagePrunerFactoryFunc is a factory function returning an image deleter for use in a worker. -type ImagePrunerFactoryFunc func() (ImageDeleter, error) +func (s *PruneStats) String() string { + s.mutex.Lock() + defer s.mutex.Unlock() -// FakeRegistryClientFactory is a registry client factory creating no client at all. Useful for dry run. -func FakeRegistryClientFactory() (*http.Client, error) { - return nil, nil + var parts []string + if s.DeletedImages != 0 { + parts = append(parts, fmt.Sprintf("deleted %d image object(s)", s.DeletedImages)) + } + if s.DeletedImageStreamTagItems != 0 { + parts = append(parts, fmt.Sprintf("deleted %d image stream tag item(s)", s.DeletedImageStreamTagItems)) + } + if s.UpdatedImageStreams != 0 { + parts = append(parts, fmt.Sprintf("updated %d image stream(s)", s.UpdatedImageStreams)) + } + if s.DeletedLayerLinks != 0 { + parts = append(parts, fmt.Sprintf("deleted %d layer link(s)", s.DeletedLayerLinks)) + } + if s.DeletedManifestLinks != 0 { + parts = append(parts, fmt.Sprintf("deleted %d manifest link(s)", s.DeletedManifestLinks)) + } + if s.DeletedBlobs != 0 { + parts = append(parts, fmt.Sprintf("deleted %d blob(s)", s.DeletedBlobs)) + } + if len(parts) == 0 { + return "deleted 0 objects" + } + return strings.Join(parts, ", ") } // pruneAlgorithm contains the various settings to use when evaluating images @@ -101,25 +176,23 @@ type ImageDeleter interface { type ImageStreamDeleter interface { // GetImageStream returns a fresh copy of an image stream. GetImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) - // UpdateImageStream removes all references to the image from the image - // stream's status.tags. The updated image stream is returned. - UpdateImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) - // NotifyImageStreamPrune shows notification about updated image stream. - NotifyImageStreamPrune(stream *imagev1.ImageStream, updatedTags []string, deletedTags []string) + // UpdateImageStream updates the image stream's status. The updated image + // stream is returned. + UpdateImageStream(stream *imagev1.ImageStream, deletedItems int) (*imagev1.ImageStream, error) } // BlobDeleter knows how to delete a blob from the container image registry. type BlobDeleter interface { // DeleteBlob uses registryClient to ask the registry at registryURL // to remove the blob. - DeleteBlob(registryClient *http.Client, registryURL *url.URL, blob string) error + DeleteBlob(blob string) error } // LayerLinkDeleter knows how to delete a repository layer link from the container image registry. type LayerLinkDeleter interface { // DeleteLayerLink uses registryClient to ask the registry at registryURL to // delete the repository layer link. - DeleteLayerLink(registryClient *http.Client, registryURL *url.URL, repo, linkName string) error + DeleteLayerLink(repo, linkName string) error } // ManifestDeleter knows how to delete image manifest data for a repository from @@ -127,7 +200,7 @@ type LayerLinkDeleter interface { type ManifestDeleter interface { // DeleteManifest uses registryClient to ask the registry at registryURL to // delete the repository's image manifest data. - DeleteManifest(registryClient *http.Client, registryURL *url.URL, repo, manifest string) error + DeleteManifest(repo, manifest string) error } // PrunerOptions contains the fields used to initialize a new Pruner. @@ -149,16 +222,12 @@ type PrunerOptions struct { PruneRegistry *bool // Namespace to be pruned, if specified it should never remove Images. Namespace string - // Images is the entire list of images in OpenShift. An image must be in this - // list to be a candidate for pruning. - Images *imagev1.ImageList - // ImageWatcher watches for image changes. - ImageWatcher watch.Interface + // Images is the entire list of images in OpenShift indexed by their name. + // An image must be in this list to be a candidate for pruning. + Images map[string]*imagev1.Image // Streams is the entire list of image streams across all namespaces in the - // cluster. - Streams *imagev1.ImageStreamList - // StreamWatcher watches for stream changes. - StreamWatcher watch.Interface + // cluster indexed by "namespace/name" strings. + Streams map[string]*imagev1.ImageStream // Pods is the entire list of pods across all namespaces in the cluster. Pods *corev1.PodList // RCs is the entire list of replication controllers across all namespaces in @@ -182,10 +251,6 @@ type PrunerOptions struct { // DryRun indicates that no changes will be made to the cluster and nothing // will be removed. DryRun bool - // RegistryClient is the http.Client to use when contacting the registry. - RegistryClientFactory RegistryClientFactoryFunc - // RegistryURL is the URL of the integrated container image registry. - RegistryURL *url.URL // IgnoreInvalidRefs indicates that all invalid references should be ignored. IgnoreInvalidRefs bool // NumWorkers is a desired number of workers concurrently handling image prune jobs. If less than 1, the @@ -195,34 +260,30 @@ type PrunerOptions struct { // Pruner knows how to prune istags, images, manifest, layers, image configs and blobs. type Pruner interface { - // Prune uses imagePruner, streamPruner, layerLinkPruner, blobPruner, and - // manifestPruner to remove images that have been identified as candidates - // for pruning based on the Pruner's internal pruning algorithm. + // Prune uses deleters to remove images that have been identified as + // candidates for pruning based on the Pruner's internal pruning algorithm. // Please see NewPruner for details on the algorithm. Prune( - imagePrunerFactory ImagePrunerFactoryFunc, - streamPruner ImageStreamDeleter, - layerLinkPruner LayerLinkDeleter, - blobPruner BlobDeleter, - manifestPruner ManifestDeleter, - ) (deletions []Deletion, failures []Failure) + imageStreamDeleter ImageStreamDeleter, + layerLinkDeleter LayerLinkDeleter, + manifestDeleter ManifestDeleter, + blobDeleter BlobDeleter, + imageDeleter ImageDeleter, + ) (*PruneStats, kerrors.Aggregate) } // pruner is an object that knows how to prune a data set type pruner struct { - g genericgraph.Graph - algorithm pruneAlgorithm - ignoreInvalidRefs bool - registryClientFactory RegistryClientFactoryFunc - registryURL *url.URL - imageWatcher watch.Interface - imageStreamWatcher watch.Interface - imageStreamLimits map[string][]*corev1.LimitRange - // sorted queue of images to prune; nil stands for empty queue - queue *nodeItem - // contains prunable images removed from queue that are currently being processed - processedImages map[*imagegraph.ImageNode]*Job - numWorkers int + usedTags map[imageStreamTagReference][]resourceReference + usedImages map[imageStreamImageReference][]resourceReference + + images map[string]*imagev1.Image + imageStreams map[string]*imagev1.ImageStream + + algorithm pruneAlgorithm + ignoreInvalidRefs bool + imageStreamLimits map[string][]*corev1.LimitRange + numWorkers int } var _ Pruner = &pruner{} @@ -299,190 +360,345 @@ func NewPruner(options PrunerOptions) (Pruner, kerrors.Aggregate) { algorithm.namespace = options.Namespace p := &pruner{ - algorithm: algorithm, - ignoreInvalidRefs: options.IgnoreInvalidRefs, - registryClientFactory: options.RegistryClientFactory, - registryURL: options.RegistryURL, - processedImages: make(map[*imagegraph.ImageNode]*Job), - imageWatcher: options.ImageWatcher, - imageStreamWatcher: options.StreamWatcher, - imageStreamLimits: options.LimitRanges, - numWorkers: options.NumWorkers, + images: options.Images, + imageStreams: options.Streams, + algorithm: algorithm, + ignoreInvalidRefs: options.IgnoreInvalidRefs, + imageStreamLimits: options.LimitRanges, + numWorkers: options.NumWorkers, } if p.numWorkers < 1 { p.numWorkers = defaultPruneImageWorkerCount } - if err := p.buildGraph(options); err != nil { + for _, image := range p.images { + if err := imageutil.ImageWithMetadata(image); err != nil { + return nil, kerrors.NewAggregate([]error{ + fmt.Errorf("failed to read image metadata for %s: %v", image.Name, err), + }) + } + } + + if err := p.analyzeImageStreamsReferences(options); err != nil { return nil, err } return p, nil } -// buildGraph builds a graph -func (p *pruner) buildGraph(options PrunerOptions) kerrors.Aggregate { - p.g = genericgraph.New() +func (p *pruner) analyzeImageStreamsReferences(options PrunerOptions) kerrors.Aggregate { + p.usedTags = map[imageStreamTagReference][]resourceReference{} + p.usedImages = map[imageStreamImageReference][]resourceReference{} var errs []error - - errs = append(errs, p.addImagesToGraph(options.Images)...) - errs = append(errs, p.addImageStreamsToGraph(options.Streams, options.LimitRanges)...) - errs = append(errs, p.addPodsToGraph(options.Pods)...) - errs = append(errs, p.addReplicationControllersToGraph(options.RCs)...) - errs = append(errs, p.addBuildConfigsToGraph(options.BCs)...) - errs = append(errs, p.addBuildsToGraph(options.Builds)...) - errs = append(errs, p.addDaemonSetsToGraph(options.DSs)...) - errs = append(errs, p.addDeploymentsToGraph(options.Deployments)...) - errs = append(errs, p.addDeploymentConfigsToGraph(options.DCs)...) - errs = append(errs, p.addReplicaSetsToGraph(options.RSs)...) - + errs = append(errs, p.analyzeReferencesFromPods(options.Pods)...) + errs = append(errs, p.analyzeReferencesFromReplicationControllers(options.RCs)...) + errs = append(errs, p.analyzeReferencesFromDeploymentConfigs(options.DCs)...) + errs = append(errs, p.analyzeReferencesFromReplicaSets(options.RSs)...) + errs = append(errs, p.analyzeReferencesFromDeployments(options.Deployments)...) + errs = append(errs, p.analyzeReferencesFromDaemonSets(options.DSs)...) + errs = append(errs, p.analyzeReferencesFromBuilds(options.Builds)...) + errs = append(errs, p.analyzeReferencesFromBuildConfigs(options.BCs)...) return kerrors.NewAggregate(errs) } -func getValue(option interface{}) string { - if v := reflect.ValueOf(option); !v.IsNil() { - return fmt.Sprintf("%v", v.Elem()) +// analyzeImageReference analyzes which ImageStreamImage or ImageStreamTag is +// referenced by imageReference, and associates it with referrer. +func (p *pruner) analyzeImageReference(referrer resourceReference, subreferrer string, imageReference string) error { + logPrefix := subreferrer + if logPrefix != "" { + logPrefix += ": " } - return "" + + ref, err := reference.Parse(imageReference) + if err != nil { + err = newErrBadReferenceTo(referrer, subreferrer, "image", imageReference, err) + if !p.ignoreInvalidRefs { + return err + } + klog.V(1).Infof("%s - skipping", err) + return nil + } + + if ref.Registry == "" || ref.Namespace == "" || strings.Index(ref.Name, "/") != -1 { + klog.V(4).Infof("%s: %simage reference %s does not match hostname/namespace/name pattern - skipping", referrer, logPrefix, imageReference) + return nil + } + + if len(ref.ID) == 0 { + // Attempt to dereference istag. Since we cannot be sure whether the reference refers to the + // integrated registry or not, we ignore the host part completely. As a consequence, we may keep + // image otherwise sentenced for a removal just because its pull spec accidentally matches one of + // our imagestreamtags. + + // set the tag if empty + ref = ref.DockerClientDefaults() + + istag := imageStreamTagReference{ + Namespace: ref.Namespace, + Name: ref.Name, + Tag: ref.Tag, + } + klog.V(4).Infof("%s: %sgot reference to ImageStreamTag %s (registry name is %s)", referrer, logPrefix, istag, ref.Registry) + p.usedTags[istag] = append(p.usedTags[istag], referrer) + return nil + } + + isimage := imageStreamImageReference{ + Namespace: ref.Namespace, + Name: ref.Name, + Digest: ref.ID, + } + klog.V(4).Infof("%s: %sgot reference to ImageStreamImage %s (registry name is %s)", referrer, logPrefix, isimage, ref.Registry) + p.usedImages[isimage] = append(p.usedImages[isimage], referrer) + return nil } -// addImagesToGraph adds all images, their manifests and their layers to the graph. -func (p *pruner) addImagesToGraph(images *imagev1.ImageList) []error { +// analyzeReferencesFromPodSpec extracts information about image streams that +// are specified by referrer's pod spec's containers. +func (p *pruner) analyzeReferencesFromPodSpec(referrer resourceReference, spec *corev1.PodSpec) []error { var errs []error - for i := range images.Items { - image := &images.Items[i] - klog.V(4).Infof("Adding image %q to graph", image.Name) - imageNode := imagegraph.EnsureImageNode(p.g, image) + for _, container := range spec.InitContainers { + if len(strings.TrimSpace(container.Image)) == 0 { + klog.V(4).Infof("%s: init container %s: ignoring container because it has no reference to image", referrer, container.Name) + continue + } - if err := imageutil.ImageWithMetadata(image); err != nil { - klog.V(1).Infof("Failed to read image metadata for image %s: %v", image.Name, err) + err := p.analyzeImageReference(referrer, fmt.Sprintf("init container %s", container.Name), container.Image) + if err != nil { errs = append(errs, err) - continue } - dockerImage, ok := image.DockerImageMetadata.Object.(*dockerv10.DockerImage) - if !ok { - klog.V(1).Infof("Failed to read image metadata for image %s", image.Name) - errs = append(errs, fmt.Errorf("Failed to read image metadata for image %s", image.Name)) + } + + for _, container := range spec.Containers { + if len(strings.TrimSpace(container.Image)) == 0 { + klog.V(4).Infof("%s: container %s: ignoring container because it has no reference to image", referrer, container.Name) continue } - if image.DockerImageManifestMediaType == schema2.MediaTypeManifest && len(dockerImage.ID) > 0 { - configName := dockerImage.ID - klog.V(4).Infof("Adding image config %q to graph", configName) - configNode := imagegraph.EnsureImageComponentConfigNode(p.g, configName) - p.g.AddEdge(imageNode, configNode, ReferencedImageConfigEdgeKind) - } - for _, layer := range image.DockerImageLayers { - klog.V(4).Infof("Adding image layer %q to graph", layer.Name) - layerNode := imagegraph.EnsureImageComponentLayerNode(p.g, layer.Name) - p.g.AddEdge(imageNode, layerNode, ReferencedImageLayerEdgeKind) + err := p.analyzeImageReference(referrer, fmt.Sprintf("container %s", container.Name), container.Image) + if err != nil { + errs = append(errs, err) } - - klog.V(4).Infof("Adding image manifest %q to graph", image.Name) - manifestNode := imagegraph.EnsureImageComponentManifestNode(p.g, image.Name) - p.g.AddEdge(imageNode, manifestNode, ReferencedImageManifestEdgeKind) } return errs } -// addImageStreamsToGraph adds all the streams to the graph. The most recent n -// image revisions for a tag will be preserved, where n is specified by the -// algorithm's keepTagRevisions. Image revisions older than n are candidates -// for pruning if the image stream's age is at least as old as the minimum -// threshold in algorithm. Otherwise, if the image stream is younger than the -// threshold, all image revisions for that stream are ineligible for pruning. -// If pruneOverSizeLimit flag is set to true, above does not matter, instead -// all images size is checked against LimitRanges defined in that same namespace, -// and whenever its size exceeds the smallest limit in that namespace, it will be -// considered a candidate for pruning. -// -// addImageStreamsToGraph also adds references from each stream to all the -// layers it references (via each image a stream references). -func (p *pruner) addImageStreamsToGraph(streams *imagev1.ImageStreamList, limits map[string][]*corev1.LimitRange) []error { - for i := range streams.Items { - stream := &streams.Items[i] - - klog.V(4).Infof("Examining ImageStream %s", getName(stream)) +// analyzeReferencesFromBuildStrategy extracts information about image streams +// that are used by referrer's build strategy. +func (p *pruner) analyzeReferencesFromBuildStrategy(referrer resourceReference, strategy buildv1.BuildStrategy) []error { + from := buildutil.GetInputReference(strategy) + if from == nil { + klog.V(4).Infof("%s: unable to determine 'from' reference - skipping", referrer) + return nil + } - // use a weak reference for old image revisions by default - oldImageRevisionReferenceKind := WeakReferencedImageEdgeKind + switch from.Kind { + case "DockerImage": + if len(strings.TrimSpace(from.Name)) == 0 { + klog.V(4).Infof("%s: ignoring build strategy because it has no reference to image", referrer) + return nil + } - if !p.algorithm.pruneOverSizeLimit && stream.CreationTimestamp.Time.After(p.algorithm.keepYoungerThan) { - // stream's age is below threshold - use a strong reference for old image revisions instead - oldImageRevisionReferenceKind = ReferencedImageEdgeKind + err := p.analyzeImageReference(referrer, "", from.Name) + if err != nil { + return []error{err} } - klog.V(4).Infof("Adding ImageStream %s to graph", getName(stream)) - isNode := imagegraph.EnsureImageStreamNode(p.g, stream) - imageStreamNode := isNode.(*imagegraph.ImageStreamNode) + case "ImageStreamImage": + name, id, err := imageutil.ParseImageStreamImageName(from.Name) + if err != nil { + if !p.ignoreInvalidRefs { + return []error{newErrBadReferenceTo(referrer, "", "ImageStreamImage", from.Name, err)} + } + klog.V(1).Infof("%s: failed to parse ImageStreamImage name %q: %v - skipping", referrer, from.Name, err) + return nil + } - for _, tag := range stream.Status.Tags { - istNode := imagegraph.EnsureImageStreamTagNode(p.g, makeISTagWithStream(stream, tag.Tag)) - - for i, tagEvent := range tag.Items { - imageNode := imagegraph.FindImage(p.g, tag.Items[i].Image) - if imageNode == nil { - klog.V(2).Infof("Unable to find image %q in graph (from tag=%q, revision=%d, dockerImageReference=%s) - skipping", - tag.Items[i].Image, tag.Tag, tagEvent.Generation, tag.Items[i].DockerImageReference) - continue - } + isimage := imageStreamImageReference{ + Namespace: from.Namespace, + Name: name, + Digest: id, + } + if isimage.Namespace == "" { + isimage.Namespace = referrer.Namespace + } + klog.V(4).Infof("%s: got reference to ImageStreamImage %s", referrer, isimage) + p.usedImages[isimage] = append(p.usedImages[isimage], referrer) - kind := oldImageRevisionReferenceKind - if p.algorithm.pruneOverSizeLimit { - if exceedsLimits(stream, imageNode.Image, limits) { - kind = WeakReferencedImageEdgeKind - } else { - kind = ReferencedImageEdgeKind - } - } else { - if i < p.algorithm.keepTagRevisions { - kind = ReferencedImageEdgeKind - } - } + case "ImageStreamTag": + name, tag, err := imageutil.ParseImageStreamTagName(from.Name) + if err != nil { + if !p.ignoreInvalidRefs { + return []error{newErrBadReferenceTo(referrer, "", "ImageStreamTag", from.Name, err)} + } + klog.V(1).Infof("%s: failed to parse ImageStreamTag name %q: %v", referrer, from.Name, err) + return nil + } - if i == 0 { - klog.V(4).Infof("Adding edge (kind=%s) from %q to %q", kind, istNode.UniqueName(), imageNode.UniqueName()) - p.g.AddEdge(istNode, imageNode, kind) - } + istag := imageStreamTagReference{ + Namespace: from.Namespace, + Name: name, + Tag: tag, + } + if istag.Namespace == "" { + istag.Namespace = referrer.Namespace + } + klog.V(4).Infof("%s: got reference to ImageStreamTag %s", referrer, istag) + p.usedTags[istag] = append(p.usedTags[istag], referrer) - klog.V(4).Infof("Checking for existing strong reference from stream %s to image %s", getName(stream), imageNode.Image.Name) - if edge := p.g.Edge(imageStreamNode, imageNode); edge != nil && p.g.EdgeKinds(edge).Has(ReferencedImageEdgeKind) { - klog.V(4).Infof("Strong reference found") - continue - } + default: + klog.Warningf("%s: ignoring unrecognized source location %#v", referrer, from) + } - klog.V(4).Infof("Adding edge (kind=%s) from %q to %q", kind, imageStreamNode.UniqueName(), imageNode.UniqueName()) - p.g.AddEdge(imageStreamNode, imageNode, kind) + return nil +} - klog.V(4).Infof("Adding stream->(layer|config) references") - // add stream -> layer references so we can prune them later - for _, s := range p.g.From(imageNode) { - cn, ok := s.(*imagegraph.ImageComponentNode) - if !ok { - continue - } +// analyzeReferencesFromPods finds references to imagestreams from pods. +func (p *pruner) analyzeReferencesFromPods(pods *corev1.PodList) []error { + var errs []error + for _, pod := range pods.Items { + ref := resourceReference{ + Resource: "pod", + Namespace: pod.Namespace, + Name: pod.Name, + } + klog.V(4).Infof("Examining %s", ref) - klog.V(4).Infof("Adding reference from stream %s to %s", getName(stream), cn.Describe()) - switch cn.Type { - case imagegraph.ImageComponentTypeConfig: - p.g.AddEdge(imageStreamNode, s, ReferencedImageConfigEdgeKind) - case imagegraph.ImageComponentTypeLayer: - p.g.AddEdge(imageStreamNode, s, ReferencedImageLayerEdgeKind) - case imagegraph.ImageComponentTypeManifest: - p.g.AddEdge(imageStreamNode, s, ReferencedImageManifestEdgeKind) - default: - utilruntime.HandleError(fmt.Errorf("internal error: unhandled image component type %q", cn.Type)) - } - } + // A pod is only *excluded* from being added to the graph if its phase is not + // pending or running. Additionally, it has to be at least as old as the minimum + // age threshold defined by the algorithm. + if pod.Status.Phase != corev1.PodRunning && pod.Status.Phase != corev1.PodPending { + if pod.CreationTimestamp.Time.Before(p.algorithm.keepYoungerThan) { + klog.V(4).Infof("%s: ignoring pod because it's not running nor pending and is too old", ref) + continue } } + + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &pod.Spec)...) + } + return errs +} + +// analyzeReferencesFromReplicationControllers finds references to imagestreams +// from replication controllers. +func (p *pruner) analyzeReferencesFromReplicationControllers(rcs *corev1.ReplicationControllerList) []error { + var errs []error + for _, rc := range rcs.Items { + ref := resourceReference{ + Resource: "replicationcontroller", + Namespace: rc.Namespace, + Name: rc.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &rc.Spec.Template.Spec)...) } + return errs +} - return nil +// analyzeReferencesFromDeploymentConfigs finds references to imagestreams from +// deployment configs. +func (p *pruner) analyzeReferencesFromDeploymentConfigs(dcs *appsv1.DeploymentConfigList) []error { + var errs []error + for _, dc := range dcs.Items { + ref := resourceReference{ + Resource: "deploymentconfig", + Namespace: dc.Namespace, + Name: dc.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &dc.Spec.Template.Spec)...) + } + return errs +} + +// analyzeReferencesFromReplicaSets finds references to imagestreams from +// replica sets. +func (p *pruner) analyzeReferencesFromReplicaSets(rss *kappsv1.ReplicaSetList) []error { + var errs []error + for _, rs := range rss.Items { + ref := resourceReference{ + Resource: "replicaset", + Namespace: rs.Namespace, + Name: rs.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &rs.Spec.Template.Spec)...) + } + return errs +} + +// analyzeReferencesFromDeployments finds references to imagestreams from +// deployments. +func (p *pruner) analyzeReferencesFromDeployments(deploys *kappsv1.DeploymentList) []error { + var errs []error + for _, deploy := range deploys.Items { + ref := resourceReference{ + Resource: "deployment", + Namespace: deploy.Namespace, + Name: deploy.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &deploy.Spec.Template.Spec)...) + } + return errs +} + +// analyzeReferencesFromDaemonSets finds references to imagestreams from daemon +// sets. +func (p *pruner) analyzeReferencesFromDaemonSets(dss *kappsv1.DaemonSetList) []error { + var errs []error + for _, ds := range dss.Items { + ref := resourceReference{ + Resource: "daemonset", + Namespace: ds.Namespace, + Name: ds.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromPodSpec(ref, &ds.Spec.Template.Spec)...) + } + return errs +} + +// analyzeReferencesFromBuilds finds references to imagestreams from builds. +func (p *pruner) analyzeReferencesFromBuilds(builds *buildv1.BuildList) []error { + var errs []error + for _, build := range builds.Items { + ref := resourceReference{ + Resource: "build", + Namespace: build.Namespace, + Name: build.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromBuildStrategy(ref, build.Spec.Strategy)...) + } + return errs +} + +// analyzeReferencesFromBuildConfigs finds references to imagestreams from +// build configs. +func (p *pruner) analyzeReferencesFromBuildConfigs(bcs *buildv1.BuildConfigList) []error { + var errs []error + for _, bc := range bcs.Items { + ref := resourceReference{ + Resource: "buildconfig", + Namespace: bc.Namespace, + Name: bc.Name, + } + klog.V(4).Infof("Examining %s", ref) + errs = append(errs, p.analyzeReferencesFromBuildStrategy(ref, bc.Spec.Strategy)...) + } + return errs +} + +func getValue(option interface{}) string { + if v := reflect.ValueOf(option); !v.IsNil() { + return fmt.Sprintf("%v", v.Elem()) + } + return "" } // exceedsLimits checks if given image exceeds LimitRanges defined in ImageStream's namespace. @@ -492,9 +708,6 @@ func exceedsLimits(is *imagev1.ImageStream, image *imagev1.Image, limits map[str return false } - if err := imageutil.ImageWithMetadata(image); err != nil { - return false - } dockerImage, ok := image.DockerImageMetadata.Object.(*dockerv10.DockerImage) if !ok { return false @@ -515,8 +728,8 @@ func exceedsLimits(is *imagev1.ImageStream, image *imagev1.Image, limits map[str } if limitQuantity.Cmp(*imageSize) < 0 { // image size is larger than the permitted limit range max size - klog.V(4).Infof("Image %s in stream %s exceeds limit %s: %v vs %v", - image.Name, getName(is), limitRange.Name, *imageSize, limitQuantity) + klog.V(4).Infof("Image %s in stream %s/%s exceeds limit %s: %v vs %v", + image.Name, is.Namespace, is.Name, limitRange.Name, *imageSize, limitQuantity) return true } } @@ -524,1083 +737,491 @@ func exceedsLimits(is *imagev1.ImageStream, image *imagev1.Image, limits map[str return false } -// addPodsToGraph adds pods to the graph. -// -// Edges are added to the graph from each pod to the images specified by that -// pod's list of containers, as long as the image is managed by OpenShift. -func (p *pruner) addPodsToGraph(pods *corev1.PodList) []error { - var errs []error +func getImageBlobs(image *imagev1.Image) ([]string, error) { + blobs := make([]string, 0, len(image.DockerImageLayers)+1) + + for _, layer := range image.DockerImageLayers { + blobs = append(blobs, layer.Name) + } + + dockerImage, ok := image.DockerImageMetadata.Object.(*dockerv10.DockerImage) + if !ok { + return blobs, fmt.Errorf("failed to read metadata for image %s", image.Name) + } - for i := range pods.Items { - pod := &pods.Items[i] + if image.DockerImageManifestMediaType == schema2.MediaTypeManifest && len(dockerImage.ID) > 0 { + configName := dockerImage.ID + blobs = append(blobs, configName) + } - desc := fmt.Sprintf("Pod %s", getName(pod)) - klog.V(4).Infof("Examining %s", desc) + return blobs, nil +} - // A pod is only *excluded* from being added to the graph if its phase is not - // pending or running. Additionally, it has to be at least as old as the minimum - // age threshold defined by the algorithm. - if pod.Status.Phase != corev1.PodRunning && pod.Status.Phase != corev1.PodPending { - if !pod.CreationTimestamp.Time.After(p.algorithm.keepYoungerThan) { - klog.V(4).Infof("Ignoring %s for image reference counting because it's not running/pending and is too old", desc) +type stringsCounter struct { + mutex sync.Mutex + counts map[string]int +} + +func newStringsCounter() *stringsCounter { + return &stringsCounter{ + counts: make(map[string]int), + } +} + +func (c *stringsCounter) Add(key string, delta int) int { + c.mutex.Lock() + defer c.mutex.Unlock() + c.counts[key] += delta + return c.counts[key] +} + +type referenceCounts struct { + Blobs *stringsCounter + Manifests *stringsCounter +} + +func newReferenceCounts() referenceCounts { + return referenceCounts{ + Blobs: newStringsCounter(), + Manifests: newStringsCounter(), + } +} + +func (p *pruner) getRepositoryReferenceCounts(is *imagev1.ImageStream) (referenceCounts, error) { + counts := newReferenceCounts() + for _, tagEventList := range is.Status.Tags { + for _, tagEvent := range tagEventList.Items { + image, ok := p.images[tagEvent.Image] + if !ok { + klog.Warningf("imagestream %s/%s: tag %s: image %s not found", is.Namespace, is.Name, tagEventList.Tag, tagEvent.Image) continue } - } - klog.V(4).Infof("Adding %s to graph", desc) - podNode := kubegraph.EnsurePodNode(p.g, pod) + counts.Manifests.Add(image.Name, 1) - errs = append(errs, p.addPodSpecToGraph(getRef(pod), &pod.Spec, podNode)...) + imageBlobs, err := getImageBlobs(image) + if err != nil { + return counts, fmt.Errorf("tag %s: image %s: %v", tagEventList.Tag, tagEvent.Image, err) + } + for _, blob := range imageBlobs { + counts.Blobs.Add(blob, 1) + } + } } - - return errs + return counts, nil } -// Edges are added to the graph from each predecessor (pod or replication -// controller) to the images specified by the pod spec's list of containers, as -// long as the image is managed by OpenShift. -func (p *pruner) addPodSpecToGraph(referrer *corev1.ObjectReference, spec *corev1.PodSpec, predecessor gonum.Node) []error { - var errs []error +func (p *pruner) getGlobalReferenceCounts(images map[string]*imagev1.Image) (referenceCounts, error) { + counts := newReferenceCounts() + for _, image := range images { + counts.Manifests.Add(image.Name, 1) + counts.Blobs.Add(image.Name, 1) - for j := range spec.Containers { - container := spec.Containers[j] + imageBlobs, err := getImageBlobs(image) + if err != nil { + return counts, fmt.Errorf("image %s: %v", image.Name, err) + } + for _, blob := range imageBlobs { + counts.Blobs.Add(blob, 1) + } + } + return counts, nil +} - if len(strings.TrimSpace(container.Image)) == 0 { - klog.V(4).Infof("Ignoring edge from %s because container has no reference to image", getKindName(referrer)) +func (p *pruner) pruneImageStreamTag(is *imagev1.ImageStream, tagEventList imagev1.NamedTagEventList, counts referenceCounts, layerLinkDeleter LayerLinkDeleter) (imagev1.NamedTagEventList, int, []string, []error) { + filteredItems := tagEventList.Items[:0] + var manifestsToDelete []string + var errs []error + for rev, item := range tagEventList.Items { + if item.Created.After(p.algorithm.keepYoungerThan) { + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: keeping %s because of --keep-younger-than", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image) + filteredItems = append(filteredItems, item) continue } - klog.V(4).Infof("Examining container image %q", container.Image) - - ref, err := reference.Parse(container.Image) - if err != nil { - klog.Warningf("Unable to parse DockerImageReference %q of %s: %v - skipping", container.Image, getKindName(referrer), err) - if !p.ignoreInvalidRefs { - errs = append(errs, newErrBadReferenceToImage(container.Image, referrer, err.Error())) + if rev == 0 { + istag := imageStreamTagReference{ + Namespace: is.Namespace, + Name: is.Name, + Tag: tagEventList.Tag, + } + if usedBy := p.usedTags[istag]; len(usedBy) > 0 { + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: keeping %s because tag is used by %s", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image, referencesSample(usedBy)) + filteredItems = append(filteredItems, item) + continue } + } + + image, ok := p.images[item.Image] + if !ok { + // There are few options why the image may not be found: + // 1. the image is deleted manually and this record is no longer valid + // 2. the imagestream was observed before the image creation, i.e. + // this record was created recently and it should be protected + // by keepYoungerThan + klog.Infof("imagestream %s/%s: tag %s: revision %d: image %s not found, deleting...", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image) continue } - if len(ref.ID) == 0 { - // Attempt to dereference istag. Since we cannot be sure whether the reference refers to the - // integrated registry or not, we ignore the host part completely. As a consequence, we may keep - // image otherwise sentenced for a removal just because its pull spec accidentally matches one of - // our imagestreamtags. - - // set the tag if empty - ref = ref.DockerClientDefaults() - klog.V(4).Infof("%q has no image ID", container.Image) - node := p.g.Find(imagegraph.ImageStreamTagNodeName(makeISTag(ref.Namespace, ref.Name, ref.Tag))) - if node == nil { - klog.V(4).Infof("No image stream tag found for %q - skipping", container.Image) + if p.algorithm.pruneOverSizeLimit { + if !exceedsLimits(is, image, p.imageStreamLimits) { + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: keeping %s because --prune-over-size-limit is used and image does not exceed limits", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image) + filteredItems = append(filteredItems, item) continue } - for _, n := range p.g.From(node) { - imgNode, ok := n.(*imagegraph.ImageNode) - if !ok { - continue - } - klog.V(4).Infof("Adding edge from pod to image %q referenced by %s:%s", imgNode.Image.Name, ref.RepositoryName(), ref.Tag) - p.g.AddEdge(predecessor, imgNode, ReferencedImageEdgeKind) + } else { + if rev < p.algorithm.keepTagRevisions { + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: keeping %s because of --keep-tag-revisions", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image) + filteredItems = append(filteredItems, item) + continue } - continue } - imageNode := imagegraph.FindImage(p.g, ref.ID) - if imageNode == nil { - klog.V(2).Infof("Unable to find image %q referenced by %s in the graph - skipping", ref.ID, getKindName(referrer)) + isimage := imageStreamImageReference{ + Namespace: is.Namespace, + Name: is.Name, + Digest: item.Image, + } + if usedBy := p.usedImages[isimage]; len(usedBy) > 0 { + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: keeping %s because image is used by %s", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image, referencesSample(usedBy)) + filteredItems = append(filteredItems, item) continue } - klog.V(4).Infof("Adding edge from %s to image %v", getKindName(referrer), imageNode) - p.g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind) + klog.V(4).Infof("imagestream %s/%s: tag %s: revision %d: deleting repository links for %s...", is.Namespace, is.Name, tagEventList.Tag, rev+1, item.Image) + + if p.algorithm.pruneRegistry { + if counts.Manifests.Add(image.Name, -1) == 0 { + manifestsToDelete = append(manifestsToDelete, image.Name) + } + + imageBlobs, err := getImageBlobs(image) + if err != nil { + klog.Warningf("imagestream %s/%s: tag %s: image %s: %s", is.Namespace, is.Name, tagEventList.Tag, item.Image, err) + } + for _, blob := range imageBlobs { + if counts.Blobs.Add(blob, -1) == 0 { + err := layerLinkDeleter.DeleteLayerLink(fmt.Sprintf("%s/%s", is.Namespace, is.Name), blob) + if err != nil { + errs = append(errs, fmt.Errorf("failed to delete layer link %s: %v", blob, err)) + } + } + } + } } - return errs + deletedItems := len(tagEventList.Items) - len(filteredItems) + + tagEventList.Items = filteredItems + + return tagEventList, deletedItems, manifestsToDelete, errs } -// addReplicationControllersToGraph adds replication controllers to the graph. -// -// Edges are added to the graph from each replication controller to the images -// specified by its pod spec's list of containers, as long as the image is -// managed by OpenShift. -func (p *pruner) addReplicationControllersToGraph(rcs *corev1.ReplicationControllerList) []error { - var errs []error +func (p *pruner) pruneImageStream(stream *imagev1.ImageStream, imageStreamDeleter ImageStreamDeleter, layerLinkDeleter LayerLinkDeleter, manifestDeleter ManifestDeleter) (*imagev1.ImageStream, *PruneStats, []error) { + klog.V(4).Infof("Examining ImageStream %s/%s", stream.Namespace, stream.Name) - for i := range rcs.Items { - rc := &rcs.Items[i] - desc := fmt.Sprintf("ReplicationController %s", getName(rc)) - klog.V(4).Infof("Examining %s", desc) - rcNode := kubegraph.EnsureReplicationControllerNode(p.g, rc) - errs = append(errs, p.addPodSpecToGraph(getRef(rc), &rc.Spec.Template.Spec, rcNode)...) + if !p.algorithm.pruneOverSizeLimit && stream.CreationTimestamp.Time.After(p.algorithm.keepYoungerThan) { + klog.V(4).Infof("imagestream %s/%s: keeping all images because of --keep-younger-than", stream.Namespace, stream.Name) + return stream, &PruneStats{}, nil } - return errs -} - -// addDaemonSetsToGraph adds daemon set to the graph. -// -// Edges are added to the graph from each daemon set to the images specified by its pod spec's list of -// containers, as long as the image is managed by OpenShift. -func (p *pruner) addDaemonSetsToGraph(dss *kappsv1.DaemonSetList) []error { - var errs []error - - for i := range dss.Items { - ds := &dss.Items[i] - desc := fmt.Sprintf("DaemonSet %s", getName(ds)) - klog.V(4).Infof("Examining %s", desc) - dsNode := kubegraph.EnsureDaemonSetNode(p.g, ds) - errs = append(errs, p.addPodSpecToGraph(getRef(ds), &ds.Spec.Template.Spec, dsNode)...) - } - - return errs -} - -// addDeploymentsToGraph adds kube's deployments to the graph. -// -// Edges are added to the graph from each deployment to the images specified by its pod spec's list of -// containers, as long as the image is managed by OpenShift. -func (p *pruner) addDeploymentsToGraph(dmnts *kappsv1.DeploymentList) []error { - var errs []error - - for i := range dmnts.Items { - d := &dmnts.Items[i] - ref := getRef(d) - klog.V(4).Infof("Examining %s", getKindName(ref)) - dNode := kubegraph.EnsureDeploymentNode(p.g, d) - errs = append(errs, p.addPodSpecToGraph(ref, &d.Spec.Template.Spec, dNode)...) - } - - return errs -} - -// addDeploymentConfigsToGraph adds deployment configs to the graph. -// -// Edges are added to the graph from each deployment config to the images -// specified by its pod spec's list of containers, as long as the image is -// managed by OpenShift. -func (p *pruner) addDeploymentConfigsToGraph(dcs *appsv1.DeploymentConfigList) []error { - var errs []error - - for i := range dcs.Items { - dc := &dcs.Items[i] - ref := getRef(dc) - klog.V(4).Infof("Examining %s", getKindName(ref)) - dcNode := appsgraph.EnsureDeploymentConfigNode(p.g, dc) - errs = append(errs, p.addPodSpecToGraph(getRef(dc), &dc.Spec.Template.Spec, dcNode)...) - } - - return errs -} - -// addReplicaSetsToGraph adds replica set to the graph. -// -// Edges are added to the graph from each replica set to the images specified by its pod spec's list of -// containers, as long as the image is managed by OpenShift. -func (p *pruner) addReplicaSetsToGraph(rss *kappsv1.ReplicaSetList) []error { - var errs []error - - for i := range rss.Items { - rs := &rss.Items[i] - ref := getRef(rs) - klog.V(4).Infof("Examining %s", getKindName(ref)) - rsNode := kubegraph.EnsureReplicaSetNode(p.g, rs) - errs = append(errs, p.addPodSpecToGraph(ref, &rs.Spec.Template.Spec, rsNode)...) - } - - return errs -} - -// addBuildConfigsToGraph adds build configs to the graph. -// -// Edges are added to the graph from each build config to the image specified by its strategy.from. -func (p *pruner) addBuildConfigsToGraph(bcs *buildv1.BuildConfigList) []error { - var errs []error - - for i := range bcs.Items { - bc := &bcs.Items[i] - ref := getRef(bc) - klog.V(4).Infof("Examining %s", getKindName(ref)) - bcNode := buildgraph.EnsureBuildConfigNode(p.g, bc) - errs = append(errs, p.addBuildStrategyImageReferencesToGraph(ref, bc.Spec.Strategy, bcNode)...) - } - - return errs -} + collectingLayerLinkDeleter := newCollectingLayerLinkDeleter(layerLinkDeleter) -// addBuildsToGraph adds builds to the graph. -// -// Edges are added to the graph from each build to the image specified by its strategy.from. -func (p *pruner) addBuildsToGraph(builds *buildv1.BuildList) []error { + var manifestsToDelete []string var errs []error + var deletedItems int + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + manifestsToDelete = nil - for i := range builds.Items { - build := &builds.Items[i] - ref := getRef(build) - klog.V(4).Infof("Examining %s", getKindName(ref)) - buildNode := buildgraph.EnsureBuildNode(p.g, build) - errs = append(errs, p.addBuildStrategyImageReferencesToGraph(ref, build.Spec.Strategy, buildNode)...) - } - - return errs -} - -// resolveISTagName parses and tries to find it in the graph. If the parsing fails, -// an error is returned. If the istag cannot be found, nil is returned. -func (p *pruner) resolveISTagName(g genericgraph.Graph, referrer *corev1.ObjectReference, istagName string) (*imagegraph.ImageStreamTagNode, error) { - name, tag, err := imageutil.ParseImageStreamTagName(istagName) - if err != nil { - if p.ignoreInvalidRefs { - klog.Warningf("Failed to parse ImageStreamTag name %q: %v", istagName, err) - return nil, nil - } - return nil, newErrBadReferenceTo("ImageStreamTag", istagName, referrer, err.Error()) - } - node := g.Find(imagegraph.ImageStreamTagNodeName(makeISTag(referrer.Namespace, name, tag))) - if istNode, ok := node.(*imagegraph.ImageStreamTagNode); ok { - return istNode, nil - } - - return nil, nil -} - -// addBuildStrategyImageReferencesToGraph ads references from the build strategy's parent node to the image -// the build strategy references. -// -// Edges are added to the graph from each predecessor (build or build config) -// to the image specified by strategy.from, as long as the image is managed by -// OpenShift. -func (p *pruner) addBuildStrategyImageReferencesToGraph(referrer *corev1.ObjectReference, strategy buildv1.BuildStrategy, predecessor gonum.Node) []error { - from := buildutil.GetInputReference(strategy) - if from == nil { - klog.V(4).Infof("Unable to determine 'from' reference - skipping") - return nil - } - - klog.V(4).Infof("Examining build strategy with from: %#v", from) - - var imageID string - - switch from.Kind { - case "DockerImage": - if len(strings.TrimSpace(from.Name)) == 0 { - klog.V(4).Infof("Ignoring edge from %s because build strategy has no reference to image", getKindName(referrer)) - return nil - } - ref, err := reference.Parse(from.Name) + is, err := imageStreamDeleter.GetImageStream(stream) if err != nil { - klog.Warningf("Failed to parse DockerImage name %q of %s: %v", from.Name, getKindName(referrer), err) - if !p.ignoreInvalidRefs { - return []error{newErrBadReferenceToImage(from.Name, referrer, err.Error())} + if kerrapi.IsNotFound(err) { + klog.V(4).Infof("imagestream %s/%s: skipping because it does not exist anymore", stream.Namespace, stream.Name) + stream = nil + return nil } - return nil + return err } - imageID = ref.ID + stream = is - case "ImageStreamImage": - _, id, err := imageutil.ParseImageStreamImageName(from.Name) + counts, err := p.getRepositoryReferenceCounts(is) if err != nil { - klog.Warningf("Failed to parse ImageStreamImage name %q of %s: %v", from.Name, getKindName(referrer), err) - if !p.ignoreInvalidRefs { - return []error{newErrBadReferenceTo("ImageStreamImage", from.Name, referrer, err.Error())} - } + klog.Warningf("imagestream %s/%s: %v", is.Namespace, is.Name, err) return nil } - imageID = id - case "ImageStreamTag": - istNode, err := p.resolveISTagName(p.g, referrer, from.Name) - if err != nil { - klog.V(4).Infof(err.Error()) - return []error{err} - } - if istNode == nil { - klog.V(2).Infof("%s referenced by %s could not be found", getKindName(from), getKindName(referrer)) - return nil + deletedItems = 0 + for i, tagEventList := range is.Status.Tags { + updatedTagEventList, deletedTagItems, tagManifestsToDelete, tagErrs := p.pruneImageStreamTag(is, tagEventList, counts, collectingLayerLinkDeleter) + is.Status.Tags[i] = updatedTagEventList + deletedItems += deletedTagItems + manifestsToDelete = append(manifestsToDelete, tagManifestsToDelete...) + errs = append(errs, tagErrs...) } - for _, n := range p.g.From(istNode) { - imgNode, ok := n.(*imagegraph.ImageNode) - if !ok { - continue - } - imageID = imgNode.Image.Name - break - } - if len(imageID) == 0 { - klog.V(4).Infof("No image referenced by %s found", getKindName(from)) - return nil - } - - default: - klog.V(4).Infof("Ignoring unrecognized source location %q in %s", getKindName(from), getKindName(referrer)) - return nil - } - - klog.V(4).Infof("Looking for image %q in graph", imageID) - imageNode := imagegraph.FindImage(p.g, imageID) - if imageNode == nil { - klog.V(2).Infof("Unable to find image %q in graph referenced by %s - skipping", imageID, getKindName(referrer)) - return nil - } - - klog.V(4).Infof("Adding edge from %s to image %s", predecessor, imageNode.Image.Name) - p.g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind) - - return nil -} - -func (p *pruner) handleImageStreamEvent(event watch.Event) { - getIsNode := func() (*imagev1.ImageStream, *imagegraph.ImageStreamNode) { - is, ok := event.Object.(*imagev1.ImageStream) - if !ok { - utilruntime.HandleError(fmt.Errorf("internal error: expected ImageStream object in %s event, not %T", event.Type, event.Object)) - return nil, nil - } - n := p.g.Find(imagegraph.ImageStreamNodeName(is)) - if isNode, ok := n.(*imagegraph.ImageStreamNode); ok { - return is, isNode - } - return is, nil - } - - // NOTE: an addition of an imagestream previously deleted from the graph is a noop due to a limitation of - // the current gonum/graph package - switch event.Type { - case watch.Added: - is, isNode := getIsNode() - if is == nil { - return - } - if isNode != nil { - klog.V(4).Infof("Ignoring added ImageStream %s that is already present in the graph", getName(is)) - return - } - klog.V(4).Infof("Adding ImageStream %s to the graph", getName(is)) - p.addImageStreamsToGraph(&imagev1.ImageStreamList{Items: []imagev1.ImageStream{*is}}, p.imageStreamLimits) - case watch.Modified: - is, isNode := getIsNode() - if is == nil { - return + // deleting tags without items + tags := is.Status.Tags[:0] + for i := range is.Status.Tags { + if len(is.Status.Tags[i].Items) > 0 { + tags = append(tags, is.Status.Tags[i]) + } } + is.Status.Tags = tags - if isNode != nil { - klog.V(4).Infof("Removing updated ImageStream %s from the graph", getName(is)) - // first remove the current node if present - p.g.RemoveNode(isNode) + if deletedItems > 0 { + updatedStream, err := imageStreamDeleter.UpdateImageStream(is, deletedItems) + if kerrapi.IsNotFound(err) { + klog.V(4).Infof("imagestream %s/%s: the image stream cannot be updated because it's gone", is.Namespace, is.Name) + stream = nil + return nil + } else if err != nil { + return err + } + stream = updatedStream } - klog.V(4).Infof("Adding updated ImageStream %s back to the graph", getName(is)) - p.addImageStreamsToGraph(&imagev1.ImageStreamList{Items: []imagev1.ImageStream{*is}}, p.imageStreamLimits) + return nil + }) + stats := &PruneStats{ + DeletedLayerLinks: collectingLayerLinkDeleter.DeletedLayerLinkCount(), } -} - -func (p *pruner) handleImageEvent(event watch.Event) { - getImageNode := func() (*imagev1.Image, *imagegraph.ImageNode) { - img, ok := event.Object.(*imagev1.Image) - if !ok { - utilruntime.HandleError(fmt.Errorf("internal error: expected Image object in %s event, not %T", event.Type, event.Object)) - return nil, nil - } - return img, imagegraph.FindImage(p.g, img.Name) + if err != nil { + errs = append(errs, err) + return stream, stats, errs } - switch event.Type { - // NOTE: an addition of an image previously deleted from the graph is a noop due to a limitation of the - // current gonum/graph package - case watch.Added: - img, imgNode := getImageNode() - if img == nil { - return - } - if imgNode != nil { - klog.V(4).Infof("Ignoring added Image %s that is already present in the graph", img.Name) - return - } - klog.V(4).Infof("Adding new Image %s to the graph", img.Name) - p.addImagesToGraph(&imagev1.ImageList{Items: []imagev1.Image{*img}}) - - case watch.Deleted: - img, imgNode := getImageNode() - if imgNode == nil { - klog.V(4).Infof("Ignoring event for deleted Image %s that is not present in the graph", img.Name) - return - } - klog.V(4).Infof("Removing deleted image %s from the graph", img.Name) - p.g.RemoveNode(imgNode) + if deletedItems > 0 { + stats.DeletedImageStreamTagItems = deletedItems + stats.UpdatedImageStreams = 1 } -} -// getImageNodes returns only nodes of type ImageNode. -func getImageNodes(nodes []gonum.Node) map[string]*imagegraph.ImageNode { - ret := make(map[string]*imagegraph.ImageNode) - for i := range nodes { - if node, ok := nodes[i].(*imagegraph.ImageNode); ok { - ret[node.Image.Name] = node + if p.algorithm.pruneRegistry { + for _, digest := range manifestsToDelete { + err := manifestDeleter.DeleteManifest(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name), digest) + if err != nil { + errs = append(errs, fmt.Errorf("failed to delete manifest link %s: %v", digest, err)) + } else { + stats.DeletedManifestLinks++ + } } } - return ret -} -// edgeKind returns true if the edge from "from" to "to" is of the desired kind. -func edgeKind(g genericgraph.Graph, from, to gonum.Node, desiredKind string) bool { - edge := g.Edge(from, to) - kinds := g.EdgeKinds(edge) - return kinds.Has(desiredKind) + return stream, stats, errs } -// imageIsPrunable returns true if the image node only has weak references -// from its predecessors to it. A weak reference to an image is a reference -// from an image stream to an image where the image is not the current image -// for a tag and the image stream is at least as old as the minimum pruning -// age. -func imageIsPrunable(g genericgraph.Graph, imageNode *imagegraph.ImageNode, algorithm pruneAlgorithm) bool { - if !algorithm.allImages { - if imageNode.Image.Annotations[imagev1.ManagedByOpenShiftAnnotation] != "true" { - klog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", - imageNode.Image.Name, imageNode.Image.DockerImageReference) - return false - } - } +func (p *pruner) pruneImage(image *imagev1.Image, usedImages map[string]bool, counts referenceCounts, blobDeleter BlobDeleter, imageDeleter ImageDeleter) (*PruneStats, []error) { + stats := &PruneStats{} - if !algorithm.pruneOverSizeLimit && imageNode.Image.CreationTimestamp.Time.After(algorithm.keepYoungerThan) { - klog.V(4).Infof("Image %q is younger than minimum pruning age", imageNode.Image.Name) - return false + if usedImages[image.Name] { + klog.V(4).Infof("image %s: keeping because it is used by imagestreams", image.Name) + return stats, nil } - for _, n := range g.To(imageNode) { - klog.V(4).Infof("Examining predecessor %#v", n) - if edgeKind(g, n, imageNode, ReferencedImageEdgeKind) { - klog.V(4).Infof("Strong reference detected") - return false + if !p.algorithm.allImages { + if image.Annotations[imagev1.ManagedByOpenShiftAnnotation] != "true" { + klog.V(4).Infof("image %s: keeping external image because --all=false", image.Name) + return stats, nil } } - return true -} - -func calculatePrunableImages( - g genericgraph.Graph, - imageNodes map[string]*imagegraph.ImageNode, - algorithm pruneAlgorithm, -) []*imagegraph.ImageNode { - prunable := []*imagegraph.ImageNode{} - - for _, imageNode := range imageNodes { - klog.V(4).Infof("Examining image %q", imageNode.Image.Name) - - if imageIsPrunable(g, imageNode, algorithm) { - klog.V(4).Infof("Image %q is prunable", imageNode.Image.Name) - prunable = append(prunable, imageNode) - } + if !p.algorithm.pruneOverSizeLimit && image.CreationTimestamp.Time.After(p.algorithm.keepYoungerThan) { + klog.V(4).Infof("image %s: keeping because of --keep-younger-than", image.Name) + return stats, nil } - return prunable -} - -// pruneStreams removes references from all image streams' status.tags entries to prunable images, invoking -// streamPruner.UpdateImageStream for each updated stream. -func pruneStreams( - g genericgraph.Graph, - prunableImageNodes []*imagegraph.ImageNode, - streamPruner ImageStreamDeleter, - keepYoungerThan time.Time, -) (deletions []Deletion, failures []Failure) { - imageNameToNode := map[string]*imagegraph.ImageNode{} - for _, node := range prunableImageNodes { - imageNameToNode[node.Image.Name] = node - } + klog.V(4).Infof("image %s: deleting...", image.Name) - noChangeErr := errors.New("nothing changed") + var errs []error + failures := 0 - klog.V(4).Infof("Removing pruned image references from streams") - for _, node := range g.Nodes() { - streamNode, ok := node.(*imagegraph.ImageStreamNode) - if !ok { - continue + if p.algorithm.pruneRegistry { + imageBlobs, err := getImageBlobs(image) + if err != nil { + return stats, []error{err} } - streamName := getName(streamNode.ImageStream) - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - stream, err := streamPruner.GetImageStream(streamNode.ImageStream) - if err != nil { - if kerrapi.IsNotFound(err) { - klog.V(4).Infof("Unable to get image stream %s: removed during prune", streamName) - return noChangeErr - } - return err - } - updatedTags := sets.NewString() - deletedTags := sets.NewString() - - for _, tag := range stream.Status.Tags { - if updated, deleted := pruneISTagHistory(g, imageNameToNode, keepYoungerThan, streamName, stream, tag.Tag); deleted { - deletedTags.Insert(tag.Tag) - } else if updated { - updatedTags.Insert(tag.Tag) + for _, blob := range imageBlobs { + if counts.Blobs.Add(blob, -1) == 0 { + err := blobDeleter.DeleteBlob(blob) + if err != nil { + failures++ + errs = append(errs, fmt.Errorf("failed to delete blob %s: %v", blob, err)) + } else { + stats.DeletedBlobs++ } } - - if updatedTags.Len() == 0 && deletedTags.Len() == 0 { - return noChangeErr - } - - updatedStream, err := streamPruner.UpdateImageStream(stream) - if err == nil { - streamPruner.NotifyImageStreamPrune(stream, updatedTags.List(), deletedTags.List()) - streamNode.ImageStream = updatedStream - } - - if kerrapi.IsNotFound(err) { - klog.V(4).Infof("Unable to update image stream %s: removed during prune", streamName) - return nil - } - - return err - }) - - if err == noChangeErr { - continue } - if err != nil { - failures = append(failures, Failure{Node: streamNode, Err: err}) - } else { - deletions = append(deletions, Deletion{Node: streamNode}) - } - } - - klog.V(4).Infof("Done removing pruned image references from streams") - return -} - -// strengthenReferencesFromFailedImageStreams turns weak references between image streams and images to -// strong. This must be called right after the image stream pruning to prevent images that failed to be -// untagged from being pruned. -func strengthenReferencesFromFailedImageStreams(g genericgraph.Graph, failures []Failure) { - for _, f := range failures { - for _, n := range g.From(f.Node) { - imageNode, ok := n.(*imagegraph.ImageNode) - if !ok { - continue - } - edge := g.Edge(f.Node, imageNode) - if edge == nil { - continue - } - kinds := g.EdgeKinds(edge) - if kinds.Has(ReferencedImageEdgeKind) { - continue + if counts.Blobs.Add(image.Name, -1) == 0 { + err := blobDeleter.DeleteBlob(image.Name) + if err != nil { + failures++ + errs = append(errs, fmt.Errorf("failed to delete manifest blob %s: %v", image.Name, err)) + } else { + stats.DeletedBlobs++ } - g.RemoveEdge(edge) - g.AddEdge(f.Node, imageNode, ReferencedImageEdgeKind) } } -} - -// pruneISTagHistory processes tag event list of the given image stream tag. It removes references to images -// that are going to be removed or are missing in the graph. -func pruneISTagHistory( - g genericgraph.Graph, - prunableImageNodes map[string]*imagegraph.ImageNode, - keepYoungerThan time.Time, - streamName string, - imageStream *imagev1.ImageStream, - tag string, -) (tagUpdated, tagDeleted bool) { - history, _ := imageutil.StatusHasTag(imageStream, tag) - newHistory := imagev1.NamedTagEventList{Tag: tag} - - for _, tagEvent := range history.Items { - klog.V(4).Infof("Checking image stream tag %s:%s generation %d with image %q", streamName, tag, tagEvent.Generation, tagEvent.Image) - if ok, reason := tagEventIsPrunable(tagEvent, g, prunableImageNodes, keepYoungerThan); ok { - klog.V(4).Infof("Image stream tag %s:%s generation %d - removing because %s", streamName, tag, tagEvent.Generation, reason) - tagUpdated = true + if stats.DeletedBlobs > 0 || failures == 0 { + if err := imageDeleter.DeleteImage(image); err != nil { + errs = append(errs, fmt.Errorf("failed to delete image %s: %v", image.Name, err)) } else { - klog.V(4).Infof("Image stream tag %s:%s generation %d - keeping because %s", streamName, tag, tagEvent.Generation, reason) - newHistory.Items = append(newHistory.Items, tagEvent) - } - } - - if len(newHistory.Items) == 0 { - klog.V(4).Infof("Image stream tag %s:%s - removing empty tag", streamName, tag) - tags := []imagev1.NamedTagEventList{} - for i := range imageStream.Status.Tags { - t := imageStream.Status.Tags[i] - if t.Tag != tag { - tags = append(tags, t) - } - } - imageStream.Status.Tags = tags - tagDeleted = true - tagUpdated = false - } else if tagUpdated { - for i := range imageStream.Status.Tags { - t := imageStream.Status.Tags[i] - if t.Tag == tag { - imageStream.Status.Tags[i] = newHistory - break - } + stats.DeletedImages++ } } - return -} - -func tagEventIsPrunable( - tagEvent imagev1.TagEvent, - g genericgraph.Graph, - prunableImageNodes map[string]*imagegraph.ImageNode, - keepYoungerThan time.Time, -) (ok bool, reason string) { - if _, ok := prunableImageNodes[tagEvent.Image]; ok { - return true, fmt.Sprintf("image %q matches deleted image", tagEvent.Image) - } - - n := imagegraph.FindImage(g, tagEvent.Image) - if n != nil { - return false, fmt.Sprintf("image %q is not deleted", tagEvent.Image) - } - - if n == nil && !tagEvent.Created.After(keepYoungerThan) { - return true, fmt.Sprintf("image %q is absent", tagEvent.Image) - } - - return false, "the tag event is younger than threshold" -} - -// byLayerCountAndAge sorts a list of image nodes from the largest (by the number of image layers) to the -// smallest. Images with the same number of layers are ordered from the oldest to the youngest. -type byLayerCountAndAge []*imagegraph.ImageNode - -func (b byLayerCountAndAge) Len() int { return len(b) } -func (b byLayerCountAndAge) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func (b byLayerCountAndAge) Less(i, j int) bool { - fst, snd := b[i].Image, b[j].Image - if len(fst.DockerImageLayers) > len(snd.DockerImageLayers) { - return true - } - if len(fst.DockerImageLayers) < len(snd.DockerImageLayers) { - return false - } - - return fst.CreationTimestamp.Before(&snd.CreationTimestamp) || - (!snd.CreationTimestamp.Before(&fst.CreationTimestamp) && fst.Name < snd.Name) -} - -// nodeItem is an item of a doubly-linked list of image nodes. -type nodeItem struct { - node *imagegraph.ImageNode - prev, next *nodeItem + return stats, errs } -// pop removes the item from a doubly-linked list and returns the image node it holds and its former next -// neighbour. -func (i *nodeItem) pop() (node *imagegraph.ImageNode, next *nodeItem) { - n, p := i.next, i.prev - if p != nil { - p.next = n - } - if n != nil { - n.prev = p - } - return i.node, n -} - -// insertAfter makes a new list item from the given node and inserts it into the list right after the given -// item. The newly created item is returned. -func insertAfter(item *nodeItem, node *imagegraph.ImageNode) *nodeItem { - newItem := &nodeItem{ - node: node, - prev: item, - } - if item != nil { - if item.next != nil { - item.next.prev = newItem - newItem.next = item.next - } - item.next = newItem - } - return newItem -} - -// makeQueue makes a doubly-linked list of items out of the given array of image nodes. -func makeQueue(nodes []*imagegraph.ImageNode) *nodeItem { - var head, tail *nodeItem - for i, n := range nodes { - tail = insertAfter(tail, n) - if i == 0 { - head = tail - } +func (p *pruner) pruneImageStreams( + streamPruner ImageStreamDeleter, + layerLinkDeleter LayerLinkDeleter, + manifestDeleter ManifestDeleter, +) (*PruneStats, []error) { + var keys []string + for k := range p.imageStreams { + keys = append(keys, k) } - return head -} -// Prune prunes the objects like this: -// 1. it calculates the prunable images and builds a queue -// - the queue does not ever grow, it only shrinks (newly created images are not added) -// 2. it untags the prunable images from image streams -// 3. it spawns workers -// 4. it turns each prunable image into a job for the workers and makes sure they are busy -// 5. it terminates the workers once the queue is empty and reports results -func (p *pruner) Prune( - imagePrunerFactory ImagePrunerFactoryFunc, - streamPruner ImageStreamDeleter, - layerLinkPruner LayerLinkDeleter, - blobPruner BlobDeleter, - manifestPruner ManifestDeleter, -) (deletions []Deletion, failures []Failure) { - allNodes := p.g.Nodes() - - imageNodes := getImageNodes(allNodes) - prunable := calculatePrunableImages(p.g, imageNodes, p.algorithm) - - /* Instead of deleting streams in a per-image job, prune them all at once. Otherwise each image stream - * would have to be modified for each prunable image it contains. */ - deletions, failures = pruneStreams(p.g, prunable, streamPruner, p.algorithm.keepYoungerThan) - /* if namespace is specified, prune only ImageStreams and nothing more if we have any errors after - * ImageStreams pruning this may mean that we still have references to images. */ - if len(p.algorithm.namespace) > 0 || len(prunable) == 0 { - return deletions, failures - } - - strengthenReferencesFromFailedImageStreams(p.g, failures) - - // Sorting images from the largest (by number of layers) to the smallest is supposed to distribute the - // blob deletion workload equally across whole queue. - // If processed randomly, most probably, job processed in the beginning wouldn't delete any blobs (due to - // too many remaining referers) contrary to the jobs processed at the end. - // The assumption is based on another assumption that images with many layers have a low probability of - // sharing their components with other images. - sort.Sort(byLayerCountAndAge(prunable)) - p.queue = makeQueue(prunable) - - var ( - jobChan = make(chan *Job) - resultChan = make(chan JobResult) - ) - - defer close(jobChan) + var wg sync.WaitGroup + var mutex sync.Mutex + workQueue := make(chan string) + pruneStats := &PruneStats{} + errorsCh := make(chan error) for i := 0; i < p.numWorkers; i++ { - worker, err := NewWorker( - p.algorithm, - p.registryClientFactory, - p.registryURL, - imagePrunerFactory, - streamPruner, - layerLinkPruner, - blobPruner, - manifestPruner, - ) - if err != nil { - failures = append(failures, Failure{ - Err: fmt.Errorf("failed to initialize worker: %v", err), - }) - return - } - go worker.Run(jobChan, resultChan) - } - - ds, fs := p.runLoop(jobChan, resultChan) - deletions = append(deletions, ds...) - failures = append(failures, fs...) - - return -} - -// runLoop processes the queue of prunable images until empty. It makes the workers busy and updates the graph -// with each change. -func (p *pruner) runLoop( - jobChan chan<- *Job, - resultChan <-chan JobResult, -) (deletions []Deletion, failures []Failure) { - imgUpdateChan := p.imageWatcher.ResultChan() - isUpdateChan := p.imageStreamWatcher.ResultChan() - for { - // make workers busy - for len(p.processedImages) < p.numWorkers { - job, blocked := p.getNextJob() - if blocked { - break - } - if job == nil { - if len(p.processedImages) == 0 { - return + wg.Add(1) + go func() { + defer wg.Done() + for k := range workQueue { + mutex.Lock() + stream := p.imageStreams[k] + mutex.Unlock() + + updatedStream, stats, errs := p.pruneImageStream(stream, streamPruner, layerLinkDeleter, manifestDeleter) + + if updatedStream == nil { + mutex.Lock() + delete(p.imageStreams, k) + mutex.Unlock() + } else if updatedStream != stream { + mutex.Lock() + p.imageStreams[k] = updatedStream + mutex.Unlock() } - break - } - jobChan <- job - p.processedImages[job.Image] = job - } - select { - case res := <-resultChan: - p.updateGraphWithResult(&res) - for _, deletion := range res.Deletions { - deletions = append(deletions, deletion) - } - for _, failure := range res.Failures { - failures = append(failures, failure) - } - delete(p.processedImages, res.Job.Image) - case <-isUpdateChan: - // TODO: fix gonum/graph to not reuse IDs of deleted nodes and reenable event handling - //p.handleImageStreamEvent(event) - case <-imgUpdateChan: - // TODO: fix gonum/graph to not reuse IDs of deleted nodes and reenable event handling - //p.handleImageEvent(event) - } - } -} + pruneStats.Add(stats) -// getNextJob removes a prunable image from the queue, makes a job out of it and returns it. -// Image may be removed from the queue without being processed if it becomes not prunable (by being referred -// by a new image stream). Image may also be skipped and processed later when it is currently blocked. -// -// Image is blocked when at least one of its components is currently being processed in a running job and -// the component has either: -// - only one remaining strong reference from the blocked image (the other references are being currently -// removed) -// - only one remaining reference in an image stream, where the component is tagged (via image) (the other -// references are being currently removed) -// -// The concept of blocked images attempts to preserve image components until the very last image -// referencing them is deleted. Otherwise an image previously considered as prunable becomes not prunable may -// become not usable since its components have been removed already. -func (p *pruner) getNextJob() (job *Job, blocked bool) { - if p.queue == nil { - return - } - - pop := func(item *nodeItem) (*imagegraph.ImageNode, *nodeItem) { - node, next := item.pop() - if item == p.queue { - p.queue = next - } - return node, next + for _, err := range errs { + err = fmt.Errorf("imagestream %s/%s: %v", stream.Namespace, stream.Name, err) + klog.V(4).Info(err) + errorsCh <- err + } + } + }() } - for item := p.queue; item != nil; { - // something could have changed - if !imageIsPrunable(p.g, item.node, p.algorithm) { - _, item = pop(item) - continue + go func() { + for _, k := range keys { + workQueue <- k } + close(workQueue) + wg.Wait() + close(errorsCh) + }() - if components, blocked := getImageComponents(p.g, p.processedImages, item.node); !blocked { - job = &Job{ - Image: item.node, - Components: components, - } - _, item = pop(item) - break - } - item = item.next + var errs []error + for err := range errorsCh { + errs = append(errs, err) } - - blocked = job == nil && p.queue != nil - - return + return pruneStats, errs } -// updateGraphWithResult updates the graph with the result from completed job. Image nodes are deleted for -// each deleted image. Image components are deleted if they were removed from the global blob store. Unlinked -// imagecomponent (layer/config/manifest link) will cause an edge between image stream and the component to be -// deleted. -func (p *pruner) updateGraphWithResult(res *JobResult) { - imageDeleted := false - for _, d := range res.Deletions { - switch d.Node.(type) { - case *imagegraph.ImageNode: - imageDeleted = true - p.g.RemoveNode(d.Node) - case *imagegraph.ImageComponentNode: - // blob -> delete the node with all the edges - if d.Parent == nil { - p.g.RemoveNode(d.Node) - continue - } - - // link in a repository -> delete just edges - isn, ok := d.Parent.(*imagegraph.ImageStreamNode) - if !ok { - continue - } - edge := p.g.Edge(isn, d.Node) - if edge == nil { - continue +func (p *pruner) pruneImages( + blobDeleter BlobDeleter, + imageDeleter ImageDeleter, +) (*PruneStats, []error) { + usedImages := map[string]bool{} + for _, stream := range p.imageStreams { + for _, tag := range stream.Status.Tags { + for _, item := range tag.Items { + usedImages[item.Image] = true } - p.g.RemoveEdge(edge) - case *imagegraph.ImageStreamNode: - // ignore - default: - utilruntime.HandleError(fmt.Errorf("internal error: unhandled graph node %t", d.Node)) } } - if imageDeleted { - return - } -} - -// getImageComponents gathers image components with locations, where they can be removed at this time. -// Each component can be prunable in several image streams and in the global blob store. -func getImageComponents( - g genericgraph.Graph, - processedImages map[*imagegraph.ImageNode]*Job, - image *imagegraph.ImageNode, -) (components ComponentRetentions, blocked bool) { - components = make(ComponentRetentions) + pruneStats := &PruneStats{} - for _, node := range g.From(image) { - kinds := g.EdgeKinds(g.Edge(image, node)) - if len(kinds.Intersection(sets.NewString( - ReferencedImageLayerEdgeKind, - ReferencedImageConfigEdgeKind, - ReferencedImageManifestEdgeKind, - ))) == 0 { - continue - } - - imageStrongRefCounter := 0 - imageMarkedForDeletionCounter := 0 - referencingStreams := map[*imagegraph.ImageStreamNode]struct{}{} - referencingImages := map[*imagegraph.ImageNode]struct{}{} + counts, err := p.getGlobalReferenceCounts(p.images) + if err != nil { + return pruneStats, []error{err} + } - comp, ok := node.(*imagegraph.ImageComponentNode) - if !ok { - continue - } + var wg sync.WaitGroup + imagesToDelete := make(chan *imagev1.Image) + errorsCh := make(chan error) - for _, ref := range g.To(comp) { - switch t := ref.(type) { - case (*imagegraph.ImageNode): - imageStrongRefCounter++ - if _, processed := processedImages[t]; processed { - imageMarkedForDeletionCounter++ + for i := 0; i < p.numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for image := range imagesToDelete { + stats, errs := p.pruneImage(image, usedImages, counts, blobDeleter, imageDeleter) + + pruneStats.Add(stats) + + for _, err := range errs { + err = fmt.Errorf("image %s: %v", image.Name, err) + klog.V(4).Info(err) + errorsCh <- err } - referencingImages[t] = struct{}{} - - case *imagegraph.ImageStreamNode: - referencingStreams[t] = struct{}{} - - default: - continue } - } - - switch { - // the component is referenced only by the given image -> prunable globally - case imageStrongRefCounter < 2: - components.Add(comp, true) - // the component can be pruned once the other referencing image that is being deleted is finished; - // don't touch it until then - case imageStrongRefCounter-imageMarkedForDeletionCounter < 2: - return nil, true - // not prunable component - default: - components.Add(comp, false) - } - - if addComponentReferencingStreams( - g, - components, - referencingImages, - referencingStreams, - processedImages, - comp, - ) { - return nil, true - } + }() } - return -} - -// addComponentReferencingStreams records information about prunability of the given component in all the -// streams referencing it (via tagged image). It updates given components attribute. -func addComponentReferencingStreams( - g genericgraph.Graph, - components ComponentRetentions, - referencingImages map[*imagegraph.ImageNode]struct{}, - referencingStreams map[*imagegraph.ImageStreamNode]struct{}, - processedImages map[*imagegraph.ImageNode]*Job, - comp *imagegraph.ImageComponentNode, -) (blocked bool) { -streamLoop: - for stream := range referencingStreams { - refCounter := 0 - markedForDeletionCounter := 0 - - for image := range referencingImages { - edge := g.Edge(stream, image) - if edge == nil { - continue - } - kinds := g.EdgeKinds(edge) - // tagged not prunable image -> keep the component in the stream - if kinds.Has(ReferencedImageEdgeKind) { - components.AddReferencingStreams(comp, false, stream) - continue streamLoop - } - if !kinds.Has(WeakReferencedImageEdgeKind) { - continue - } - - refCounter++ - if _, processed := processedImages[image]; processed { - markedForDeletionCounter++ - } - - if refCounter-markedForDeletionCounter > 1 { - components.AddReferencingStreams(comp, false, stream) - continue streamLoop - } + go func() { + for _, image := range p.images { + imagesToDelete <- image } + close(imagesToDelete) + wg.Wait() + close(errorsCh) + }() - switch { - // there's just one remaining strong reference from the stream -> unlink - case refCounter < 2: - components.AddReferencingStreams(comp, true, stream) - // there's just one remaining strong reference and at least one another reference now being - // dereferenced in a running job -> wait until it completes - case refCounter-markedForDeletionCounter < 2: - return true - // not yet prunable - default: - components.AddReferencingStreams(comp, false, stream) - } + var errs []error + for err = range errorsCh { + errs = append(errs, err) } - - return false + return pruneStats, errs } -// imageComponentIsPrunable returns true if the image component is not referenced by any images. -func imageComponentIsPrunable(g genericgraph.Graph, cn *imagegraph.ImageComponentNode) bool { - for _, predecessor := range g.To(cn) { - klog.V(4).Infof("Examining predecessor %#v of image config %v", predecessor, cn) - if g.Kind(predecessor) == imagegraph.ImageNodeKind { - klog.V(4).Infof("Config %v has an image predecessor", cn) - return false - } - } +// Prune deletes historical items from image streams (image stream tag +// revisions) that are not protected by the pruner options and not used by the +// cluster objects. After that, it deletes images that are not used by image +// streams. +func (p *pruner) Prune( + streamPruner ImageStreamDeleter, + layerLinkDeleter LayerLinkDeleter, + manifestDeleter ManifestDeleter, + blobDeleter BlobDeleter, + imageDeleter ImageDeleter, +) (*PruneStats, kerrors.Aggregate) { + pruneStats := &PruneStats{} + var errs []error - return true -} + // Stage 1: delete history from image streams + stats, errors := p.pruneImageStreams(streamPruner, layerLinkDeleter, manifestDeleter) + pruneStats.Add(stats) + errs = append(errs, errors...) -// streamReferencingImageComponent returns a list of ImageStreamNodes that reference a -// given ImageComponentNode. -func streamsReferencingImageComponent(g genericgraph.Graph, cn *imagegraph.ImageComponentNode) []*imagegraph.ImageStreamNode { - ret := []*imagegraph.ImageStreamNode{} - for _, predecessor := range g.To(cn) { - if g.Kind(predecessor) != imagegraph.ImageStreamNodeKind { - continue - } - ret = append(ret, predecessor.(*imagegraph.ImageStreamNode)) + // Stage 2: delete images + if p.algorithm.namespace == "" { + stats, errors := p.pruneImages(blobDeleter, imageDeleter) + pruneStats.Add(stats) + errs = append(errs, errors...) } - return ret + return pruneStats, kerrors.NewAggregate(errs) } // imageDeleter removes an image from OpenShift. @@ -1627,8 +1248,6 @@ type imageStreamDeleter struct { streams imagev1client.ImageStreamsGetter } -var _ ImageStreamDeleter = &imageStreamDeleter{} - // NewImageStreamDeleter creates a new imageStreamDeleter. func NewImageStreamDeleter(streams imagev1client.ImageStreamsGetter) ImageStreamDeleter { return &imageStreamDeleter{ @@ -1640,8 +1259,8 @@ func (p *imageStreamDeleter) GetImageStream(stream *imagev1.ImageStream) (*image return p.streams.ImageStreams(stream.Namespace).Get(context.TODO(), stream.Name, metav1.GetOptions{}) } -func (p *imageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) { - klog.V(4).Infof("Updating ImageStream %s", getName(stream)) +func (p *imageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream, deletedItems int) (*imagev1.ImageStream, error) { + klog.V(4).Infof("Updating ImageStream %s/%s", stream.Namespace, stream.Name) is, err := p.streams.ImageStreams(stream.Namespace).UpdateStatus(context.TODO(), stream, metav1.UpdateOptions{}) if err == nil { klog.V(5).Infof("Updated ImageStream: %#v", is) @@ -1649,14 +1268,8 @@ func (p *imageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream) (*im return is, err } -// NotifyImageStreamPrune shows notification about updated image stream. -func (p *imageStreamDeleter) NotifyImageStreamPrune(stream *imagev1.ImageStream, updatedTags []string, deletedTags []string) { - return -} - // deleteFromRegistry uses registryClient to send a DELETE request to the -// provided url. It attempts an https request first; if that fails, it fails -// back to http. +// provided url. func deleteFromRegistry(registryClient *http.Client, url string) error { req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { @@ -1670,10 +1283,8 @@ func deleteFromRegistry(registryClient *http.Client, url string) error { } defer resp.Body.Close() - // TODO: investigate why we're getting non-existent layers, for now we're logging - // them out and continue working if resp.StatusCode == http.StatusNotFound { - klog.Warningf("Unable to prune layer %s, returned %v", url, resp.Status) + klog.V(4).Infof("Unable to delete layer %s, returned %v", url, resp.Status) return nil } @@ -1697,60 +1308,113 @@ func deleteFromRegistry(registryClient *http.Client, url string) error { return err } -// layerLinkDeleter removes a repository layer link from the registry. -type layerLinkDeleter struct{} +// remoteLayerLinkDeleter removes a repository layer link from the registry. +type remoteLayerLinkDeleter struct { + registryClient *http.Client + registryURL *url.URL +} -var _ LayerLinkDeleter = &layerLinkDeleter{} +func NewLayerLinkDeleter(registryClient *http.Client, registryURL *url.URL) LayerLinkDeleter { + return &remoteLayerLinkDeleter{ + registryClient: registryClient, + registryURL: registryURL, + } +} -// NewLayerLinkDeleter creates a new layerLinkDeleter. -func NewLayerLinkDeleter() LayerLinkDeleter { - return &layerLinkDeleter{} +func (p *remoteLayerLinkDeleter) DeleteLayerLink(repoName, linkName string) error { + klog.V(4).Infof("Deleting layer link %s from repository %s/%s", linkName, p.registryURL.Host, repoName) + return deleteFromRegistry(p.registryClient, fmt.Sprintf("%s/v2/%s/blobs/%s", p.registryURL.String(), repoName, linkName)) } -func (p *layerLinkDeleter) DeleteLayerLink(registryClient *http.Client, registryURL *url.URL, repoName, linkName string) error { - klog.V(4).Infof("Deleting layer link %s from repository %s/%s", linkName, registryURL.Host, repoName) - return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/blobs/%s", registryURL.String(), repoName, linkName)) +// collectingLayerLinkDeleter gathers information about which layers it has +// deleted and does not attempt to delete the same layer link twice. +type collectingLayerLinkDeleter struct { + LayerLinkDeleter + + mutex sync.Mutex + deleted map[string]bool + count int } -// blobDeleter removes a blob from the registry. -type blobDeleter struct{} +func newCollectingLayerLinkDeleter(deleter LayerLinkDeleter) *collectingLayerLinkDeleter { + return &collectingLayerLinkDeleter{ + LayerLinkDeleter: deleter, + deleted: make(map[string]bool), + } +} -var _ BlobDeleter = &blobDeleter{} +func (p *collectingLayerLinkDeleter) isDeleted(repoName, linkName string) bool { + key := fmt.Sprintf("%s@%s", repoName, linkName) -// NewBlobDeleter creates a new blobDeleter. -func NewBlobDeleter() BlobDeleter { - return &blobDeleter{} + p.mutex.Lock() + defer p.mutex.Unlock() + return p.deleted[key] } -func (p *blobDeleter) DeleteBlob(registryClient *http.Client, registryURL *url.URL, blob string) error { - klog.V(4).Infof("Deleting blob %s from registry %s", blob, registryURL.Host) - return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/blobs/%s", registryURL.String(), blob)) +func (p *collectingLayerLinkDeleter) markAsDeleted(repoName, linkName string) { + key := fmt.Sprintf("%s@%s", repoName, linkName) + + p.mutex.Lock() + defer p.mutex.Unlock() + p.deleted[key] = true + p.count++ } -// manifestDeleter deletes repository manifest data from the registry. -type manifestDeleter struct{} +func (p *collectingLayerLinkDeleter) DeletedLayerLinkCount() int { + p.mutex.Lock() + defer p.mutex.Unlock() + return p.count +} + +func (p *collectingLayerLinkDeleter) DeleteLayerLink(repoName, linkName string) error { + if p.isDeleted(repoName, linkName) { + return nil + } + + err := p.LayerLinkDeleter.DeleteLayerLink(repoName, linkName) + if err != nil { + return err + } -var _ ManifestDeleter = &manifestDeleter{} + p.markAsDeleted(repoName, linkName) + + return nil +} + +// manifestDeleter deletes repository manifest data from the registry. +type remoteManifestDeleter struct { + registryClient *http.Client + registryURL *url.URL +} // NewManifestDeleter creates a new manifestDeleter. -func NewManifestDeleter() ManifestDeleter { - return &manifestDeleter{} +func NewManifestDeleter(registryClient *http.Client, registryURL *url.URL) ManifestDeleter { + return &remoteManifestDeleter{ + registryClient: registryClient, + registryURL: registryURL, + } } -func (p *manifestDeleter) DeleteManifest(registryClient *http.Client, registryURL *url.URL, repoName, manifest string) error { - klog.V(4).Infof("Deleting manifest %s from repository %s/%s", manifest, registryURL.Host, repoName) - return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL.String(), repoName, manifest)) +func (p *remoteManifestDeleter) DeleteManifest(repoName, manifest string) error { + klog.V(4).Infof("Deleting manifest %s from repository %s/%s", manifest, p.registryURL.Host, repoName) + return deleteFromRegistry(p.registryClient, fmt.Sprintf("%s/v2/%s/manifests/%s", p.registryURL.String(), repoName, manifest)) } -func makeISTag(namespace, name, tag string) *imagev1.ImageStreamTag { - return &imagev1.ImageStreamTag{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: imageutil.JoinImageStreamTag(name, tag), - }, +// blobDeleter removes a blob from the registry. +type blobDeleter struct { + registryClient *http.Client + registryURL *url.URL +} + +// NewBlobDeleter creates a new blobDeleter. +func NewBlobDeleter(registryClient *http.Client, registryURL *url.URL) BlobDeleter { + return &blobDeleter{ + registryClient: registryClient, + registryURL: registryURL, } } -func makeISTagWithStream(is *imagev1.ImageStream, tag string) *imagev1.ImageStreamTag { - return makeISTag(is.Namespace, is.Name, tag) +func (p *blobDeleter) DeleteBlob(blob string) error { + klog.V(4).Infof("Deleting blob %s from registry %s", blob, p.registryURL.Host) + return deleteFromRegistry(p.registryClient, fmt.Sprintf("%s/admin/blobs/%s", p.registryURL.String(), blob)) } diff --git a/pkg/cli/admin/prune/imageprune/prune_test.go b/pkg/cli/admin/prune/imageprune/prune_test.go index 53c3489c0e..d078af14d1 100644 --- a/pkg/cli/admin/prune/imageprune/prune_test.go +++ b/pkg/cli/admin/prune/imageprune/prune_test.go @@ -9,8 +9,7 @@ import ( "net/http" "net/url" "reflect" - "regexp" - "sort" + "strings" "sync" "testing" "time" @@ -21,7 +20,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/rest/fake" clienttesting "k8s.io/client-go/testing" "k8s.io/klog/v2" @@ -31,65 +29,98 @@ import ( appsv1 "github.com/openshift/api/apps/v1" buildv1 "github.com/openshift/api/build/v1" imagev1 "github.com/openshift/api/image/v1" - fakeimageclient "github.com/openshift/client-go/image/clientset/versioned/fake" - imagev1client "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1" fakeimagev1client "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1/fake" - "github.com/openshift/oc/pkg/helpers/graph/genericgraph" - imagegraph "github.com/openshift/oc/pkg/helpers/graph/imagegraph/nodes" imagetest "github.com/openshift/oc/pkg/helpers/image/test" ) var logLevel = flag.Int("loglevel", 0, "") +func Images(images ...imagev1.Image) map[string]*imagev1.Image { + m := map[string]*imagev1.Image{} + for i := range images { + image := &images[i] + m[image.Name] = image + } + return m +} + +func Streams(streams ...imagev1.ImageStream) map[string]*imagev1.ImageStream { + m := map[string]*imagev1.ImageStream{} + for i := range streams { + stream := &streams[i] + m[fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)] = stream + } + return m +} + func TestImagePruning(t *testing.T) { var level klog.Level level.Set(fmt.Sprint(*logLevel)) registryHost := "registry.io" - registryURL := "https://" + registryHost tests := []struct { - name string - pruneOverSizeLimit *bool - allImages *bool - pruneRegistry *bool - ignoreInvalidRefs *bool - keepTagRevisions *int - namespace string - images imagev1.ImageList - pods corev1.PodList - streams imagev1.ImageStreamList - rcs corev1.ReplicationControllerList - bcs buildv1.BuildConfigList - builds buildv1.BuildList - dss kappsv1.DaemonSetList - deployments kappsv1.DeploymentList - dcs appsv1.DeploymentConfigList - rss kappsv1.ReplicaSetList - limits map[string][]*corev1.LimitRange - imageDeleterErr error - imageStreamDeleterErr error - layerDeleterErr error - manifestDeleterErr error - blobDeleterErrorGetter errorForSHA - expectedImageDeletions []string - expectedStreamUpdates []string - expectedLayerLinkDeletions []string - expectedManifestLinkDeletions []string - expectedBlobDeletions []string - expectedFailures []string - expectedErrorString string + name string + pruneOverSizeLimit *bool + allImages *bool + pruneRegistry *bool + ignoreInvalidRefs *bool + keepTagRevisions *int + namespace string + images map[string]*imagev1.Image + pods corev1.PodList + streams map[string]*imagev1.ImageStream + rcs corev1.ReplicationControllerList + bcs buildv1.BuildConfigList + builds buildv1.BuildList + dss kappsv1.DaemonSetList + deployments kappsv1.DeploymentList + dcs appsv1.DeploymentConfigList + rss kappsv1.ReplicaSetList + limits map[string][]*corev1.LimitRange + imageDeleterErr error + imageStreamDeleterErr error + layerDeleterErr error + manifestDeleterErr error + blobDeleterErrorGetter errorForSHA + expectedImageDeletions []string + expectedImageDeletionFailures []string + expectedStreamUpdates []string + expectedStreamUpdateFailures []string + expectedLayerLinkDeletions []string + expectedLayerLinkDeletionFailures []string + expectedManifestLinkDeletions []string + expectedManifestLinkDeletionFailures []string + expectedBlobDeletions []string + expectedBlobDeletionFailures []string + expectedErrorString string }{ { - name: "1 pod - phase pending - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "1 pod - phase pending - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.Pod("foo", "pod1", corev1.PodPending, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "3 pods - last phase pending - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "3 pods - last phase pending - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod2", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -99,15 +130,31 @@ func TestImagePruning(t *testing.T) { }, { - name: "1 pod - phase running - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "1 pod - phase running - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.Pod("foo", "pod1", corev1.PodRunning, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "3 pods - last phase running - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "3 pods - last phase running - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod2", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -117,272 +164,580 @@ func TestImagePruning(t *testing.T) { }, { - name: "pod phase succeeded - prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod phase succeeded - prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.Pod("foo", "pod1", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + imagetest.Layer1, + imagetest.Layer2, + imagetest.Layer3, + imagetest.Layer4, + imagetest.Layer5, }, }, { - name: "pod phase succeeded - prune leave registry alone", - pruneRegistry: newBool(false), - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod phase succeeded - prune leave registry alone", + keepTagRevisions: keepTagRevisions(0), + pruneRegistry: newBool(false), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.Pod("foo", "pod1", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, - expectedBlobDeletions: []string{}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedBlobDeletions: []string{}, }, { - name: "pod phase succeeded, pod less than min pruning age - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod phase succeeded, pod less than min pruning age - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.AgedPod("foo", "pod1", corev1.PodSucceeded, 5, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "pod phase succeeded, image less than min pruning age - don't prune", - images: imagetest.ImageList(imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", 5)), + name: "pod phase succeeded, image less than min pruning age - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", 5)), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList(imagetest.Pod("foo", "pod1", corev1.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, }, { - name: "pod phase failed - prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod phase failed - prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodFailed, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod2", corev1.PodFailed, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod3", corev1.PodFailed, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + imagetest.Layer1, + imagetest.Layer2, + imagetest.Layer3, + imagetest.Layer4, + imagetest.Layer5, }, }, { - name: "pod phase unknown - prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod phase unknown - prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodUnknown, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod2", corev1.PodUnknown, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Pod("foo", "pod3", corev1.PodUnknown, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + imagetest.Layer1, + imagetest.Layer2, + imagetest.Layer3, + imagetest.Layer4, + imagetest.Layer5, }, }, { - name: "pod container image not parsable", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod container image not parsable", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodRunning, "a/b/c/d/e"), ), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + imagetest.Layer1, + imagetest.Layer2, + imagetest.Layer3, + imagetest.Layer4, + imagetest.Layer5, }, }, { - name: "pod container image doesn't have an id", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod container image doesn't have an id", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( - imagetest.Pod("foo", "pod1", corev1.PodRunning, "foo/bar:latest"), + imagetest.Pod("foo", "pod1", corev1.PodRunning, registryHost+"/foo/bar:latest"), ), - expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, - expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, - }, }, { - name: "pod refers to image not in graph", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "pod refers to image not in graph", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), pods: imagetest.PodList( imagetest.Pod("foo", "pod1", corev1.PodRunning, registryHost+"/foo/bar@sha256:ABC0000000000000000000000000000000000000000000000000000000000002"), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{ + "foo/bar:latest", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + expectedLayerLinkDeletions: []string{ + "foo/bar|" + imagetest.Layer1, + "foo/bar|" + imagetest.Layer2, + "foo/bar|" + imagetest.Layer3, + "foo/bar|" + imagetest.Layer4, + "foo/bar|" + imagetest.Layer5, + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|" + imagetest.Layer1, - registryURL + "|" + imagetest.Layer2, - registryURL + "|" + imagetest.Layer3, - registryURL + "|" + imagetest.Layer4, - registryURL + "|" + imagetest.Layer5, + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + imagetest.Layer1, + imagetest.Layer2, + imagetest.Layer3, + imagetest.Layer4, + imagetest.Layer5, }, }, { - name: "referenced by rc - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by rc - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), rcs: imagetest.RCList(imagetest.RC("foo", "rc1", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by dc - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by dc - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), dcs: imagetest.DCList(imagetest.DC("foo", "rc1", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by daemonset - don't prune", - images: imagetest.ImageList( + name: "referenced by daemonset - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), ), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), + ), + }), + ), dss: imagetest.DSList(imagetest.DS("foo", "rc1", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001"}, + expectedStreamUpdates: []string{ + "foo/bar|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, }, { - name: "referenced by replicaset - don't prune", - images: imagetest.ImageList( + name: "referenced by replicaset - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), ), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), + ), + }), + ), rss: imagetest.RSList(imagetest.RS("foo", "rc1", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001"}, + expectedStreamUpdates: []string{ + "foo/bar|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, }, { - name: "referenced by upstream deployment - don't prune", - images: imagetest.ImageList( + name: "referenced by upstream deployment - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), ), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), + ), + }), + ), deployments: imagetest.DeploymentList(imagetest.Deployment("foo", "rc1", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001"}, + expectedStreamUpdates: []string{ + "foo/bar|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedManifestLinkDeletions: []string{ + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + }, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, }, { - name: "referenced by bc - sti - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - sti - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "source", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by bc - docker - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - docker - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "docker", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by bc - custom - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - custom - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "custom", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by bc - sti - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - sti - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "source", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by bc - docker - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - docker - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "docker", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by bc - custom - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by bc - custom - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "custom", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - sti - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - sti - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "source", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - docker - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - docker - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "docker", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - custom - ImageStreamImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - custom - ImageStreamImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "custom", "ImageStreamImage", "foo", "bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - sti - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - sti - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "source", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - docker - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - docker - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "docker", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { - name: "referenced by build - custom - DockerImage - don't prune", - images: imagetest.ImageList(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + name: "referenced by build - custom - DockerImage - don't prune", + keepTagRevisions: keepTagRevisions(0), + images: Images(imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), + streams: Streams( + imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ + imagetest.Tag("latest", + imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ), + }), + ), builds: imagetest.BuildList(imagetest.Build("foo", "build1", "custom", "DockerImage", "foo", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")), expectedImageDeletions: []string{}, }, { name: "image stream - keep most recent n images", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -393,20 +748,20 @@ func TestImagePruning(t *testing.T) { }), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedBlobDeletions: []string{registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, }, { name: "continue on blob deletion failure", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", nil, "layer1", "layer2"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -423,26 +778,28 @@ func TestImagePruning(t *testing.T) { return nil }, expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedLayerLinkDeletions: []string{registryURL + "|foo/bar|layer1", registryURL + "|foo/bar|layer2"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedLayerLinkDeletions: []string{"foo/bar|layer1", "foo/bar|layer2"}, expectedBlobDeletions: []string{ - registryURL + "|" + "layer1", - registryURL + "|" + "layer2", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "layer1", + "layer2", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + }, + expectedBlobDeletionFailures: []string{ + "image sha256:0000000000000000000000000000000000000000000000000000000000000004: failed to delete blob layer1: err", }, - expectedFailures: []string{registryURL + "|" + "layer1|err"}, }, { name: "keep image when all blob deletions fail", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", nil, "layer1", "layer2"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -454,22 +811,26 @@ func TestImagePruning(t *testing.T) { ), blobDeleterErrorGetter: func(dgst string) error { return errors.New("err") }, expectedImageDeletions: []string{}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedLayerLinkDeletions: []string{registryURL + "|foo/bar|layer1", registryURL + "|foo/bar|layer2"}, - expectedBlobDeletions: []string{registryURL + "|layer1", registryURL + "|layer2", registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedFailures: []string{registryURL + "|" + "layer1|err", registryURL + "|" + "layer2|err", registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004|err"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedLayerLinkDeletions: []string{"foo/bar|layer1", "foo/bar|layer2"}, + expectedBlobDeletions: []string{"layer1", "layer2", "sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedBlobDeletionFailures: []string{ + "image sha256:0000000000000000000000000000000000000000000000000000000000000004: failed to delete blob layer1: err", + "image sha256:0000000000000000000000000000000000000000000000000000000000000004: failed to delete blob layer2: err", + "image sha256:0000000000000000000000000000000000000000000000000000000000000004: failed to delete manifest blob sha256:0000000000000000000000000000000000000000000000000000000000000004: err", + }, }, { name: "continue on manifest link deletion failure", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -481,21 +842,23 @@ func TestImagePruning(t *testing.T) { ), manifestDeleterErr: fmt.Errorf("err"), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedBlobDeletions: []string{registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedFailures: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004|err"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletionFailures: []string{ + "imagestream foo/bar: failed to delete manifest link sha256:0000000000000000000000000000000000000000000000000000000000000004: err", + }, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, }, { name: "stop on image stream update failure", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -506,16 +869,18 @@ func TestImagePruning(t *testing.T) { }), ), imageStreamDeleterErr: fmt.Errorf("err"), - expectedFailures: []string{"foo/bar|err"}, + expectedStreamUpdateFailures: []string{ + "imagestream foo/bar: err", + }, }, { name: "image stream - same manifest listed multiple times in tag history", - images: imagetest.ImageList( + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -525,17 +890,20 @@ func TestImagePruning(t *testing.T) { ), }), ), + expectedStreamUpdates: []string{ + "foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000002", + }, }, { name: "image stream age less than min pruning age - don't prune", - images: imagetest.ImageList( + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.AgedStream(registryHost, "foo", "bar", 5, []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -551,10 +919,10 @@ func TestImagePruning(t *testing.T) { { name: "image stream - unreference absent image", - images: imagetest.ImageList( + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -562,15 +930,15 @@ func TestImagePruning(t *testing.T) { ), }), ), - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedStreamUpdates: []string{"foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, }, { name: "image stream with dangling references - delete tags", - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", nil, "layer1"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -584,19 +952,19 @@ func TestImagePruning(t *testing.T) { expectedStreamUpdates: []string{ "foo/bar:latest", "foo/bar:tag", - "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", - "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002", + "foo/bar|latest|0|sha256:0000000000000000000000000000000000000000000000000000000000000000", + "foo/bar|tag|0|sha256:0000000000000000000000000000000000000000000000000000000000000002", }, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001", registryURL + "|layer1"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001", "layer1"}, }, { name: "image stream - keep reference to a young absent image", - images: imagetest.ImageList( + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.YoungTagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", metav1.Now()), @@ -605,13 +973,14 @@ func TestImagePruning(t *testing.T) { }), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000002"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000002"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000002"}, }, { name: "images referenced by istag - keep", keepTagRevisions: keepTagRevisions(0), - images: imagetest.ImageList( + images: Images( + imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), @@ -619,7 +988,7 @@ func TestImagePruning(t *testing.T) { imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000005", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000005"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000006", registryHost+"/foo/baz@sha256:0000000000000000000000000000000000000000000000000000000000000006"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -655,33 +1024,35 @@ func TestImagePruning(t *testing.T) { }, expectedStreamUpdates: []string{ "foo/bar:dummy", - "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000000", + "foo/bar|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000001", + "foo/bar|latest|2|sha256:0000000000000000000000000000000000000000000000000000000000000002", + "foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000003", + "foo/bar|latest|4|sha256:0000000000000000000000000000000000000000000000000000000000000004", + "foo/bar|latest|5|sha256:0000000000000000000000000000000000000000000000000000000000000005", + "foo/bar|dummy|0|sha256:0000000000000000000000000000000000000000000000000000000000000005", + }, + expectedManifestLinkDeletions: []string{ "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002", "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003", "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004", "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000005", }, - expectedManifestLinkDeletions: []string{ - registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", - registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003", - registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000005", - }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000003", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000005", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + "sha256:0000000000000000000000000000000000000000000000000000000000000003", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000005", }, }, { name: "multiple resources pointing to image - don't prune", - images: imagetest.ImageList( + images: Images( imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), imagetest.Image("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -700,29 +1071,29 @@ func TestImagePruning(t *testing.T) { { name: "image with nil annotations", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, expectedStreamUpdates: []string{}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, }, { name: "prune all-images=true image with nil annotations", allImages: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, expectedStreamUpdates: []string{}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, }, { name: "prune all-images=false image with nil annotations", allImages: newBool(false), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), ), expectedImageDeletions: []string{}, @@ -731,17 +1102,17 @@ func TestImagePruning(t *testing.T) { { name: "image missing managed annotation", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, "foo", "bar"), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, expectedStreamUpdates: []string{}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, }, { name: "image with managed annotation != true", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "false"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "0"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "1"), @@ -759,30 +1130,30 @@ func TestImagePruning(t *testing.T) { }, expectedStreamUpdates: []string{}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000002", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000003", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|" + "sha256:0000000000000000000000000000000000000000000000000000000000000005", + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + "sha256:0000000000000000000000000000000000000000000000000000000000000002", + "sha256:0000000000000000000000000000000000000000000000000000000000000003", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000005", }, }, { name: "prune all-images=true with image missing managed annotation", allImages: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, "foo", "bar"), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, expectedStreamUpdates: []string{}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"}, }, { name: "prune all-images=true with image with managed annotation != true", allImages: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "false"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "0"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "1"), @@ -800,19 +1171,19 @@ func TestImagePruning(t *testing.T) { }, expectedStreamUpdates: []string{}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000000", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000002", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000003", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000005", + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + "sha256:0000000000000000000000000000000000000000000000000000000000000002", + "sha256:0000000000000000000000000000000000000000000000000000000000000003", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000005", }, }, { name: "prune all-images=false with image missing managed annotation", allImages: newBool(false), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, "foo", "bar"), ), expectedImageDeletions: []string{}, @@ -822,7 +1193,7 @@ func TestImagePruning(t *testing.T) { { name: "prune all-images=false with image with managed annotation != true", allImages: newBool(false), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "false"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "0"), imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", "someregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", true, imagev1.ManagedByOpenShiftAnnotation, "1"), @@ -836,13 +1207,13 @@ func TestImagePruning(t *testing.T) { { name: "image with layers", - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", nil, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", nil, "layer5", "layer6", "layer7", "layer8"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -853,32 +1224,32 @@ func TestImagePruning(t *testing.T) { }), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedLayerLinkDeletions: []string{ - registryURL + "|foo/bar|layer5", - registryURL + "|foo/bar|layer6", - registryURL + "|foo/bar|layer7", - registryURL + "|foo/bar|layer8", + "foo/bar|layer5", + "foo/bar|layer6", + "foo/bar|layer7", + "foo/bar|layer8", }, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|layer5", - registryURL + "|layer6", - registryURL + "|layer7", - registryURL + "|layer8", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "layer5", + "layer6", + "layer7", + "layer8", }, }, { name: "continue on layer link error", - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", nil, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", nil, "layer5", "layer6", "layer7", "layer8"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -890,39 +1261,39 @@ func TestImagePruning(t *testing.T) { ), layerDeleterErr: fmt.Errorf("err"), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|layer5", - registryURL + "|layer6", - registryURL + "|layer7", - registryURL + "|layer8", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "layer5", + "layer6", + "layer7", + "layer8", }, expectedLayerLinkDeletions: []string{ - registryURL + "|foo/bar|layer5", - registryURL + "|foo/bar|layer6", - registryURL + "|foo/bar|layer7", - registryURL + "|foo/bar|layer8", + "foo/bar|layer5", + "foo/bar|layer6", + "foo/bar|layer7", + "foo/bar|layer8", }, - expectedFailures: []string{ - registryURL + "|foo/bar|layer5|err", - registryURL + "|foo/bar|layer6|err", - registryURL + "|foo/bar|layer7|err", - registryURL + "|foo/bar|layer8|err", + expectedLayerLinkDeletionFailures: []string{ + "imagestream foo/bar: failed to delete layer link layer5: err", + "imagestream foo/bar: failed to delete layer link layer6: err", + "imagestream foo/bar: failed to delete layer link layer7: err", + "imagestream foo/bar: failed to delete layer link layer8: err", }, }, { name: "images with duplicate layers and configs", - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", &imagetest.Config2, "layer5", "layer6", "layer7", "layer8"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000005", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000005", &imagetest.Config2, "layer5", "layer6", "layer9", "layerX"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -933,38 +1304,38 @@ func TestImagePruning(t *testing.T) { }), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004", "sha256:0000000000000000000000000000000000000000000000000000000000000005"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedLayerLinkDeletions: []string{ - registryURL + "|foo/bar|" + imagetest.Config2, - registryURL + "|foo/bar|layer5", - registryURL + "|foo/bar|layer6", - registryURL + "|foo/bar|layer7", - registryURL + "|foo/bar|layer8", + "foo/bar|" + imagetest.Config2, + "foo/bar|layer5", + "foo/bar|layer6", + "foo/bar|layer7", + "foo/bar|layer8", }, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000005", - registryURL + "|" + imagetest.Config2, - registryURL + "|layer5", - registryURL + "|layer6", - registryURL + "|layer7", - registryURL + "|layer8", - registryURL + "|layer9", - registryURL + "|layerX", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000005", + imagetest.Config2, + "layer5", + "layer6", + "layer7", + "layer8", + "layer9", + "layerX", }, }, { name: "continue on image deletion failure", - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004", &imagetest.Config2, "layer5", "layer6", "layer7", "layer8"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000005", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000005", &imagetest.Config2, "layer5", "layer6", "layer9", "layerX"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -976,45 +1347,51 @@ func TestImagePruning(t *testing.T) { ), imageDeleterErr: fmt.Errorf("err"), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004", "sha256:0000000000000000000000000000000000000000000000000000000000000005"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{"foo/bar|latest|3|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedLayerLinkDeletions: []string{ - registryURL + "|foo/bar|" + imagetest.Config2, - registryURL + "|foo/bar|layer5", - registryURL + "|foo/bar|layer6", - registryURL + "|foo/bar|layer7", - registryURL + "|foo/bar|layer8", + "foo/bar|" + imagetest.Config2, + "foo/bar|layer5", + "foo/bar|layer6", + "foo/bar|layer7", + "foo/bar|layer8", }, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000005", - registryURL + "|layer7", - registryURL + "|layer8", - registryURL + "|layer9", - registryURL + "|layerX", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000005", + imagetest.Config2, + "layer5", + "layer6", + "layer7", + "layer8", + "layer9", + "layerX", + }, + expectedImageDeletionFailures: []string{ + "image sha256:0000000000000000000000000000000000000000000000000000000000000004: failed to delete image sha256:0000000000000000000000000000000000000000000000000000000000000004: err", + "image sha256:0000000000000000000000000000000000000000000000000000000000000005: failed to delete image sha256:0000000000000000000000000000000000000000000000000000000000000005: err", }, - expectedFailures: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000004|err", "sha256:0000000000000000000000000000000000000000000000000000000000000005|err"}, }, { name: "layers shared with young images are not pruned", - images: imagetest.ImageList( + images: Images( imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", 43200), imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 5), ), expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000001"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000001"}, }, { name: "image exceeding limits", pruneOverSizeLimit: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 100, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 200, nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -1027,21 +1404,21 @@ func TestImagePruning(t *testing.T) { "foo": imagetest.LimitList(100, 200), }, expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedStreamUpdates: []string{"foo/bar|latest|2|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000003"}, }, { name: "multiple images in different namespaces exceeding different limits", pruneOverSizeLimit: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", 100, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 200, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/bar/foo@sha256:0000000000000000000000000000000000000000000000000000000000000003", 500, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000004", registryHost+"/bar/foo@sha256:0000000000000000000000000000000000000000000000000000000000000004", 600, nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -1060,26 +1437,29 @@ func TestImagePruning(t *testing.T) { "bar": imagetest.LimitList(550), }, expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000002", "sha256:0000000000000000000000000000000000000000000000000000000000000004"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002", "bar/foo|sha256:0000000000000000000000000000000000000000000000000000000000000004"}, + expectedStreamUpdates: []string{ + "foo/bar|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000002", + "bar/foo|latest|1|sha256:0000000000000000000000000000000000000000000000000000000000000004", + }, expectedManifestLinkDeletions: []string{ - registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002", - registryURL + "|bar/foo|sha256:0000000000000000000000000000000000000000000000000000000000000004", + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000002", + "bar/foo|sha256:0000000000000000000000000000000000000000000000000000000000000004", }, expectedBlobDeletions: []string{ - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000002", - registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000004", + "sha256:0000000000000000000000000000000000000000000000000000000000000002", + "sha256:0000000000000000000000000000000000000000000000000000000000000004", }, }, { name: "image within allowed limits", pruneOverSizeLimit: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 100, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 200, nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -1099,12 +1479,12 @@ func TestImagePruning(t *testing.T) { name: "image exceeding limits with namespace specified", pruneOverSizeLimit: newBool(true), namespace: "foo", - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 100, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 200, nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -1116,19 +1496,20 @@ func TestImagePruning(t *testing.T) { limits: map[string][]*corev1.LimitRange{ "foo": imagetest.LimitList(100, 200), }, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedStreamUpdates: []string{"foo/bar|latest|2|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, }, { name: "build with ignored bad image reference", pruneOverSizeLimit: newBool(true), ignoreInvalidRefs: newBool(true), - images: imagetest.ImageList( + images: Images( imagetest.UnmanagedImage("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000", false, "", ""), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 100, nil), imagetest.SizedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 200, nil), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream(registryHost, "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "otherregistry/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), @@ -1144,21 +1525,21 @@ func TestImagePruning(t *testing.T) { "foo": imagetest.LimitList(100, 200), }, expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedManifestLinkDeletions: []string{registryURL + "|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedBlobDeletions: []string{registryURL + "|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, - expectedStreamUpdates: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedManifestLinkDeletions: []string{"foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedBlobDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000003"}, + expectedStreamUpdates: []string{"foo/bar|latest|2|sha256:0000000000000000000000000000000000000000000000000000000000000003"}, }, { name: "build with bad image reference", builds: imagetest.BuildList(imagetest.Build("foo", "build1", "source", "DockerImage", "foo", registryHost+"/foo/bar@invalid-digest")), - expectedErrorString: fmt.Sprintf(`Build[foo/build1]: invalid container image reference "%s/foo/bar@invalid-digest": invalid reference format`, registryHost), + expectedErrorString: fmt.Sprintf(`build/build1 namespace=foo: invalid image reference "%s/foo/bar@invalid-digest": invalid reference format`, registryHost), }, { name: "buildconfig with bad imagestreamtag", bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "source", "ImageStreamTag", "ns", "bad/tag@name")), - expectedErrorString: `BuildConfig[foo/bc1]: invalid ImageStreamTag reference "bad/tag@name":` + + expectedErrorString: `buildconfig/bc1 namespace=foo: invalid ImageStreamTag reference "bad/tag@name":` + ` "bad/tag@name" is an image stream image, not an image stream tag`, }, @@ -1167,10 +1548,11 @@ func TestImagePruning(t *testing.T) { bcs: imagetest.BCList(imagetest.BC("foo", "bc1", "source", "ImageStreamImage", "ns", "bad:isi")), deployments: imagetest.DeploymentList(imagetest.Deployment("nm", "dep1", "garbage")), rss: imagetest.RSList(imagetest.RS("nm", "rs1", "I am certainly a valid reference")), - expectedErrorString: `[BuildConfig[foo/bc1]: invalid ImageStreamImage reference "bad:isi":` + - ` expected exactly one @ in the isimage name "bad:isi",` + - ` ReplicaSet[nm/rs1]: invalid container image reference "I am certainly a valid reference":` + - ` invalid reference format]`, + expectedErrorString: `[` + + `replicaset/rs1 namespace=nm: container app: invalid image reference "I am certainly a valid reference":` + + ` invalid reference format, ` + + `buildconfig/bc1 namespace=foo: invalid ImageStreamImage reference "bad:isi":` + + ` expected exactly one @ in the isimage name "bad:isi"]`, }, } @@ -1180,23 +1562,19 @@ func TestImagePruning(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { options := PrunerOptions{ - Namespace: test.namespace, - AllImages: test.allImages, - Images: &test.images, - ImageWatcher: watch.NewFake(), - Streams: &test.streams, - StreamWatcher: watch.NewFake(), - Pods: &test.pods, - RCs: &test.rcs, - BCs: &test.bcs, - Builds: &test.builds, - DSs: &test.dss, - Deployments: &test.deployments, - DCs: &test.dcs, - RSs: &test.rss, - LimitRanges: test.limits, - RegistryClientFactory: FakeRegistryClientFactory, - RegistryURL: &url.URL{Scheme: "https", Host: registryHost}, + Namespace: test.namespace, + AllImages: test.allImages, + Images: test.images, + Streams: test.streams, + Pods: &test.pods, + RCs: &test.rcs, + BCs: &test.bcs, + Builds: &test.builds, + DSs: &test.dss, + Deployments: &test.deployments, + DCs: &test.dcs, + RSs: &test.rss, + LimitRanges: test.limits, } if test.pruneOverSizeLimit != nil { options.PruneOverSizeLimit = test.pruneOverSizeLimit @@ -1230,23 +1608,25 @@ func TestImagePruning(t *testing.T) { return } - imageDeleter, imageDeleterFactory := newFakeImageDeleter(test.imageDeleterErr) streamDeleter := &fakeImageStreamDeleter{err: test.imageStreamDeleterErr, invocations: sets.NewString()} layerLinkDeleter := &fakeLayerLinkDeleter{err: test.layerDeleterErr, invocations: sets.NewString()} - blobDeleter := &fakeBlobDeleter{getError: test.blobDeleterErrorGetter, invocations: sets.NewString()} manifestDeleter := &fakeManifestDeleter{err: test.manifestDeleterErr, invocations: sets.NewString()} + blobDeleter := &fakeBlobDeleter{getError: test.blobDeleterErrorGetter, invocations: sets.NewString()} + imageDeleter := newFakeImageDeleter(test.imageDeleterErr) - deletions, failures := p.Prune(imageDeleterFactory, streamDeleter, layerLinkDeleter, blobDeleter, manifestDeleter) + stats, errs := p.Prune(streamDeleter, layerLinkDeleter, manifestDeleter, blobDeleter, imageDeleter) - expectedFailures := sets.NewString(test.expectedFailures...) + expectedFailures := sets.NewString() + expectedFailures.Insert(test.expectedImageDeletionFailures...) + expectedFailures.Insert(test.expectedStreamUpdateFailures...) + expectedFailures.Insert(test.expectedLayerLinkDeletionFailures...) + expectedFailures.Insert(test.expectedManifestLinkDeletionFailures...) + expectedFailures.Insert(test.expectedBlobDeletionFailures...) renderedFailures := sets.NewString() - for _, f := range failures { - rendered := renderFailure(registryURL, &f) - if renderedFailures.Has(rendered) { - t.Errorf("got the following failure more than once: %v", rendered) - continue + if errs != nil { + for _, f := range errs.Errors() { + renderedFailures.Insert(f.Error()) } - renderedFailures.Insert(rendered) } for f := range renderedFailures { if expectedFailures.Has(f) { @@ -1261,94 +1641,59 @@ func TestImagePruning(t *testing.T) { expectedImageDeletions := sets.NewString(test.expectedImageDeletions...) if a, e := imageDeleter.invocations, expectedImageDeletions; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected image deletions: %s", diff.ObjectDiff(a, e)) + t.Errorf("unexpected image deletions (-actual, +expected): %s", diff.ObjectDiff(a, e)) + } + if want := expectedImageDeletions.Len() - len(test.expectedImageDeletionFailures); stats.DeletedImages != want { + t.Errorf("image deletions: got %d, want %d", stats.DeletedImages, want) } expectedStreamUpdates := sets.NewString(test.expectedStreamUpdates...) if a, e := streamDeleter.invocations, expectedStreamUpdates; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected stream updates: %s", diff.ObjectDiff(a, e)) + t.Errorf("unexpected stream updates (-actual, +expected): %s", diff.ObjectDiff(a, e)) + } + expectedImageStreamUpdates := sets.NewString() + expectedImageStreamItemsDeletions := 0 + for update := range expectedStreamUpdates { + if i := strings.Index(update, "|"); i != -1 { + expectedImageStreamUpdates.Insert(update[:i]) + expectedImageStreamItemsDeletions++ + } else if i := strings.Index(update, ":"); i != -1 { + expectedImageStreamUpdates.Insert(update[:i]) + } else { + t.Errorf("invalid update: %s", update) + } + } + if got, want := stats.UpdatedImageStreams+stats.DeletedImageStreamTagItems, expectedImageStreamUpdates.Len()+expectedImageStreamItemsDeletions; got != want { + t.Errorf("stream updates: got %d, want %d", got, want) } expectedLayerLinkDeletions := sets.NewString(test.expectedLayerLinkDeletions...) if a, e := layerLinkDeleter.invocations, expectedLayerLinkDeletions; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected layer link deletions: %s", diff.ObjectDiff(a, e)) + t.Errorf("unexpected layer link deletions (-actual, +expected): %s", diff.ObjectDiff(a, e)) + } + if want := expectedLayerLinkDeletions.Len() - len(test.expectedLayerLinkDeletionFailures); stats.DeletedLayerLinks != want { + t.Errorf("layer link deletions: got %d, want %d", stats.DeletedLayerLinks, want) } expectedManifestLinkDeletions := sets.NewString(test.expectedManifestLinkDeletions...) if a, e := manifestDeleter.invocations, expectedManifestLinkDeletions; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected manifest link deletions: %s", diff.ObjectDiff(a, e)) + t.Errorf("unexpected manifest link deletions (-actual, +expected): %s", diff.ObjectDiff(a, e)) + } + if want := expectedManifestLinkDeletions.Len() - len(test.expectedManifestLinkDeletionFailures); stats.DeletedManifestLinks != want { + t.Errorf("manifest link deletions: got %d, want %d", stats.DeletedManifestLinks, want) } expectedBlobDeletions := sets.NewString(test.expectedBlobDeletions...) if a, e := blobDeleter.invocations, expectedBlobDeletions; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected blob deletions: %s", diff.ObjectDiff(a, e)) + t.Errorf("unexpected blob deletions (-actual, +expected): %s", diff.ObjectDiff(a, e)) } - - // TODO: shall we return deletion for each layer link unlinked from the image stream?? - imageStreamUpdates := sets.NewString() - expectedAllDeletions := sets.NewString() - for _, s := range []sets.String{expectedImageDeletions, expectedLayerLinkDeletions, expectedBlobDeletions} { - expectedAllDeletions.Insert(s.List()...) - } - for _, d := range deletions { - rendered, isImageStreamUpdate, isManifestLinkDeletion := renderDeletion(registryURL, &d) - if isManifestLinkDeletion { - continue - } - if isImageStreamUpdate { - imageStreamUpdates.Insert(rendered) - continue - } - if expectedAllDeletions.Has(rendered) { - expectedAllDeletions.Delete(rendered) - } else { - t.Errorf("got unexpected deletion: %#+v (rendered: %q)", d, rendered) - } - } - for _, f := range failures { - rendered, _, _ := renderDeletion(registryURL, &Deletion{Node: f.Node, Parent: f.Parent}) - expectedAllDeletions.Delete(rendered) - } - for del, ok := expectedAllDeletions.PopAny(); ok; del, ok = expectedAllDeletions.PopAny() { - t.Errorf("expected deletion %q did not happen", del) - } - - expectedStreamUpdateNames := sets.NewString() - for u := range expectedStreamUpdates { - expectedStreamUpdateNames.Insert(regexp.MustCompile(`[@|:]`).Split(u, 2)[0]) - } - if a, e := imageStreamUpdates, expectedStreamUpdateNames; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected image stream updates in deletions: %s", diff.ObjectDiff(a, e)) + if want := expectedBlobDeletions.Len() - len(test.expectedBlobDeletionFailures); stats.DeletedBlobs != want { + t.Errorf("blob deletions: got %d, want %d", stats.DeletedBlobs, want) } }) } } -func renderDeletion(registryURL string, deletion *Deletion) (rendered string, isImageStreamUpdate, isManifestLinkDeletion bool) { - switch t := deletion.Node.(type) { - case *imagegraph.ImageNode: - return t.Image.Name, false, false - case *imagegraph.ImageComponentNode: - // deleting blob - if deletion.Parent == nil { - return fmt.Sprintf("%s|%s", registryURL, t.Component), false, false - } - streamName := "unknown" - if sn, ok := deletion.Parent.(*imagegraph.ImageStreamNode); ok { - streamName = getName(sn.ImageStream) - } - return fmt.Sprintf("%s|%s|%s", registryURL, streamName, t.Component), false, t.Type == imagegraph.ImageComponentTypeManifest - case *imagegraph.ImageStreamNode: - return getName(t.ImageStream), true, false - } - return "unknown", false, false -} - -func renderFailure(registryURL string, failure *Failure) string { - rendered, _, _ := renderDeletion(registryURL, &Deletion{Node: failure.Node, Parent: failure.Parent}) - return rendered + "|" + failure.Err.Error() -} - func TestImageDeleter(t *testing.T) { var level klog.Level level.Set(fmt.Sprint(*logLevel)) @@ -1396,8 +1741,8 @@ func TestLayerDeleter(t *testing.T) { actions = append(actions, req.Method+":"+req.URL.String()) return &http.Response{StatusCode: http.StatusServiceUnavailable, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil }) - layerLinkDeleter := NewLayerLinkDeleter() - layerLinkDeleter.DeleteLayerLink(client, &url.URL{Scheme: "http", Host: "registry1"}, "repo", "layer1") + layerLinkDeleter := NewLayerLinkDeleter(client, &url.URL{Scheme: "http", Host: "registry1"}) + layerLinkDeleter.DeleteLayerLink("repo", "layer1") if e := []string{"DELETE:http://registry1/v2/repo/blobs/layer1"}; !reflect.DeepEqual(actions, e) { t.Errorf("unexpected actions: %s", diff.ObjectDiff(actions, e)) @@ -1413,8 +1758,8 @@ func TestNotFoundLayerDeleter(t *testing.T) { actions = append(actions, req.Method+":"+req.URL.String()) return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil }) - layerLinkDeleter := NewLayerLinkDeleter() - layerLinkDeleter.DeleteLayerLink(client, &url.URL{Scheme: "https", Host: "registry1"}, "repo", "layer1") + layerLinkDeleter := NewLayerLinkDeleter(client, &url.URL{Scheme: "https", Host: "registry1"}) + layerLinkDeleter.DeleteLayerLink("repo", "layer1") if e := []string{"DELETE:https://registry1/v2/repo/blobs/layer1"}; !reflect.DeepEqual(actions, e) { t.Errorf("unexpected actions: %s", diff.ObjectDiff(actions, e)) @@ -1427,8 +1772,8 @@ func TestRegistryPruning(t *testing.T) { tests := []struct { name string - images imagev1.ImageList - streams imagev1.ImageStreamList + images map[string]*imagev1.Image + streams map[string]*imagev1.ImageStream expectedLayerLinkDeletions sets.String expectedBlobDeletions sets.String expectedManifestDeletions sets.String @@ -1438,11 +1783,11 @@ func TestRegistryPruning(t *testing.T) { { name: "layers unique to id1 pruned", pruneRegistry: true, - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer3", "layer4", "layer5", "layer6"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream("registry1.io", "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), @@ -1456,28 +1801,28 @@ func TestRegistryPruning(t *testing.T) { }), ), expectedLayerLinkDeletions: sets.NewString( - "https://registry1.io|foo/bar|"+imagetest.Config1, - "https://registry1.io|foo/bar|layer1", - "https://registry1.io|foo/bar|layer2", + "foo/bar|"+imagetest.Config1, + "foo/bar|layer1", + "foo/bar|layer2", ), expectedBlobDeletions: sets.NewString( - "https://registry1.io|sha256:0000000000000000000000000000000000000000000000000000000000000001", - "https://registry1.io|"+imagetest.Config1, - "https://registry1.io|layer1", - "https://registry1.io|layer2", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + imagetest.Config1, + "layer1", + "layer2", ), expectedManifestDeletions: sets.NewString( - "https://registry1.io|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", ), }, { name: "no pruning when no images are pruned", pruneRegistry: true, - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream("registry1.io", "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"), @@ -1492,22 +1837,22 @@ func TestRegistryPruning(t *testing.T) { { name: "blobs pruned when streams have already been deleted", pruneRegistry: true, - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", "layer4"), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer3", "layer4", "layer5", "layer6"), ), expectedLayerLinkDeletions: sets.NewString(), expectedBlobDeletions: sets.NewString( - "https://registry1.io|sha256:0000000000000000000000000000000000000000000000000000000000000001", - "https://registry1.io|sha256:0000000000000000000000000000000000000000000000000000000000000002", - "https://registry1.io|"+imagetest.Config1, - "https://registry1.io|"+imagetest.Config2, - "https://registry1.io|layer1", - "https://registry1.io|layer2", - "https://registry1.io|layer3", - "https://registry1.io|layer4", - "https://registry1.io|layer5", - "https://registry1.io|layer6", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + "sha256:0000000000000000000000000000000000000000000000000000000000000002", + imagetest.Config1, + imagetest.Config2, + "layer1", + "layer2", + "layer3", + "layer4", + "layer5", + "layer6", ), expectedManifestDeletions: sets.NewString(), }, @@ -1515,12 +1860,12 @@ func TestRegistryPruning(t *testing.T) { { name: "config used as a layer", pruneRegistry: true, - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", imagetest.Config1), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer3", "layer4", "layer5", imagetest.Config1), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/other@sha256:0000000000000000000000000000000000000000000000000000000000000003", nil, "layer3", "layer4", "layer6", imagetest.Config1), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream("registry1.io", "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), @@ -1530,33 +1875,32 @@ func TestRegistryPruning(t *testing.T) { imagetest.Stream("registry1.io", "foo", "other", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/other@sha256:0000000000000000000000000000000000000000000000000000000000000003"), - imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), ), }), ), expectedLayerLinkDeletions: sets.NewString( - "https://registry1.io|foo/bar|layer1", - "https://registry1.io|foo/bar|layer2", + "foo/bar|layer1", + "foo/bar|layer2", ), expectedBlobDeletions: sets.NewString( - "https://registry1.io|sha256:0000000000000000000000000000000000000000000000000000000000000001", - "https://registry1.io|layer1", - "https://registry1.io|layer2", + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + "layer1", + "layer2", ), expectedManifestDeletions: sets.NewString( - "https://registry1.io|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", + "foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001", ), }, { name: "config used as a layer, but leave registry alone", pruneRegistry: false, - images: imagetest.ImageList( + images: Images( imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &imagetest.Config1, "layer1", "layer2", "layer3", imagetest.Config1), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &imagetest.Config2, "layer3", "layer4", "layer5", imagetest.Config1), imagetest.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/other@sha256:0000000000000000000000000000000000000000000000000000000000000003", nil, "layer3", "layer4", "layer6", imagetest.Config1), ), - streams: imagetest.StreamList( + streams: Streams( imagetest.Stream("registry1.io", "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"), @@ -1581,36 +1925,32 @@ func TestRegistryPruning(t *testing.T) { keepYoungerThan := 60 * time.Minute keepTagRevisions := 1 options := PrunerOptions{ - KeepYoungerThan: &keepYoungerThan, - KeepTagRevisions: &keepTagRevisions, - PruneRegistry: &test.pruneRegistry, - Images: &test.images, - ImageWatcher: watch.NewFake(), - Streams: &test.streams, - StreamWatcher: watch.NewFake(), - Pods: &corev1.PodList{}, - RCs: &corev1.ReplicationControllerList{}, - BCs: &buildv1.BuildConfigList{}, - Builds: &buildv1.BuildList{}, - DSs: &kappsv1.DaemonSetList{}, - Deployments: &kappsv1.DeploymentList{}, - DCs: &appsv1.DeploymentConfigList{}, - RSs: &kappsv1.ReplicaSetList{}, - RegistryClientFactory: FakeRegistryClientFactory, - RegistryURL: &url.URL{Scheme: "https", Host: "registry1.io"}, + KeepYoungerThan: &keepYoungerThan, + KeepTagRevisions: &keepTagRevisions, + PruneRegistry: &test.pruneRegistry, + Images: test.images, + Streams: test.streams, + Pods: &corev1.PodList{}, + RCs: &corev1.ReplicationControllerList{}, + BCs: &buildv1.BuildConfigList{}, + Builds: &buildv1.BuildList{}, + DSs: &kappsv1.DaemonSetList{}, + Deployments: &kappsv1.DeploymentList{}, + DCs: &appsv1.DeploymentConfigList{}, + RSs: &kappsv1.ReplicaSetList{}, } p, err := NewPruner(options) if err != nil { t.Fatalf("unexpected error: %v", err) } - _, imageDeleterFactory := newFakeImageDeleter(nil) streamDeleter := &fakeImageStreamDeleter{invocations: sets.NewString()} layerLinkDeleter := &fakeLayerLinkDeleter{invocations: sets.NewString()} - blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()} manifestDeleter := &fakeManifestDeleter{invocations: sets.NewString()} + blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()} + imageDeleter := newFakeImageDeleter(nil) - p.Prune(imageDeleterFactory, streamDeleter, layerLinkDeleter, blobDeleter, manifestDeleter) + p.Prune(streamDeleter, layerLinkDeleter, manifestDeleter, blobDeleter, imageDeleter) if a, e := layerLinkDeleter.invocations, test.expectedLayerLinkDeletions; !reflect.DeepEqual(a, e) { t.Errorf("unexpected layer link deletions: %s", diff.ObjectDiff(a, e)) @@ -1635,12 +1975,12 @@ func TestImageWithStrongAndWeakRefsIsNotPruned(t *testing.T) { var level klog.Level level.Set(fmt.Sprint(*logLevel)) - images := imagetest.ImageList( + images := Images( imagetest.AgedImage("0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", 1540), imagetest.AgedImage("0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 1540), imagetest.AgedImage("0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 1540), ) - streams := imagetest.StreamList( + streams := Streams( imagetest.Stream("registry1", "foo", "bar", []imagev1.NamedTagEventList{ imagetest.Tag("latest", imagetest.TagEvent("0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003"), @@ -1662,18 +2002,16 @@ func TestImageWithStrongAndWeakRefsIsNotPruned(t *testing.T) { rss := imagetest.RSList() options := PrunerOptions{ - Images: &images, - ImageWatcher: watch.NewFake(), - Streams: &streams, - StreamWatcher: watch.NewFake(), - Pods: &pods, - RCs: &rcs, - BCs: &bcs, - Builds: &builds, - DSs: &dss, - Deployments: &deployments, - DCs: &dcs, - RSs: &rss, + Images: images, + Streams: streams, + Pods: &pods, + RCs: &rcs, + BCs: &bcs, + Builds: &builds, + DSs: &dss, + Deployments: &deployments, + DCs: &dcs, + RSs: &rss, } keepYoungerThan := 24 * time.Hour keepTagRevisions := 2 @@ -1684,25 +2022,24 @@ func TestImageWithStrongAndWeakRefsIsNotPruned(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - imageDeleter, imageDeleterFactory := newFakeImageDeleter(nil) streamDeleter := &fakeImageStreamDeleter{invocations: sets.NewString()} layerLinkDeleter := &fakeLayerLinkDeleter{invocations: sets.NewString()} - blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()} manifestDeleter := &fakeManifestDeleter{invocations: sets.NewString()} + blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()} + imageDeleter := newFakeImageDeleter(nil) - deletions, failures := p.Prune(imageDeleterFactory, streamDeleter, layerLinkDeleter, blobDeleter, manifestDeleter) - if len(failures) != 0 { - t.Errorf("got unexpected failures: %#+v", failures) + stats, errs := p.Prune(streamDeleter, layerLinkDeleter, manifestDeleter, blobDeleter, imageDeleter) + if errs != nil { + t.Errorf("got unexpected errors: %#+v", errs) } - - if len(deletions) > 0 { - t.Fatalf("got unexpected deletions: %#+v", deletions) + if stats.String() != "deleted 1 image stream tag item(s), updated 1 image stream(s)" { + t.Errorf("got unexpected deletions: %v", stats) } if imageDeleter.invocations.Len() > 0 { t.Fatalf("unexpected imageDeleter invocations: %v", imageDeleter.invocations) } - if streamDeleter.invocations.Len() > 0 { + if !streamDeleter.invocations.Equal(sets.NewString("foo/bar|latest|2|0000000000000000000000000000000000000000000000000000000000000001")) { t.Fatalf("unexpected streamDeleter invocations: %v", streamDeleter.invocations) } if layerLinkDeleter.invocations.Len() > 0 { @@ -1716,367 +2053,6 @@ func TestImageWithStrongAndWeakRefsIsNotPruned(t *testing.T) { } } -func TestImageIsPrunable(t *testing.T) { - g := genericgraph.New() - imageNode := imagegraph.EnsureImageNode(g, &imagev1.Image{ObjectMeta: metav1.ObjectMeta{Name: "myImage"}}) - streamNode := imagegraph.EnsureImageStreamNode(g, &imagev1.ImageStream{ObjectMeta: metav1.ObjectMeta{Name: "myStream"}}) - g.AddEdge(streamNode, imageNode, ReferencedImageEdgeKind) - g.AddEdge(streamNode, imageNode, WeakReferencedImageEdgeKind) - - if imageIsPrunable(g, imageNode.(*imagegraph.ImageNode), pruneAlgorithm{}) { - t.Fatalf("Image is prunable although it should not") - } -} - -func TestPrunerGetNextJob(t *testing.T) { - var level klog.Level - level.Set(fmt.Sprint(*logLevel)) - - klog.V(2).Infof("debug") - algo := pruneAlgorithm{ - keepYoungerThan: time.Now(), - } - p := &pruner{algorithm: algo, processedImages: make(map[*imagegraph.ImageNode]*Job)} - images := imagetest.ImageList( - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 1, "layer1"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 2, "layer1", "layer2"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", 3, "Layer1", "Layer2", "Layer3"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000013", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000013", 4, "Layer1", "LayeR2", "LayeR3"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000012", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000012", 5, "LayeR1", "LayeR2"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000011", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000011", 6, "layer1", "Layer2", "LAYER3", "LAYER4"), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000010", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000010", 7, "layer1", "layer2", "layer3", "layer4"), - ) - p.g = genericgraph.New() - err := p.addImagesToGraph(&images) - if err != nil { - t.Fatalf("failed to add images: %v", err) - } - - is := images.Items - imageStreams := imagetest.StreamList( - imagetest.Stream("example.com", "foo", "bar", []imagev1.NamedTagEventList{ - imagetest.Tag("latest", - imagetest.TagEvent(is[3].Name, is[3].DockerImageReference), - imagetest.TagEvent(is[4].Name, is[4].DockerImageReference), - imagetest.TagEvent(is[5].Name, is[5].DockerImageReference))}), - imagetest.Stream("example.com", "foo", "baz", []imagev1.NamedTagEventList{ - imagetest.Tag("devel", - imagetest.TagEvent(is[3].Name, is[3].DockerImageReference), - imagetest.TagEvent(is[2].Name, is[2].DockerImageReference), - imagetest.TagEvent(is[1].Name, is[1].DockerImageReference)), - imagetest.Tag("prod", - imagetest.TagEvent(is[2].Name, is[2].DockerImageReference))})) - if err := p.addImageStreamsToGraph(&imageStreams, nil); err != nil { - t.Fatalf("failed to add image streams: %v", err) - } - - imageNodes := getImageNodes(p.g.Nodes()) - if len(imageNodes) == 0 { - t.Fatalf("not images nodes") - } - prunable := calculatePrunableImages(p.g, imageNodes, algo) - sort.Sort(byLayerCountAndAge(prunable)) - p.queue = makeQueue(prunable) - - checkQueue := func(desc string, expected ...*imagev1.Image) { - for i, item := 0, p.queue; i < len(expected) || item != nil; i++ { - if i >= len(expected) { - t.Errorf("[%s] unexpected image at #%d: %s", desc, i, item.node.Image.Name) - } else if item == nil { - t.Errorf("[%s] expected image %q not found at #%d", desc, expected[i].Name, i) - } else if item.node.Image.Name != expected[i].Name { - t.Errorf("[%s] unexpected image at #%d: %s != %s", desc, i, item.node.Image.Name, expected[i].Name) - } - if item != nil { - item = item.next - } - } - if t.Failed() { - t.FailNow() - } - } - - /* layerrefs: layer1:4, Layer1:2, LayeR1:1, layer2:2, Layer2:2, LayeR2:2, - * layer3:1, Layer3:1, LayeR3:1, LAYER3:1, layer4:1, LAYER4:1 */ - checkQueue("initial state", &is[6], &is[5], &is[3], &is[2], &is[4], &is[1], &is[0]) - job := expectBlockedOrJob(t, p, "pop first", false, &is[6], []string{"layer4", "layer3"})(p.getNextJob()) - p.processedImages[job.Image] = job - imgnd6 := job.Image - - /* layerrefs: layer1:3, Layer1:2, LayeR1:1, layer2:1, Layer2:2, LayeR2:2, - * layer3:0, Layer3:1, LayeR3:1, LAYER3:1, layer4:0, LAYER4:1 */ - checkQueue("1 removed", &is[5], &is[3], &is[2], &is[4], &is[1], &is[0]) - job = expectBlockedOrJob(t, p, "pop second", false, &is[5], []string{"LAYER3", "LAYER4"})(p.getNextJob()) - p.processedImages[job.Image] = job - imgnd5 := job.Image - - /* layerrefs: layer1:2, Layer1:2, LayeR1:1, layer2:1, Layer2:1, LayeR2:2, - * Layer3:1, LayeR3:1, LAYER3:0, LAYER4:0 */ - checkQueue("2 removed", &is[3], &is[2], &is[4], &is[1], &is[0]) - job = expectBlockedOrJob(t, p, "pop third", false, &is[3], []string{"LayeR3"})(p.getNextJob()) - p.processedImages[job.Image] = job - imgnd3 := job.Image - - // layerrefs: layer1:2, Layer1:1, LayeR1:1, layer2:1, Layer2:1, LayeR2:1, Layer3:1, LayeR3:0 - checkQueue("3 removed", &is[2], &is[4], &is[1], &is[0]) - // all the remaining images are blocked now except for the is[0] - job = expectBlockedOrJob(t, p, "pop fourth", false, &is[0], nil)(p.getNextJob()) - p.processedImages[job.Image] = job - imgnd0 := job.Image - - // layerrefs: layer1:1, Layer1:1, LayeR1:1, layer2:1, Layer2:1, LayeR2:1, Layer3:1 - checkQueue("4 removed and blocked", &is[2], &is[4], &is[1]) - // all the remaining images are blocked now - expectBlockedOrJob(t, p, "blocked", true, nil, nil)(p.getNextJob()) - - // layerrefs: layer1:1, Layer1:2, LayeR1:1, layer2:1, Layer2:1, LayeR2:1, Layer3:1 - checkQueue("3 to go", &is[2], &is[4], &is[1]) - // unblock one of the images - p.g.RemoveNode(imgnd3) - job = expectBlockedOrJob(t, p, "pop fifth", false, &is[4], - []string{"LayeR1", "LayeR2"})(p.getNextJob()) - p.processedImages[job.Image] = job - imgnd4 := job.Image - - // layerrefs: layer1:1, Layer1:2, LayeR1:0, layer2:1, Layer2:1, LayeR2:0, Layer3:1 - checkQueue("2 to go", &is[2], &is[1]) - expectBlockedOrJob(t, p, "blocked with two items#1", true, nil, nil)(p.getNextJob()) - checkQueue("still 2 to go", &is[2], &is[1]) - - p.g.RemoveNode(imgnd0) - delete(p.processedImages, imgnd0) - expectBlockedOrJob(t, p, "blocked with two items#2", true, nil, nil)(p.getNextJob()) - p.g.RemoveNode(imgnd6) - delete(p.processedImages, imgnd6) - expectBlockedOrJob(t, p, "blocked with two items#3", true, nil, nil)(p.getNextJob()) - p.g.RemoveNode(imgnd4) - delete(p.processedImages, imgnd4) - expectBlockedOrJob(t, p, "blocked with two items#4", true, nil, nil)(p.getNextJob()) - p.g.RemoveNode(imgnd5) - delete(p.processedImages, imgnd5) - - job = expectBlockedOrJob(t, p, "pop sixth", false, &is[2], - []string{"Layer1", "Layer2", "Layer3"})(p.getNextJob()) - p.processedImages[job.Image] = job - - // layerrefs: layer1:1, Layer1:0, layer2:1, Layer2:0, Layer3:0 - checkQueue("1 to go", &is[1]) - job = expectBlockedOrJob(t, p, "pop last", false, &is[1], - []string{"layer1", "layer2"})(p.getNextJob()) - p.processedImages[job.Image] = job - - // layerrefs: layer1:0, layer2:0 - checkQueue("queue empty") - expectBlockedOrJob(t, p, "empty", false, nil, nil)(p.getNextJob()) -} - -func expectBlockedOrJob( - t *testing.T, - p *pruner, - desc string, - blocked bool, - image *imagev1.Image, - layers []string, -) func(job *Job, blocked bool) *Job { - return func(job *Job, b bool) *Job { - if b != blocked { - t.Fatalf("[%s] unexpected blocked: %t != %t", desc, b, blocked) - } - - if blocked { - return job - } - - if image == nil && job != nil { - t.Fatalf("[%s] got unexpected job %#+v", desc, job) - } - if image != nil && job == nil { - t.Fatalf("[%s] got nil instead of job", desc) - } - if job == nil { - return nil - } - - if a, e := job.Image.Image.Name, image.Name; a != e { - t.Errorf("[%s] unexpected image in job: %s != %s", desc, a, e) - } - - expLayers := sets.NewString(imagegraph.EnsureImageComponentManifestNode( - p.g, job.Image.Image.Name).(*imagegraph.ImageComponentNode).String()) - for _, l := range layers { - expLayers.Insert(imagegraph.EnsureImageComponentLayerNode( - p.g, l).(*imagegraph.ImageComponentNode).String()) - } - actLayers := sets.NewString() - for c, ret := range job.Components { - if ret.PrunableGlobally { - actLayers.Insert(c.String()) - } - } - if a, e := actLayers, expLayers; !reflect.DeepEqual(a, e) { - t.Errorf("[%s] unexpected image components: %s", desc, diff.ObjectDiff(a.List(), e.List())) - } - - if t.Failed() { - t.FailNow() - } - - return job - } -} - -func TestChangeImageStreamsWhilePruning(t *testing.T) { - t.Skip("failed after commenting out") - var level klog.Level - level.Set(fmt.Sprint(*logLevel)) - - images := imagetest.ImageList( - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", 5), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", 4), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 3), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000004", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 2), - imagetest.AgedImage("sha256:0000000000000000000000000000000000000000000000000000000000000005", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000003", 1), - ) - - streams := imagetest.StreamList(imagetest.Stream("registry1", "foo", "bar", []imagev1.NamedTagEventList{})) - streamWatcher := watch.NewFake() - pods := imagetest.PodList() - rcs := imagetest.RCList() - bcs := imagetest.BCList() - builds := imagetest.BuildList() - dss := imagetest.DSList() - deployments := imagetest.DeploymentList() - dcs := imagetest.DCList() - rss := imagetest.RSList() - - options := PrunerOptions{ - Images: &images, - ImageWatcher: watch.NewFake(), - Streams: &streams, - StreamWatcher: streamWatcher, - Pods: &pods, - RCs: &rcs, - BCs: &bcs, - Builds: &builds, - DSs: &dss, - Deployments: &deployments, - DCs: &dcs, - RSs: &rss, - RegistryClientFactory: FakeRegistryClientFactory, - RegistryURL: &url.URL{Scheme: "https", Host: "registry1.io"}, - NumWorkers: 1, - } - keepYoungerThan := 30 * time.Second - keepTagRevisions := 2 - options.KeepYoungerThan = &keepYoungerThan - options.KeepTagRevisions = &keepTagRevisions - p, err := NewPruner(options) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - pruneFinished := make(chan struct{}) - deletions, failures := []Deletion{}, []Failure{} - imageDeleter, imageDeleterFactory := newBlockingImageDeleter(t) - - // run the pruning loop in a go routine - go func() { - deletions, failures = p.Prune( - imageDeleterFactory, - &fakeImageStreamDeleter{invocations: sets.NewString()}, - &fakeLayerLinkDeleter{invocations: sets.NewString()}, - &fakeBlobDeleter{invocations: sets.NewString()}, - &fakeManifestDeleter{invocations: sets.NewString()}, - ) - if len(failures) != 0 { - t.Errorf("got unexpected failures: %#+v", failures) - } - close(pruneFinished) - }() - - expectedImageDeletions := sets.NewString() - expectedBlobDeletions := sets.NewString() - - img := imageDeleter.waitForRequest() - if a, e := img.Name, images.Items[0].Name; a != e { - t.Fatalf("got unexpected image deletion request: %s != %s", a, e) - } - expectedImageDeletions.Insert(images.Items[0].Name) - expectedBlobDeletions.Insert("registry1|" + images.Items[0].Name) - - // let the pruner wait for reply and meanwhile reference an image with a new image stream - stream := imagetest.Stream("registry1", "foo", "new", []imagev1.NamedTagEventList{ - imagetest.Tag("latest", - imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1/foo/new@sha256:0000000000000000000000000000000000000000000000000000000000000002"), - )}) - streamWatcher.Add(&stream) - imageDeleter.unblock() - - // the pruner shall skip the newly referenced image - img = imageDeleter.waitForRequest() - if a, e := img.Name, images.Items[2].Name; a != e { - t.Fatalf("got unexpected image deletion request: %s != %s", a, e) - } - expectedImageDeletions.Insert(images.Items[2].Name) - expectedBlobDeletions.Insert("registry1|" + images.Items[2].Name) - - // now lets modify the existing image stream to reference some more images - stream = imagetest.Stream("registry1", "foo", "bar", []imagev1.NamedTagEventList{ - imagetest.Tag("latest", - imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000000", "registry1/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000"), - imagetest.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000004", "registry1/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000004"), - )}) - streamWatcher.Modify(&stream) - imageDeleter.unblock() - - // the pruner shall skip the newly referenced image - img = imageDeleter.waitForRequest() - if a, e := img.Name, images.Items[4].Name; a != e { - t.Fatalf("got unexpected image deletion request: %s != %s", a, e) - } - expectedImageDeletions.Insert(images.Items[4].Name) - expectedBlobDeletions.Insert("registry1|" + images.Items[4].Name) - imageDeleter.unblock() - - // no more images - wait for the pruner to finish - select { - case <-pruneFinished: - case <-time.After(time.Second): - t.Errorf("tester: timeout while waiting for pruner to finish") - } - - if a, e := imageDeleter.d.invocations, expectedImageDeletions; !reflect.DeepEqual(a, e) { - t.Errorf("unexpected image deletions: %s", diff.ObjectDiff(a, e)) - } - - expectedAllDeletions := sets.NewString( - append(expectedImageDeletions.List(), expectedBlobDeletions.List()...)...) - for _, d := range deletions { - rendered, _, isManifestLinkDeletion := renderDeletion("registry1", &d) - if isManifestLinkDeletion { - // TODO: update tests to count and verify the number of manifest link deletions - continue - } - if expectedAllDeletions.Has(rendered) { - expectedAllDeletions.Delete(rendered) - } else { - t.Errorf("got unexpected deletion: %#+v (rendered: %q)", d, rendered) - } - } - for del, ok := expectedAllDeletions.PopAny(); ok; del, ok = expectedAllDeletions.PopAny() { - t.Errorf("expected deletion %q did not happen", del) - } -} - -func streamListToClient(list *imagev1.ImageStreamList) imagev1client.ImageStreamsGetter { - streams := make([]runtime.Object, 0, len(list.Items)) - for i := range list.Items { - streams = append(streams, &list.Items[i]) - } - - return &fakeimagev1client.FakeImageV1{Fake: &(fakeimageclient.NewSimpleClientset(streams...).Fake)} -} - func keepTagRevisions(n int) *int { return &n } @@ -2096,66 +2072,18 @@ func (p *fakeImageDeleter) DeleteImage(image *imagev1.Image) error { return p.err } -func newFakeImageDeleter(err error) (*fakeImageDeleter, ImagePrunerFactoryFunc) { - deleter := &fakeImageDeleter{ +func newFakeImageDeleter(err error) *fakeImageDeleter { + return &fakeImageDeleter{ err: err, invocations: sets.NewString(), } - return deleter, func() (ImageDeleter, error) { - return deleter, nil - } -} - -type blockingImageDeleter struct { - t *testing.T - d *fakeImageDeleter - requests chan *imagev1.Image - reply chan struct{} -} - -func (bid *blockingImageDeleter) DeleteImage(img *imagev1.Image) error { - bid.requests <- img - select { - case <-bid.reply: - case <-time.After(time.Second): - bid.t.Fatalf("worker: timeout while waiting for image deletion confirmation") - } - return bid.d.DeleteImage(img) -} - -func (bid *blockingImageDeleter) waitForRequest() *imagev1.Image { - select { - case img := <-bid.requests: - return img - case <-time.After(time.Second): - bid.t.Fatalf("tester: timeout while waiting on worker's request") - return nil - } -} - -func (bid *blockingImageDeleter) unblock() { - bid.reply <- struct{}{} -} - -func newBlockingImageDeleter(t *testing.T) (*blockingImageDeleter, ImagePrunerFactoryFunc) { - deleter, _ := newFakeImageDeleter(nil) - blocking := blockingImageDeleter{ - t: t, - d: deleter, - requests: make(chan *imagev1.Image), - reply: make(chan struct{}), - } - return &blocking, func() (ImageDeleter, error) { - return &blocking, nil - } } type fakeImageStreamDeleter struct { - mutex sync.Mutex - invocations sets.String - err error - streamImages map[string][]string - streamTags map[string][]string + mutex sync.Mutex + invocations sets.String + err error + streams map[string]map[string][]string } var _ ImageStreamDeleter = &fakeImageStreamDeleter{} @@ -2163,55 +2091,61 @@ var _ ImageStreamDeleter = &fakeImageStreamDeleter{} func (p *fakeImageStreamDeleter) GetImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) { p.mutex.Lock() defer p.mutex.Unlock() - if p.streamImages == nil { - p.streamImages = make(map[string][]string) - } - if p.streamTags == nil { - p.streamTags = make(map[string][]string) + + if p.streams == nil { + p.streams = make(map[string]map[string][]string) } - for _, tag := range stream.Status.Tags { - streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) - p.streamTags[streamName] = append(p.streamTags[streamName], tag.Tag) + streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) + s := make(map[string][]string) + for _, tag := range stream.Status.Tags { + var items []string for _, tagEvent := range tag.Items { - p.streamImages[streamName] = append(p.streamImages[streamName], tagEvent.Image) + items = append(items, tagEvent.Image) } + s[tag.Tag] = items } + + p.streams[streamName] = s + return stream, p.err } -func (p *fakeImageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) { - streamImages := make(map[string]struct{}) - streamTags := make(map[string]struct{}) +func (p *fakeImageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream, revisionsDeleted int) (*imagev1.ImageStream, error) { + p.mutex.Lock() + defer p.mutex.Unlock() + streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) + s := p.streams[streamName] + + n := make(map[string][]string) for _, tag := range stream.Status.Tags { - streamTags[tag.Tag] = struct{}{} + var items []string for _, tagEvent := range tag.Items { - streamImages[tagEvent.Image] = struct{}{} + items = append(items, tagEvent.Image) } + n[tag.Tag] = items } - streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) - - for _, tag := range p.streamTags[streamName] { - if _, ok := streamTags[tag]; !ok { + for tag, items := range s { + newItems, ok := n[tag] + if !ok { p.invocations.Insert(fmt.Sprintf("%s:%s", streamName, tag)) } - } - for _, imageName := range p.streamImages[streamName] { - if _, ok := streamImages[imageName]; !ok { - p.invocations.Insert(fmt.Sprintf("%s|%s", streamName, imageName)) + newItemsIndex := 0 + for itemsIndex := 0; itemsIndex < len(items); itemsIndex++ { + if newItemsIndex < len(newItems) && newItems[newItemsIndex] == items[itemsIndex] { + newItemsIndex++ + continue + } + p.invocations.Insert(fmt.Sprintf("%s|%s|%d|%s", streamName, tag, itemsIndex, items[itemsIndex])) } } return stream, p.err } -func (p *fakeImageStreamDeleter) NotifyImageStreamPrune(stream *imagev1.ImageStream, updatedTags []string, deletedTags []string) { - return -} - type errorForSHA func(dgst string) error type fakeBlobDeleter struct { @@ -2222,10 +2156,10 @@ type fakeBlobDeleter struct { var _ BlobDeleter = &fakeBlobDeleter{} -func (p *fakeBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL *url.URL, blob string) error { +func (p *fakeBlobDeleter) DeleteBlob(blob string) error { p.mutex.Lock() defer p.mutex.Unlock() - p.invocations.Insert(fmt.Sprintf("%s|%s", registryURL.String(), blob)) + p.invocations.Insert(blob) if p.getError == nil { return nil } @@ -2240,10 +2174,10 @@ type fakeLayerLinkDeleter struct { var _ LayerLinkDeleter = &fakeLayerLinkDeleter{} -func (p *fakeLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client, registryURL *url.URL, repo, layer string) error { +func (p *fakeLayerLinkDeleter) DeleteLayerLink(repo, layer string) error { p.mutex.Lock() defer p.mutex.Unlock() - p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL.String(), repo, layer)) + p.invocations.Insert(fmt.Sprintf("%s|%s", repo, layer)) return p.err } @@ -2255,9 +2189,9 @@ type fakeManifestDeleter struct { var _ ManifestDeleter = &fakeManifestDeleter{} -func (p *fakeManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL *url.URL, repo, manifest string) error { +func (p *fakeManifestDeleter) DeleteManifest(repo, manifest string) error { p.mutex.Lock() defer p.mutex.Unlock() - p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL.String(), repo, manifest)) + p.invocations.Insert(fmt.Sprintf("%s|%s", repo, manifest)) return p.err } diff --git a/pkg/cli/admin/prune/imageprune/worker.go b/pkg/cli/admin/prune/imageprune/worker.go deleted file mode 100644 index 7056c93832..0000000000 --- a/pkg/cli/admin/prune/imageprune/worker.go +++ /dev/null @@ -1,359 +0,0 @@ -package imageprune - -import ( - "fmt" - "net/http" - "net/url" - - gonum "github.com/gonum/graph" - "k8s.io/klog/v2" - - kerrapi "k8s.io/apimachinery/pkg/api/errors" - - imagegraph "github.com/openshift/oc/pkg/helpers/graph/imagegraph/nodes" -) - -// ComponentRetention knows all the places where image component needs to be pruned (e.g. global blob store -// and repositories). -type ComponentRetention struct { - ReferencingStreams map[*imagegraph.ImageStreamNode]bool - PrunableGlobally bool -} - -// ComponentRetentions contains prunable locations for all the components of an image. -type ComponentRetentions map[*imagegraph.ImageComponentNode]*ComponentRetention - -func (cr ComponentRetentions) add(comp *imagegraph.ImageComponentNode) *ComponentRetention { - if _, ok := cr[comp]; ok { - return cr[comp] - } - cr[comp] = &ComponentRetention{ - ReferencingStreams: make(map[*imagegraph.ImageStreamNode]bool), - } - return cr[comp] -} - -// Add adds component marked as (not) prunable in the blob store. -func (cr ComponentRetentions) Add( - comp *imagegraph.ImageComponentNode, - globallyPrunable bool, -) *ComponentRetention { - r := cr.add(comp) - r.PrunableGlobally = globallyPrunable - return r -} - -// AddReferencingStreams adds a repository location as (not) prunable to the given component. -func (cr ComponentRetentions) AddReferencingStreams( - comp *imagegraph.ImageComponentNode, - prunable bool, - streams ...*imagegraph.ImageStreamNode, -) *ComponentRetention { - r := cr.add(comp) - for _, n := range streams { - r.ReferencingStreams[n] = prunable - } - return r -} - -// Job is an image pruning job for the Worker. It contains information about single image and related -// components. -type Job struct { - Image *imagegraph.ImageNode - Components ComponentRetentions -} - -func enumerateImageComponents( - crs ComponentRetentions, - compType *imagegraph.ImageComponentType, - withPreserved bool, - handler func(comp *imagegraph.ImageComponentNode, prunable bool), -) { - for c, retention := range crs { - if !withPreserved && !retention.PrunableGlobally { - continue - } - if compType != nil && c.Type != *compType { - continue - } - - handler(c, retention.PrunableGlobally) - } -} - -func enumerateImageStreamComponents( - crs ComponentRetentions, - compType *imagegraph.ImageComponentType, - withPreserved bool, - handler func(comp *imagegraph.ImageComponentNode, stream *imagegraph.ImageStreamNode, prunable bool), -) { - for c, cr := range crs { - if compType != nil && c.Type != *compType { - continue - } - - for s, prunable := range cr.ReferencingStreams { - if withPreserved || prunable { - handler(c, s, prunable) - } - } - } -} - -// Deletion denotes a single deletion of a resource as a result of processing a job. If Parent is nil, the -// deletion occured in the global blob store. Otherwise the parent identities repository location. -type Deletion struct { - Node gonum.Node - Parent gonum.Node -} - -// Failure denotes a pruning failure of a single object. -type Failure struct { - Node gonum.Node - Parent gonum.Node - Err error -} - -var _ error = &Failure{} - -func (pf *Failure) Error() string { return pf.String() } - -func (pf *Failure) String() string { - if pf.Node == nil { - return fmt.Sprintf("failed to prune blob: %v", pf.Err) - } - - switch t := pf.Node.(type) { - case *imagegraph.ImageStreamNode: - return fmt.Sprintf("failed to update ImageStream %s: %v", getName(t.ImageStream), pf.Err) - case *imagegraph.ImageNode: - return fmt.Sprintf("failed to delete Image %s: %v", t.Image.DockerImageReference, pf.Err) - case *imagegraph.ImageComponentNode: - detail := "" - if isn, ok := pf.Parent.(*imagegraph.ImageStreamNode); ok { - detail = " in repository " + getName(isn.ImageStream) - } - switch t.Type { - case imagegraph.ImageComponentTypeConfig: - return fmt.Sprintf("failed to delete image config link %s%s: %v", t.Component, detail, pf.Err) - case imagegraph.ImageComponentTypeLayer: - return fmt.Sprintf("failed to delete image layer link %s%s: %v", t.Component, detail, pf.Err) - case imagegraph.ImageComponentTypeManifest: - return fmt.Sprintf("failed to delete image manifest link %s%s: %v", t.Component, detail, pf.Err) - default: - return fmt.Sprintf("failed to delete %s%s: %v", t.String(), detail, pf.Err) - } - default: - return fmt.Sprintf("failed to delete %v: %v", t, pf.Err) - } -} - -// JobResult is a result of job's processing. -type JobResult struct { - Job *Job - Deletions []Deletion - Failures []Failure -} - -func (jr *JobResult) update(deletions []Deletion, failures []Failure) *JobResult { - jr.Deletions = append(jr.Deletions, deletions...) - jr.Failures = append(jr.Failures, failures...) - return jr -} - -// Worker knows how to prune image and its related components. -type Worker interface { - // Run is supposed to be run as a go-rutine. It terminates when nil is received through the in channel. - Run(in <-chan *Job, out chan<- JobResult) -} - -type worker struct { - algorithm pruneAlgorithm - registryClient *http.Client - registryURL *url.URL - imagePruner ImageDeleter - streamPruner ImageStreamDeleter - layerLinkPruner LayerLinkDeleter - blobPruner BlobDeleter - manifestPruner ManifestDeleter -} - -var _ Worker = &worker{} - -// NewWorker creates a new pruning worker. -func NewWorker( - algorithm pruneAlgorithm, - registryClientFactory RegistryClientFactoryFunc, - registryURL *url.URL, - imagePrunerFactory ImagePrunerFactoryFunc, - streamPruner ImageStreamDeleter, - layerLinkPruner LayerLinkDeleter, - blobPruner BlobDeleter, - manifestPruner ManifestDeleter, -) (Worker, error) { - client, err := registryClientFactory() - if err != nil { - return nil, err - } - - imagePruner, err := imagePrunerFactory() - if err != nil { - return nil, err - } - - return &worker{ - algorithm: algorithm, - registryClient: client, - registryURL: registryURL, - imagePruner: imagePruner, - streamPruner: streamPruner, - layerLinkPruner: layerLinkPruner, - blobPruner: blobPruner, - manifestPruner: manifestPruner, - }, nil -} - -func (w *worker) Run(in <-chan *Job, out chan<- JobResult) { - for { - job, more := <-in - if !more { - return - } - out <- *w.prune(job) - } -} - -func (w *worker) prune(job *Job) *JobResult { - res := &JobResult{Job: job} - - blobDeletions, blobFailures := []Deletion{}, []Failure{} - - if w.algorithm.pruneRegistry { - // NOTE: not found errors are treated as success - res.update(pruneImageComponents( - w.registryClient, - w.registryURL, - job.Components, - w.layerLinkPruner, - )) - - blobDeletions, blobFailures = pruneBlobs( - w.registryClient, - w.registryURL, - job.Components, - w.blobPruner, - ) - res.update(blobDeletions, blobFailures) - - res.update(pruneManifests( - w.registryClient, - w.registryURL, - job.Components, - w.manifestPruner, - )) - } - - // Keep the image object when its blobs could not be deleted and the image is ostensibly (we cannot be - // sure unless we ask the registry for blob's existence) still complete. Thanks to the preservation, the - // blobs can be identified and deleted next time. - if len(blobDeletions) > 0 || len(blobFailures) == 0 { - res.update(pruneImages(job.Image, w.imagePruner)) - } - - return res -} - -// pruneImages invokes imagePruner.DeleteImage with each image that is prunable. -func pruneImages( - imageNode *imagegraph.ImageNode, - imagePruner ImageDeleter, -) (deletions []Deletion, failures []Failure) { - err := imagePruner.DeleteImage(imageNode.Image) - if err != nil { - if kerrapi.IsNotFound(err) { - klog.V(2).Infof("Skipping image %s that no longer exists", imageNode.Image.Name) - } else { - failures = append(failures, Failure{Node: imageNode, Err: err}) - } - } else { - deletions = append(deletions, Deletion{Node: imageNode}) - } - - return -} - -// pruneImageComponents invokes layerLinkDeleter.DeleteLayerLink for each repository layer link to -// be deleted from the registry. -func pruneImageComponents( - registryClient *http.Client, - registryURL *url.URL, - crs ComponentRetentions, - layerLinkDeleter LayerLinkDeleter, -) (deletions []Deletion, failures []Failure) { - enumerateImageStreamComponents(crs, nil, false, func( - comp *imagegraph.ImageComponentNode, - stream *imagegraph.ImageStreamNode, - _ bool, - ) { - if comp.Type == imagegraph.ImageComponentTypeManifest { - return - } - streamName := getName(stream.ImageStream) - klog.V(4).Infof("Pruning repository %s/%s: %s", registryURL.Host, streamName, comp.Describe()) - err := layerLinkDeleter.DeleteLayerLink(registryClient, registryURL, streamName, comp.Component) - if err != nil { - failures = append(failures, Failure{Node: comp, Parent: stream, Err: err}) - } else { - deletions = append(deletions, Deletion{Node: comp, Parent: stream}) - } - }) - - return -} - -// pruneBlobs invokes blobPruner.DeleteBlob for each blob to be deleted from the registry. -func pruneBlobs( - registryClient *http.Client, - registryURL *url.URL, - crs ComponentRetentions, - blobPruner BlobDeleter, -) (deletions []Deletion, failures []Failure) { - enumerateImageComponents(crs, nil, false, func(comp *imagegraph.ImageComponentNode, prunable bool) { - err := blobPruner.DeleteBlob(registryClient, registryURL, comp.Component) - if err != nil { - failures = append(failures, Failure{Node: comp, Err: err}) - } else { - deletions = append(deletions, Deletion{Node: comp}) - } - }) - - return -} - -// pruneManifests invokes manifestPruner.DeleteManifest for each repository -// manifest to be deleted from the registry. -func pruneManifests( - registryClient *http.Client, - registryURL *url.URL, - crs ComponentRetentions, - manifestPruner ManifestDeleter, -) (deletions []Deletion, failures []Failure) { - manifestType := imagegraph.ImageComponentTypeManifest - enumerateImageStreamComponents(crs, &manifestType, false, func( - manifestNode *imagegraph.ImageComponentNode, - stream *imagegraph.ImageStreamNode, - _ bool, - ) { - repoName := getName(stream.ImageStream) - - klog.V(4).Infof("Pruning manifest %s in the repository %s/%s", manifestNode.Component, registryURL.Host, repoName) - err := manifestPruner.DeleteManifest(registryClient, registryURL, repoName, manifestNode.Component) - if err != nil { - failures = append(failures, Failure{Node: manifestNode, Parent: stream, Err: err}) - } else { - deletions = append(deletions, Deletion{Node: manifestNode, Parent: stream}) - } - }) - - return -} diff --git a/pkg/cli/admin/prune/images/images.go b/pkg/cli/admin/prune/images/images.go index 9a16d78d4b..0cd8d56d28 100644 --- a/pkg/cli/admin/prune/images/images.go +++ b/pkg/cli/admin/prune/images/images.go @@ -13,23 +13,17 @@ import ( "os" "regexp" "strings" - "text/tabwriter" "time" - gonum "github.com/gonum/graph" "github.com/spf13/cobra" - "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" kutilerrors "k8s.io/apimachinery/pkg/util/errors" knet "k8s.io/apimachinery/pkg/util/net" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" apimachineryversion "k8s.io/apimachinery/pkg/version" - "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" @@ -46,7 +40,6 @@ import ( "github.com/openshift/library-go/pkg/network/networkutils" "github.com/openshift/oc/pkg/cli/admin/prune/imageprune" - imagegraph "github.com/openshift/oc/pkg/helpers/graph/imagegraph/nodes" "github.com/openshift/oc/pkg/version" ) @@ -221,6 +214,8 @@ func (o *PruneImagesOptions) Complete(f kcmdutil.Factory, cmd *cobra.Command, ar if len(o.ClientConfig.BearerToken) == 0 { return errNoToken } + o.ClientConfig.QPS = 100 + o.ClientConfig.Burst = 100 o.KubeClient, err = kubernetes.NewForConfig(o.ClientConfig) if err != nil { return err @@ -388,50 +383,37 @@ func (o PruneImagesOptions) Run() error { limitRangesMap[limit.Namespace] = limits } - ctx := context.TODO() - allImagesUntyped, _, err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { - return o.ImageClient.Images().List(context.TODO(), opts) - }).List(ctx, metav1.ListOptions{}) + allStreams, err := o.ImageClient.ImageStreams(o.Namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } - allImages := &imagev1.ImageList{} - if err := meta.EachListItem(allImagesUntyped, func(obj runtime.Object) error { - allImages.Items = append(allImages.Items, *obj.(*imagev1.Image)) - return nil - }); err != nil { - return err - } - - imageWatcher, err := o.ImageClient.Images().Watch(context.TODO(), metav1.ListOptions{}) - if err != nil { - utilruntime.HandleError(fmt.Errorf("internal error: failed to watch for images: %v"+ - "\n - image changes will not be detected", err)) - imageWatcher = watch.NewFake() - } - imageStreamWatcher, err := o.ImageClient.ImageStreams(o.Namespace).Watch(context.TODO(), metav1.ListOptions{}) - if err != nil { - utilruntime.HandleError(fmt.Errorf("internal error: failed to watch for image streams: %v"+ - "\n - image stream changes will not be detected", err)) - imageStreamWatcher = watch.NewFake() + allStreamsMap := map[string]*imagev1.ImageStream{} + for i := range allStreams.Items { + stream := &allStreams.Items[i] + allStreamsMap[fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)] = stream } - defer imageStreamWatcher.Stop() - allStreams, err := o.ImageClient.ImageStreams(o.Namespace).List(context.TODO(), metav1.ListOptions{}) + ctx := context.TODO() + allImages := map[string]*imagev1.Image{} + err = pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + return o.ImageClient.Images().List(ctx, opts) + }).EachListItem(ctx, metav1.ListOptions{Limit: 5000}, func(obj runtime.Object) error { + image := obj.(*imagev1.Image) + allImages[image.Name] = image + return nil + }) if err != nil { return err } var ( - registryHost = o.RegistryUrlOverride - registryClientFactory imageprune.RegistryClientFactoryFunc - registryClient *http.Client - registryPinger imageprune.RegistryPinger + registryHost = o.RegistryUrlOverride + registryClient *http.Client + registryPinger imageprune.RegistryPinger ) registryPinger = &imageprune.DryRunRegistryPinger{} - registryClientFactory = imageprune.FakeRegistryClientFactory if o.Confirm && o.PruneRegistry != nil && *o.PruneRegistry { if len(registryHost) == 0 { registryHost, err = imageprune.DetermineRegistryHost(allImages, allStreams) @@ -446,10 +428,7 @@ func (o PruneImagesOptions) Run() error { strings.HasPrefix(registryHost, "http://") } - registryClientFactory = func() (*http.Client, error) { - return getRegistryClient(o.ClientConfig, o.CABundle, insecure) - } - registryClient, err = registryClientFactory() + registryClient, err = getRegistryClient(o.ClientConfig, o.CABundle, insecure) if err != nil { return err } @@ -470,28 +449,24 @@ func (o PruneImagesOptions) Run() error { } options := imageprune.PrunerOptions{ - KeepYoungerThan: o.KeepYoungerThan, - KeepTagRevisions: o.KeepTagRevisions, - PruneOverSizeLimit: o.PruneOverSizeLimit, - AllImages: o.AllImages, - Images: allImages, - ImageWatcher: imageWatcher, - Streams: allStreams, - StreamWatcher: imageStreamWatcher, - Pods: allPods, - RCs: allRCs, - BCs: allBCs, - Builds: allBuilds, - DSs: allDSs, - Deployments: allDeployments, - DCs: allDCs, - RSs: allRSs, - LimitRanges: limitRangesMap, - DryRun: o.Confirm == false, - RegistryClientFactory: registryClientFactory, - RegistryURL: registryURL, - PruneRegistry: o.PruneRegistry, - IgnoreInvalidRefs: o.IgnoreInvalidRefs, + KeepYoungerThan: o.KeepYoungerThan, + KeepTagRevisions: o.KeepTagRevisions, + PruneOverSizeLimit: o.PruneOverSizeLimit, + AllImages: o.AllImages, + Images: allImages, + Streams: allStreamsMap, + Pods: allPods, + RCs: allRCs, + BCs: allBCs, + Builds: allBuilds, + DSs: allDSs, + Deployments: allDeployments, + DCs: allDCs, + RSs: allRSs, + LimitRanges: limitRangesMap, + DryRun: o.Confirm == false, + PruneRegistry: o.PruneRegistry, + IgnoreInvalidRefs: o.IgnoreInvalidRefs, } if o.Namespace != metav1.NamespaceAll { options.Namespace = o.Namespace @@ -505,27 +480,18 @@ func (o PruneImagesOptions) Run() error { return fmt.Errorf("failed to build graph - no changes made") } - imagePrunerFactory := func() (imageprune.ImageDeleter, error) { - return &describingImageDeleter{w: o.Out, errOut: o.ErrOut}, nil - } imageStreamDeleter := &describingImageStreamDeleter{w: o.Out, errOut: o.ErrOut} layerLinkDeleter := &describingLayerLinkDeleter{w: o.Out, errOut: o.ErrOut} - blobDeleter := &describingBlobDeleter{w: o.Out, errOut: o.ErrOut} manifestDeleter := &describingManifestDeleter{w: o.Out, errOut: o.ErrOut} + blobDeleter := &describingBlobDeleter{w: o.Out, errOut: o.ErrOut} + imageDeleter := &describingImageDeleter{w: o.Out, errOut: o.ErrOut} if o.Confirm { imageStreamDeleter.delegate = imageprune.NewImageStreamDeleter(o.ImageClient) - layerLinkDeleter.delegate = imageprune.NewLayerLinkDeleter() - blobDeleter.delegate = imageprune.NewBlobDeleter() - manifestDeleter.delegate = imageprune.NewManifestDeleter() - - imagePrunerFactory = func() (imageprune.ImageDeleter, error) { - imageClient, err := o.ImageClientFactory() - if err != nil { - return nil, err - } - return imageprune.NewImageDeleter(imageClient), nil - } + layerLinkDeleter.delegate = imageprune.NewLayerLinkDeleter(registryClient, registryURL) + manifestDeleter.delegate = imageprune.NewManifestDeleter(registryClient, registryURL) + blobDeleter.delegate = imageprune.NewBlobDeleter(registryClient, registryURL) + imageDeleter.delegate = imageprune.NewImageDeleter(o.ImageClient) } else { fmt.Fprintln(o.ErrOut, "Dry run enabled - no modifications will be made. Add --confirm to remove images") } @@ -534,99 +500,15 @@ func (o PruneImagesOptions) Run() error { fmt.Fprintln(o.Out, "Only API objects will be removed. No modifications to the image registry will be made.") } - deletions, failures := pruner.Prune( - imagePrunerFactory, + stats, errs := pruner.Prune( imageStreamDeleter, layerLinkDeleter, - blobDeleter, manifestDeleter, + blobDeleter, + imageDeleter, ) - printSummary(o.Out, deletions, failures) - if len(failures) == 1 { - return &failures[0] - } - if len(failures) > 0 { - return fmt.Errorf("failed") - } - return nil -} - -func printSummary(out io.Writer, deletions []imageprune.Deletion, failures []imageprune.Failure) { - // TODO: for higher verbosity, sum by error type - if len(failures) == 0 { - fmt.Fprintf(out, "Deleted %d objects.\n", len(deletions)) - } else { - fmt.Fprintf(out, "Deleted %d objects out of %d.\n", len(deletions), len(deletions)+len(failures)) - fmt.Fprintf(out, "Failed to delete %d objects.\n", len(failures)) - } - if !klog.V(2).Enabled() { - return - } - - fmt.Fprintf(out, "\n") - - w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) - defer w.Flush() - - buckets := make(map[string]struct{ deletions, failures, total uint64 }) - count := func(node gonum.Node, parent gonum.Node, deletions uint64, failures uint64) { - bucket := "" - switch t := node.(type) { - case *imagegraph.ImageStreamNode: - bucket = "is" - case *imagegraph.ImageNode: - bucket = "image" - case *imagegraph.ImageComponentNode: - bucket = "component/" + string(t.Type) - if parent == nil { - bucket = "blob" - } - default: - bucket = fmt.Sprintf("other/%T", t) - } - c := buckets[bucket] - c.deletions += deletions - c.failures += failures - c.total += deletions + failures - buckets[bucket] = c - } - - for _, d := range deletions { - count(d.Node, d.Parent, 1, 0) - } - for _, f := range failures { - count(f.Node, f.Parent, 0, 1) - } - - printAndPopBucket := func(name string, desc string) { - cnt, ok := buckets[name] - if ok { - delete(buckets, name) - } - if cnt.total == 0 { - return - } - fmt.Fprintf(w, "%s:\t%d\n", desc, cnt.deletions) - if cnt.failures == 0 { - return - } - // add padding before failures to make it appear subordinate to the line above - for i := 0; i < len(desc)-len("failures"); i++ { - fmt.Fprintf(w, " ") - } - fmt.Fprintf(w, "failures:\t%d\n", cnt.failures) - } - - printAndPopBucket("is", "Image Stream updates") - printAndPopBucket("image", "Image deletions") - printAndPopBucket("blob", "Blob deletions") - printAndPopBucket("component/"+string(imagegraph.ImageComponentTypeManifest), "Image Manifest Link deletions") - printAndPopBucket("component/"+string(imagegraph.ImageComponentTypeConfig), "Image Config Link deletions") - printAndPopBucket("component/"+string(imagegraph.ImageComponentTypeLayer), "Image Layer Link deletions") - - for name := range buckets { - printAndPopBucket(name, fmt.Sprintf("%s deletions", strings.TrimPrefix(name, "other/"))) - } + fmt.Fprintf(o.Out, "Summary: %s\n", stats) + return errs } func (o *PruneImagesOptions) printGraphBuildErrors(errs kutilerrors.Aggregate) { @@ -662,24 +544,29 @@ func (o *PruneImagesOptions) printGraphBuildErrors(errs kutilerrors.Aggregate) { // describingImageStreamDeleter prints information about each image stream update. // If a delegate exists, its DeleteImageStream function is invoked prior to returning. type describingImageStreamDeleter struct { - w io.Writer - delegate imageprune.ImageStreamDeleter - headerPrinted bool - errOut io.Writer + w io.Writer + delegate imageprune.ImageStreamDeleter + errOut io.Writer } var _ imageprune.ImageStreamDeleter = &describingImageStreamDeleter{} func (p *describingImageStreamDeleter) GetImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) { - return stream, nil + if p.delegate == nil { + return stream, nil + } + + return p.delegate.GetImageStream(stream) } -func (p *describingImageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream) (*imagev1.ImageStream, error) { +func (p *describingImageStreamDeleter) UpdateImageStream(stream *imagev1.ImageStream, deletedItems int) (*imagev1.ImageStream, error) { + fmt.Fprintf(p.w, "Deleting %d items from image stream %s/%s\n", deletedItems, stream.Namespace, stream.Name) + if p.delegate == nil { return stream, nil } - updatedStream, err := p.delegate.UpdateImageStream(stream) + updatedStream, err := p.delegate.UpdateImageStream(stream, deletedItems) if err != nil { fmt.Fprintf(p.errOut, "error updating image stream %s/%s to remove image references: %v\n", stream.Namespace, stream.Name, err) } @@ -687,22 +574,12 @@ func (p *describingImageStreamDeleter) UpdateImageStream(stream *imagev1.ImageSt return updatedStream, err } -func (p *describingImageStreamDeleter) NotifyImageStreamPrune(stream *imagev1.ImageStream, updatedTags []string, deletedTags []string) { - if len(updatedTags) > 0 { - fmt.Fprintf(p.w, "Updating istags %s/%s: %s\n", stream.Namespace, stream.Name, strings.Join(updatedTags, ", ")) - } - if len(deletedTags) > 0 { - fmt.Fprintf(p.w, "Deleting istags %s/%s: %s\n", stream.Namespace, stream.Name, strings.Join(deletedTags, ", ")) - } -} - // describingImageDeleter prints information about each image being deleted. // If a delegate exists, its DeleteImage function is invoked prior to returning. type describingImageDeleter struct { - w io.Writer - delegate imageprune.ImageDeleter - headerPrinted bool - errOut io.Writer + w io.Writer + delegate imageprune.ImageDeleter + errOut io.Writer } var _ imageprune.ImageDeleter = &describingImageDeleter{} @@ -725,22 +602,21 @@ func (p *describingImageDeleter) DeleteImage(image *imagev1.Image) error { // describingLayerLinkDeleter prints information about each repo layer link being deleted. If a delegate // exists, its DeleteLayerLink function is invoked prior to returning. type describingLayerLinkDeleter struct { - w io.Writer - delegate imageprune.LayerLinkDeleter - headerPrinted bool - errOut io.Writer + w io.Writer + delegate imageprune.LayerLinkDeleter + errOut io.Writer } var _ imageprune.LayerLinkDeleter = &describingLayerLinkDeleter{} -func (p *describingLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client, registryURL *url.URL, repo, name string) error { +func (p *describingLayerLinkDeleter) DeleteLayerLink(repo, name string) error { fmt.Fprintf(p.w, "Deleting layer link %s in repository %s\n", name, repo) if p.delegate == nil { return nil } - err := p.delegate.DeleteLayerLink(registryClient, registryURL, repo, name) + err := p.delegate.DeleteLayerLink(repo, name) if err != nil { fmt.Fprintf(p.errOut, "error deleting repository %s layer link %s from the registry: %v\n", repo, name, err) } @@ -751,22 +627,21 @@ func (p *describingLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client // describingBlobDeleter prints information about each blob being deleted. If a // delegate exists, its DeleteBlob function is invoked prior to returning. type describingBlobDeleter struct { - w io.Writer - delegate imageprune.BlobDeleter - headerPrinted bool - errOut io.Writer + w io.Writer + delegate imageprune.BlobDeleter + errOut io.Writer } var _ imageprune.BlobDeleter = &describingBlobDeleter{} -func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL *url.URL, layer string) error { +func (p *describingBlobDeleter) DeleteBlob(layer string) error { fmt.Fprintf(p.w, "Deleting blob %s\n", layer) if p.delegate == nil { return nil } - err := p.delegate.DeleteBlob(registryClient, registryURL, layer) + err := p.delegate.DeleteBlob(layer) if err != nil { fmt.Fprintf(p.errOut, "error deleting blob %s from the registry: %v\n", layer, err) } @@ -778,22 +653,21 @@ func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registry // deleted. If a delegate exists, its DeleteManifest function is invoked prior // to returning. type describingManifestDeleter struct { - w io.Writer - delegate imageprune.ManifestDeleter - headerPrinted bool - errOut io.Writer + w io.Writer + delegate imageprune.ManifestDeleter + errOut io.Writer } var _ imageprune.ManifestDeleter = &describingManifestDeleter{} -func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL *url.URL, repo, manifest string) error { +func (p *describingManifestDeleter) DeleteManifest(repo, manifest string) error { fmt.Fprintf(p.w, "Deleting manifest link %s in repository %s\n", manifest, repo) if p.delegate == nil { return nil } - err := p.delegate.DeleteManifest(registryClient, registryURL, repo, manifest) + err := p.delegate.DeleteManifest(repo, manifest) if err != nil { fmt.Fprintf(p.errOut, "error deleting manifest link %s from repository %s: %v\n", manifest, repo, err) } diff --git a/pkg/cli/admin/prune/images/images_test.go b/pkg/cli/admin/prune/images/images_test.go index 29ca884a57..5bf07c2c73 100644 --- a/pkg/cli/admin/prune/images/images_test.go +++ b/pkg/cli/admin/prune/images/images_test.go @@ -162,9 +162,9 @@ func TestImagePruneErrOnBadReference(t *testing.T) { } } expBadRefErrors := sets.NewString( - `Pod[foo/pod1]: invalid container image reference "invalid image reference": invalid reference format`, - `BuildConfig[foo/bc1]: invalid ImageStreamImage reference "bar:invalid-digest": expected exactly one @ in the isimage name "bar:invalid-digest"`, - `Deployment[foo/dep1]: invalid container image reference "do not blame me": invalid reference format`) + `pod/pod1 namespace=foo: container app: invalid image reference "invalid image reference": invalid reference format`, + `buildconfig/bc1 namespace=foo: invalid ImageStreamImage reference "bar:invalid-digest": expected exactly one @ in the isimage name "bar:invalid-digest"`, + `deployment/dep1 namespace=foo: container app: invalid image reference "do not blame me": invalid reference format`) if a, e := badRefErrors, expBadRefErrors; !a.Equal(e) { t.Fatalf("got unexpected invalid reference errors: %s", diff.ObjectDiff(a, e)) diff --git a/pkg/helpers/image/test/util.go b/pkg/helpers/image/test/util.go index 97a76911de..695cdca2e1 100644 --- a/pkg/helpers/image/test/util.go +++ b/pkg/helpers/image/test/util.go @@ -36,13 +36,6 @@ const ( managedByOpenShiftAnnotation = "openshift.io/image.managed" ) -// ImageList turns the given images into ImageList. -func ImageList(images ...imagev1.Image) imagev1.ImageList { - return imagev1.ImageList{ - Items: images, - } -} - // AgedImage creates a test image with specified age. func AgedImage(id, ref string, ageInMinutes int64, layers ...string) imagev1.Image { return CreatedImage(id, ref, time.Now().Add(time.Duration(ageInMinutes)*time.Minute*-1), layers...) @@ -162,6 +155,7 @@ func PodSpecInternal(containerImages ...string) corev1.PodSpec { } for _, image := range containerImages { container := corev1.Container{ + Name: "app", Image: image, } spec.Containers = append(spec.Containers, container) @@ -183,13 +177,6 @@ func PodSpec(containerImages ...string) corev1.PodSpec { return spec } -// StreamList turns the given streams into StreamList. -func StreamList(streams ...imagev1.ImageStream) imagev1.ImageStreamList { - return imagev1.ImageStreamList{ - Items: streams, - } -} - // Stream creates and returns a test ImageStream object 1 minute old func Stream(registry, namespace, name string, tags []imagev1.NamedTagEventList) imagev1.ImageStream { return AgedStream(registry, namespace, name, -1, tags)