Skip to content

Commit

Permalink
add image soft prune (api object yes, registry no)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabemontero committed Dec 5, 2017
1 parent 3133750 commit f74100c
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 15 deletions.
2 changes: 2 additions & 0 deletions contrib/completions/bash/oc
Expand Up @@ -5287,6 +5287,8 @@ _oc_adm_prune_images()
local_nonpersistent_flags+=("--keep-younger-than=")
flags+=("--prune-over-size-limit")
local_nonpersistent_flags+=("--prune-over-size-limit")
flags+=("--prune-registry")
local_nonpersistent_flags+=("--prune-registry")
flags+=("--registry-url=")
local_nonpersistent_flags+=("--registry-url=")
flags+=("--as=")
Expand Down
2 changes: 2 additions & 0 deletions contrib/completions/zsh/oc
Expand Up @@ -5429,6 +5429,8 @@ _oc_adm_prune_images()
local_nonpersistent_flags+=("--keep-younger-than=")
flags+=("--prune-over-size-limit")
local_nonpersistent_flags+=("--prune-over-size-limit")
flags+=("--prune-registry")
local_nonpersistent_flags+=("--prune-registry")
flags+=("--registry-url=")
local_nonpersistent_flags+=("--registry-url=")
flags+=("--as=")
Expand Down
32 changes: 21 additions & 11 deletions pkg/image/prune/prune.go
Expand Up @@ -64,6 +64,7 @@ type pruneAlgorithm struct {
pruneOverSizeLimit bool
namespace string
allImages bool
pruneRegistry bool
}

// ImageDeleter knows how to remove images from OpenShift.
Expand Down Expand Up @@ -118,6 +119,10 @@ type PrunerOptions struct {
PruneOverSizeLimit *bool
// AllImages considers all images for pruning, not just those pushed directly to the registry.
AllImages *bool
// PruneRegistry controls whether to both prune the API Objects in etcd and corresponding
// data in the registry, or just prune the API Object and defer on the corresponding data in
// the registry
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
Expand Down Expand Up @@ -239,6 +244,10 @@ func NewPruner(options PrunerOptions) (Pruner, kerrors.Aggregate) {
if options.AllImages != nil {
algorithm.allImages = *options.AllImages
}
algorithm.pruneRegistry = true
if options.PruneRegistry != nil {
algorithm.pruneRegistry = *options.PruneRegistry
}
algorithm.namespace = options.Namespace

p := &pruner{
Expand Down Expand Up @@ -1002,19 +1011,20 @@ func (p *pruner) Prune(
return err
}

prunableComponents := getPrunableComponents(p.g, prunableImageIDs)

var errs []error

errs = append(errs, pruneImageComponents(p.g, p.registryClient, p.registryURL, prunableComponents, layerLinkPruner)...)
errs = append(errs, pruneBlobs(p.g, p.registryClient, p.registryURL, prunableComponents, blobPruner)...)
errs = append(errs, pruneManifests(p.g, p.registryClient, p.registryURL, prunableImageNodes, manifestPruner)...)

if len(errs) > 0 {
// If we had any errors deleting layers, blobs, or manifest data from the registry,
// stop here and don't delete any images. This way, you can rerun prune and retry
// things that failed.
return kerrors.NewAggregate(errs)
if p.algorithm.pruneRegistry {
prunableComponents := getPrunableComponents(p.g, prunableImageIDs)
errs = append(errs, pruneImageComponents(p.g, p.registryClient, p.registryURL, prunableComponents, layerLinkPruner)...)
errs = append(errs, pruneBlobs(p.g, p.registryClient, p.registryURL, prunableComponents, blobPruner)...)
errs = append(errs, pruneManifests(p.g, p.registryClient, p.registryURL, prunableImageNodes, manifestPruner)...)

if len(errs) > 0 {
// If we had any errors deleting layers, blobs, or manifest data from the registry,
// stop here and don't delete any images. This way, you can rerun prune and retry
// things that failed.
return kerrors.NewAggregate(errs)
}
}

errs = pruneImages(p.g, prunableImageNodes, imagePruner)
Expand Down
54 changes: 50 additions & 4 deletions pkg/image/prune/prune_test.go
Expand Up @@ -47,6 +47,7 @@ func TestImagePruning(t *testing.T) {
name string
pruneOverSizeLimit *bool
allImages *bool
pruneRegistry *bool
keepTagRevisions *int
namespace string
images imageapi.ImageList
Expand Down Expand Up @@ -116,6 +117,15 @@ func TestImagePruning(t *testing.T) {
},
},

{
name: "pod phase succeeded - prune leave registry alone",
pruneRegistry: newBool(false),
images: testutil.ImageList(testutil.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")),
pods: testutil.PodList(testutil.Pod("foo", "pod1", kapi.PodSucceeded, registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")),
expectedImageDeletions: []string{"sha256:0000000000000000000000000000000000000000000000000000000000000000"},
expectedBlobDeletions: []string{},
},

{
name: "pod phase succeeded, pod less than min pruning age - don't prune",
images: testutil.ImageList(testutil.Image("sha256:0000000000000000000000000000000000000000000000000000000000000000", registryHost+"/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")),
Expand Down Expand Up @@ -895,6 +905,9 @@ func TestImagePruning(t *testing.T) {
options.KeepYoungerThan = &youngerThan
options.KeepTagRevisions = &tagRevisions
}
if test.pruneRegistry != nil {
options.PruneRegistry = test.pruneRegistry
}
p, err := NewPruner(options)
if err != nil {
if len(test.expectedErrorString) > 0 {
Expand Down Expand Up @@ -1022,10 +1035,12 @@ func TestRegistryPruning(t *testing.T) {
expectedLayerLinkDeletions sets.String
expectedBlobDeletions sets.String
expectedManifestDeletions sets.String
pruneRegistry bool
pingErr error
}{
{
name: "layers unique to id1 pruned",
name: "layers unique to id1 pruned",
pruneRegistry: true,
images: testutil.ImageList(
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &testutil.Config1, "layer1", "layer2", "layer3", "layer4"),
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &testutil.Config2, "layer3", "layer4", "layer5", "layer6"),
Expand Down Expand Up @@ -1059,7 +1074,8 @@ func TestRegistryPruning(t *testing.T) {
},

{
name: "no pruning when no images are pruned",
name: "no pruning when no images are pruned",
pruneRegistry: true,
images: testutil.ImageList(
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &testutil.Config1, "layer1", "layer2", "layer3", "layer4"),
),
Expand All @@ -1076,7 +1092,8 @@ func TestRegistryPruning(t *testing.T) {
},

{
name: "blobs pruned when streams have already been deleted",
name: "blobs pruned when streams have already been deleted",
pruneRegistry: true,
images: testutil.ImageList(
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &testutil.Config1, "layer1", "layer2", "layer3", "layer4"),
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &testutil.Config2, "layer3", "layer4", "layer5", "layer6"),
Expand All @@ -1096,7 +1113,8 @@ func TestRegistryPruning(t *testing.T) {
},

{
name: "config used as a layer",
name: "config used as a layer",
pruneRegistry: true,
images: testutil.ImageList(
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &testutil.Config1, "layer1", "layer2", "layer3", testutil.Config1),
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &testutil.Config2, "layer3", "layer4", "layer5", testutil.Config1),
Expand Down Expand Up @@ -1129,6 +1147,33 @@ func TestRegistryPruning(t *testing.T) {
"https://registry1.io|foo/bar|sha256:0000000000000000000000000000000000000000000000000000000000000001",
),
},

{
name: "config used as a layer, but leave registry alone",
pruneRegistry: false,
images: testutil.ImageList(
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001", &testutil.Config1, "layer1", "layer2", "layer3", testutil.Config1),
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002", &testutil.Config2, "layer3", "layer4", "layer5", testutil.Config1),
testutil.ImageWithLayers("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/other@sha256:0000000000000000000000000000000000000000000000000000000000000003", nil, "layer3", "layer4", "layer6", testutil.Config1),
),
streams: testutil.StreamList(
testutil.Stream("registry1.io", "foo", "bar", testutil.Tags(
testutil.Tag("latest",
testutil.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"),
testutil.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000001", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000001"),
),
)),
testutil.Stream("registry1.io", "foo", "other", testutil.Tags(
testutil.Tag("latest",
testutil.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000003", "registry1.io/foo/other@sha256:0000000000000000000000000000000000000000000000000000000000000003"),
testutil.TagEvent("sha256:0000000000000000000000000000000000000000000000000000000000000002", "registry1.io/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000002"),
),
)),
),
expectedLayerLinkDeletions: sets.NewString(),
expectedBlobDeletions: sets.NewString(),
expectedManifestDeletions: sets.NewString(),
},
}

for _, test := range tests {
Expand All @@ -1138,6 +1183,7 @@ func TestRegistryPruning(t *testing.T) {
options := PrunerOptions{
KeepYoungerThan: &keepYoungerThan,
KeepTagRevisions: &keepTagRevisions,
PruneRegistry: &test.pruneRegistry,
Images: &test.images,
Streams: &test.streams,
Pods: &kapi.PodList{},
Expand Down
9 changes: 9 additions & 0 deletions pkg/oc/admin/prune/images.go
Expand Up @@ -104,6 +104,7 @@ var (
defaultKeepYoungerThan = 60 * time.Minute
defaultKeepTagRevisions = 3
defaultPruneImageOverSizeLimit = false
defaultPruneRegistry = true
)

// PruneImagesOptions holds all the required options for pruning images.
Expand All @@ -117,6 +118,7 @@ type PruneImagesOptions struct {
RegistryUrlOverride string
Namespace string
ForceInsecure bool
PruneRegistry *bool

ClientConfig *restclient.Config
AppsClient appsclient.AppsInterface
Expand All @@ -137,6 +139,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
KeepYoungerThan: &defaultKeepYoungerThan,
KeepTagRevisions: &defaultKeepTagRevisions,
PruneOverSizeLimit: &defaultPruneImageOverSizeLimit,
PruneRegistry: &defaultPruneRegistry,
AllImages: &allImages,
}

Expand All @@ -162,6 +165,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file. It cannot be used together with --force-insecure.")
cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works. Particular transport protocol can be enforced using '<scheme>://' prefix.")
cmd.Flags().BoolVar(&opts.ForceInsecure, "force-insecure", opts.ForceInsecure, "If true, allow an insecure connection to the docker registry that is hosted via HTTP or has an invalid HTTPS certificate. Whenever possible, use --certificate-authority instead of this dangerous option.")
cmd.Flags().BoolVar(opts.PruneRegistry, "prune-registry", *opts.PruneRegistry, "If false, the prune operation will clean up image API objects, but the none of the associated content in the registry is removed. Note, if only image API objects are cleaned up through use of this flag, the only means for subsequently cleaning up registry data corresponding to those image API objects is to employ the 'hard prune' administrative task.")

return cmd
}
Expand Down Expand Up @@ -383,6 +387,7 @@ func (o PruneImagesOptions) Run() error {
DryRun: o.Confirm == false,
RegistryClient: registryClient,
RegistryURL: registryURL,
PruneRegistry: o.PruneRegistry,
}
if o.Namespace != metav1.NamespaceAll {
options.Namespace = o.Namespace
Expand Down Expand Up @@ -412,6 +417,10 @@ func (o PruneImagesOptions) Run() error {
fmt.Fprintln(o.ErrOut, "Dry run enabled - no modifications will be made. Add --confirm to remove images")
}

if o.PruneRegistry != nil && !*o.PruneRegistry {
fmt.Fprintln(o.Out, "Only API objects will be removed. No modifications to the image registry will be made.")
}

return pruner.Prune(imageDeleter, imageStreamDeleter, layerLinkDeleter, blobDeleter, manifestDeleter)
}

Expand Down
53 changes: 53 additions & 0 deletions test/extended/images/prune.go
Expand Up @@ -86,6 +86,25 @@ var _ = g.Describe("[Feature:ImagePrune][registry][Serial] Image prune", func()
g.It("should prune old image with config", func() { testPruneImages(oc, 2) })
})

g.Describe("with --prune-registry==false", func() {
g.JustBeforeEach(func() {
if !*originalAcceptSchema2 {
g.By("ensure the registry accepts schema 2")
err := EnsureRegistryAcceptsSchema2(oc, true)
o.Expect(err).NotTo(o.HaveOccurred())
}
})

g.AfterEach(func() {
if !*originalAcceptSchema2 {
err := EnsureRegistryAcceptsSchema2(oc, false)
o.Expect(err).NotTo(o.HaveOccurred())
}
})

g.It("should prune old image but skip registry", func() { testSoftPruneImages(oc) })
})

g.Describe("with default --all flag", func() {
g.JustBeforeEach(func() {
if !*originalAcceptSchema2 {
Expand Down Expand Up @@ -239,6 +258,40 @@ func testPruneImages(oc *exutil.CLI, schemaVersion int) {
o.Expect(imgPrune.DockerImageMetadata.Size <= keepSize-confirmSize).To(o.BeTrue())
}

func testSoftPruneImages(oc *exutil.CLI) {
isName := "prune"

oc.SetOutputDir(exutil.TestContext.OutputDir)
outSink := g.GinkgoWriter

cleanUp := NewCleanUpContainer(oc)
defer cleanUp.Run()

dClient, err := testutil.NewDockerClient()
o.Expect(err).NotTo(o.HaveOccurred())

g.By("build two images using Docker and push them")
imgPruneName, _, err := BuildAndPushImageOfSizeWithDocker(oc, dClient, isName, "latest", testImageSize, 2, outSink, true, true)
o.Expect(err).NotTo(o.HaveOccurred())
cleanUp.AddImage(imgPruneName, "", "")
cleanUp.AddImageStream(isName)
pruneSize, err := GetRegistryStorageSize(oc)
o.Expect(err).NotTo(o.HaveOccurred())

g.By("prune the first image uploaded (confirm, skipping registry)")
output, err := oc.WithoutNamespace().Run("adm").Args("prune", "images", "--keep-tag-revisions=1", "--keep-younger-than=0", "--confirm", "--prune-registry=false").Output()

g.By("verify images, layers and configs about to be pruned")
o.Expect(err).NotTo(o.HaveOccurred())
o.Expect(output).NotTo(o.ContainSubstring(imgPruneName))
o.Expect(output).To(o.ContainSubstring("Only API objects will be removed"))

skipRegistrySize, err := GetRegistryStorageSize(oc)
o.Expect(err).NotTo(o.HaveOccurred())
g.By(fmt.Sprintf("confirming storage size: sizeAfterPrune=%d == beforePruneSize=%d", skipRegistrySize, pruneSize))
o.Expect(skipRegistrySize == pruneSize).To(o.BeTrue())
}

func testPruneAllImages(oc *exutil.CLI, setAllImagesToFalse bool, schemaVersion int) {
isName := fmt.Sprintf("prune-schema%d-all-images-%t", schemaVersion, setAllImagesToFalse)
repository := oc.Namespace() + "/" + isName
Expand Down

0 comments on commit f74100c

Please sign in to comment.