diff options
author | valerijf <valerijf@yahoo-inc.com> | 2016-09-06 15:57:00 +0200 |
---|---|---|
committer | valerijf <valerijf@yahoo-inc.com> | 2016-09-06 15:57:00 +0200 |
commit | 46668e41b3fd77b84eb7613e1ec830fdc02f98d5 (patch) | |
tree | 3eadfd54124e90aaa77d141e03e774f1fa713373 /docker-api | |
parent | 65727a4bd6c3f676d66deedcc7879ebdb035fd2c (diff) |
DockerImpl interface now provides deleteUnusedDockerImages instead of get
Diffstat (limited to 'docker-api')
4 files changed, 143 insertions, 120 deletions
diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java index f8e636c87c0..3ce7668cd2f 100644 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java +++ b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java @@ -44,9 +44,9 @@ public interface Docker { void deleteImage(DockerImage dockerImage); /** - * Returns the local images that are currently not in use by any container. + * Deletes the local images that are currently not in use by any container and not in the except set. */ - Set<DockerImage> getUnusedDockerImages(); + void deleteUnusedDockerImages(Set<DockerImage> except); /** * TODO: Make this function interruptible, see https://github.com/spotify/docker-client/issues/421 diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java index 0a45dc6c873..bfe39620f84 100644 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java +++ b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java @@ -22,8 +22,8 @@ import com.yahoo.vespa.applicationmodel.HostName; import javax.annotation.concurrent.GuardedBy; import java.io.ByteArrayOutputStream; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -260,111 +260,98 @@ public class DockerImpl implements Docker { dockerClient.removeImageCmd(dockerImage.asString()).exec(); } - @Override - public Set<DockerImage> getUnusedDockerImages() { - // Description of concepts and relationships: - // - a docker image has an id, and refers to its parent image (if any) by image id. - // - a docker image may, in addition to id, have multiple tags, but each tag identifies exactly one image. - // - a docker container refers to its image (exactly one) either by image id or by image tag. - // What this method does to find images considered unused, is build a tree of dependencies - // (e.g. container->tag->image->image) and identify image nodes whose only children (if any) are leaf tags. - // In other words, an image node with no children, or only tag children having no children themselves is unused. - // An image node with an image child is considered used. - // An image node with a container child is considered used. - // An image node with a tag child with a container child is considered used. - try { - final Map<String, DockerObject> objects = new HashMap<>(); - final Map<String, String> dependencies = new HashMap<>(); - - // Populate maps with images (including tags) and their dependencies (parents). - for (Image image : dockerClient.listImagesCmd().withShowAll(true).exec()) { - objects.put(image.getId(), new DockerObject(image.getId(), DockerObjectType.IMAGE)); - if (image.getParentId() != null && !image.getParentId().isEmpty()) { - dependencies.put(image.getId(), image.getParentId()); - } - for (String tag : image.getRepoTags()) { - objects.put(tag, new DockerObject(tag, DockerObjectType.IMAGE_TAG)); - dependencies.put(tag, image.getId()); - } - } - - // Populate maps with containers and their dependency to the image they run on. - for (com.github.dockerjava.api.model.Container container : dockerClient.listContainersCmd().withShowAll(true).exec()) { - objects.put(container.getId(), new DockerObject(container.getId(), DockerObjectType.CONTAINER)); - dependencies.put(container.getId(), container.getImage()); - } + private Map<String, Image> filterOutImagesUsedByContainers( + Map<String, Image> dockerImagesByImageId, List<com.github.dockerjava.api.model.Container> containerList) { + Map<String, Image> filteredDockerImagesByImageId = new HashMap<>(dockerImagesByImageId); - // Now update every object with its dependencies. - dependencies.forEach((fromId, toId) -> { - Optional.ofNullable(objects.get(toId)) - .ifPresent(obj -> obj.addDependee(objects.get(fromId))); - }); - - // Find images that are not in use (i.e. leafs not used by any containers). - return objects.values().stream() - .filter(dockerObject -> dockerObject.type == DockerObjectType.IMAGE) - .filter(dockerObject -> !dockerObject.isInUse()) - .map(obj -> obj.id) - .map(DockerImage::new) - .collect(Collectors.toSet()); - } catch (DockerException e) { - throw new RuntimeException("Unexpected exception", e); + for (com.github.dockerjava.api.model.Container container : containerList) { + String imageToSpare = container.getImageId(); + do { + Image sparedImage = filteredDockerImagesByImageId.remove(imageToSpare); + imageToSpare = sparedImage.getParentId(); + } while (imageToSpare != null && !imageToSpare.isEmpty()); } - } - // Helper enum for calculating which images are unused. - private enum DockerObjectType { - IMAGE_TAG, IMAGE, CONTAINER + return filteredDockerImagesByImageId; } - // Helper class for calculating which images are unused. - private static class DockerObject { - public final String id; - public final DockerObjectType type; - private final List<DockerObject> dependees = new LinkedList<>(); - - public DockerObject(final String id, final DockerObjectType type) { - this.id = id; - this.type = type; - } - - public boolean isInUse() { - if (type == DockerObjectType.CONTAINER) { - return true; - } - - if (dependees.isEmpty()) { - return false; - } - - if (type == DockerObjectType.IMAGE) { - if (dependees.stream().anyMatch(obj -> obj.type == DockerObjectType.IMAGE)) { - return true; + private Map<String, Image> filterOutExceptImages(Map<String, Image> dockerImagesByImageId, Set<DockerImage> except) { + Map<String, Image> dockerImageByImageTags = new HashMap<>(); + // Transform map of image ID:image to map of image tag:image (for each tag the image has) + for (Map.Entry<String, Image> entry : dockerImagesByImageId.entrySet()) { + String[] repoTags = entry.getValue().getRepoTags(); + // If no tags present, fall back to image ID:image + if (repoTags.length == 0 || repoTags[0].isEmpty()) { + dockerImageByImageTags.put(entry.getKey(), entry.getValue()); + } else { + for (String tag : repoTags) { + if (!tag.isEmpty()) { + dockerImageByImageTags.put(tag, entry.getValue()); + } } } - - return dependees.stream().anyMatch(DockerObject::isInUse); } - public void addDependee(final DockerObject dockerObject) { - dependees.add(dockerObject); - } + // Remove images we want to keep from the map of unused images, also recursively keep the parents of images we want to keep + except.forEach(image -> { + String imageToSpare = image.asString(); + do { + Image sparedImage = dockerImageByImageTags.remove(imageToSpare); + imageToSpare = sparedImage == null ? null : sparedImage.getParentId(); + } while (imageToSpare != null && !imageToSpare.isEmpty()); + }); + return dockerImageByImageTags; + } - @Override - public String toString() { - return "DockerObject {" - + " id=" + id - + " type=" + type.name().toLowerCase() - + " inUse=" + isInUse() - + " dependees=" + dependees.stream().map(obj -> obj.id).collect(Collectors.toList()) - + " }"; + /** + * Generates lists of images that are safe to delete, in the order that is safe to delete them (children before + * parents). The function starts with a map of all images and the filters out images that are used now or will be + * used in near future (through the use of except set). + * + * @param except set of image tags to keep, regardless whether they are being used right now or not. + * @return List of image tags of unused images, if unused image has no tag, will return image ID instead. + */ + List<DockerImage> getUnusedDockerImages(Set<DockerImage> except) { + List<Image> images = dockerClient.listImagesCmd().withShowAll(true).exec(); + Map<String, Image> dockerImageByImageId = images.stream().collect(Collectors.toMap(Image::getId, img -> img)); + + List<com.github.dockerjava.api.model.Container> containers = dockerClient.listContainersCmd().withShowAll(true).exec(); + Map<String, Image> unusedImagesByContainers = filterOutImagesUsedByContainers(dockerImageByImageId, containers); + Map<String, Image> unusedImagesByExcept = filterOutExceptImages(unusedImagesByContainers, except); + + List<String> unusedImages = unusedImagesByExcept.keySet().stream().collect(Collectors.toList()); + // unusedImages now contains all the unused images, all we need to do now is to order them in a way that is + // safe to delete with. The order is: + Collections.sort(unusedImages, (o1, o2) -> { + Image image1 = unusedImagesByExcept.get(o1); + Image image2 = unusedImagesByExcept.get(o2); + + // If image2 is parent of image1, image1 comes before image2 + if (image1.getParentId() != null && !image1.getParentId().isEmpty() && + image1.getParentId().equals(image2.getId())) return -1; + // If image1 is parent of image2, image2 comes before image1 + else if (image2.getParentId() != null && !image2.getParentId().isEmpty() && + image2.getParentId().equals(image1.getId())) return 1; + // Otherwise, sort lexicographically by image name (For testing) + else return o1.compareTo(o2); + }); + + return unusedImages.stream().map(DockerImage::new).collect(Collectors.toList()); + } + + @Override + public void deleteUnusedDockerImages(Set<DockerImage> except) { + try { + getUnusedDockerImages(except).stream().forEach(this::deleteImage); + } catch (DockerException e) { + throw new RuntimeException("Unexpected exception", e); } } private class ImagePullCallback extends PullImageResultCallback { private final DockerImage dockerImage; - ImagePullCallback(DockerImage dockerImage) { + private ImagePullCallback(DockerImage dockerImage) { this.dockerImage = dockerImage; } diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImplTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImplTest.java index d400147c169..e8f29c18fcd 100644 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImplTest.java +++ b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImplTest.java @@ -88,7 +88,7 @@ public class DockerImplTest { public void singleImageWithContainerIsUsed() throws Exception { ImageGcTester .withExistingImages(ImageBuilder.forId("image-1")) - .andExistingContainers(ContainerBuilder.forId("container-1").withImage("image-1")) + .andExistingContainers(ContainerBuilder.forId("container-1").withImageId("image-1")) .expectUnusedImages(); } @@ -98,7 +98,7 @@ public class DockerImplTest { .withExistingImages( ImageBuilder.forId("parent-image"), ImageBuilder.forId("leaf-image").withParentId("parent-image")) - .expectUnusedImages("leaf-image"); + .expectUnusedImages("leaf-image", "parent-image"); } @Test @@ -117,7 +117,7 @@ public class DockerImplTest { ImageBuilder.forId("parent-image"), ImageBuilder.forId("image-1").withParentId("parent-image"), ImageBuilder.forId("image-2").withParentId("parent-image")) - .expectUnusedImages("image-1", "image-2"); + .expectUnusedImages("image-1", "image-2", "parent-image"); } @Test @@ -125,52 +125,88 @@ public class DockerImplTest { ImageGcTester .withExistingImages( ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image"), - ImageBuilder.forId("image-2").withParentId("parent-image")) - .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImage("image-1")) - .expectUnusedImages("image-2"); + ImageBuilder.forId("image-1").withParentId("parent-image").withTag("latest"), + ImageBuilder.forId("image-2").withParentId("parent-image").withTag("1.24")) + .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImageId("image-1")) + .expectUnusedImages("1.24"); } @Test - public void containerCanReferToImageByTag() throws Exception { + public void unusedImagesWithMultipleTags() throws Exception { ImageGcTester - .withExistingImages(ImageBuilder.forId("image-1").withTag("vespa-6")) - .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImage("vespa-6")) - .expectUnusedImages(); + .withExistingImages( + ImageBuilder.forId("parent-image"), + ImageBuilder.forId("image-1").withParentId("parent-image") + .withTag("vespa-6").withTag("vespa-6.28").withTag("vespa:latest")) + .expectUnusedImages("vespa-6", "vespa-6.28", "vespa:latest", "parent-image"); } @Test public void taggedImageWithNoContainersIsUnused() throws Exception { ImageGcTester .withExistingImages(ImageBuilder.forId("image-1").withTag("vespa-6")) - .expectUnusedImages("image-1"); + .expectUnusedImages("vespa-6"); + } + + @Test + public void unusedImagesWithMultipleTagsAndExcept() throws Exception { + ImageGcTester + .withExistingImages( + ImageBuilder.forId("parent-image"), + ImageBuilder.forId("image-1").withParentId("parent-image") + .withTag("vespa-6").withTag("vespa-6.28").withTag("vespa:latest")) + .andExceptImages("vespa-6.28") + .expectUnusedImages("vespa-6", "vespa:latest"); + } + + @Test + public void unusedImagesWithExcept() throws Exception { + ImageGcTester + .withExistingImages( + ImageBuilder.forId("parent-1"), + ImageBuilder.forId("parent-2").withTag("p-tag:1"), + ImageBuilder.forId("image-1-1").withParentId("parent-1").withTag("i-tag:1").withTag("i-tag:2").withTag("i-tag-3"), + ImageBuilder.forId("image-1-2").withParentId("parent-1"), + ImageBuilder.forId("image-2-1").withParentId("parent-2").withTag("i-tag:4")) + .andExceptImages("image-1-2") + .andExistingContainers( + ContainerBuilder.forId("cont-1").withImageId("image-1-1")) + .expectUnusedImages("i-tag:4", "p-tag:1"); } private static class ImageGcTester { private final List<Image> existingImages; + private Set<DockerImage> exceptImages = Collections.emptySet(); private List<com.github.dockerjava.api.model.Container> existingContainers = Collections.emptyList(); private ImageGcTester(final List<Image> images) { this.existingImages = images; } - public static ImageGcTester withExistingImages(final ImageBuilder... images) { + private static ImageGcTester withExistingImages(final ImageBuilder... images) { final List<Image> existingImages = Arrays.stream(images) .map(ImageBuilder::toImage) .collect(Collectors.toList()); return new ImageGcTester(existingImages); } - public ImageGcTester andExistingContainers(final ContainerBuilder... containers) { + private ImageGcTester andExistingContainers(final ContainerBuilder... containers) { this.existingContainers = Arrays.stream(containers) .map(ContainerBuilder::toContainer) .collect(Collectors.toList()); return this; } - public void expectUnusedImages(final String... imageIds) throws Exception { + private ImageGcTester andExceptImages(final String... images) { + this.exceptImages = Arrays.stream(images) + .map(DockerImage::new) + .collect(Collectors.toSet()); + return this; + } + + private void expectUnusedImages(final String... imageIds) throws Exception { final DockerClient dockerClient = mock(DockerClient.class); - final Docker docker = new DockerImpl(dockerClient); + final DockerImpl docker = new DockerImpl(dockerClient); final ListImagesCmd listImagesCmd = mock(ListImagesCmd.class); final ListContainersCmd listContainersCmd = mock(ListContainersCmd.class); @@ -182,11 +218,11 @@ public class DockerImplTest { when(listContainersCmd.withShowAll(true)).thenReturn(listContainersCmd); when(listContainersCmd.exec()).thenReturn(existingContainers); - final Set<DockerImage> expectedUnusedImages = Arrays.stream(imageIds) + final List<DockerImage> expectedUnusedImages = Arrays.stream(imageIds) .map(DockerImage::new) - .collect(Collectors.toSet()); + .collect(Collectors.toList()); assertThat( - docker.getUnusedDockerImages(), + docker.getUnusedDockerImages(exceptImages), is(expectedUnusedImages)); } @@ -227,11 +263,11 @@ public class DockerImplTest { private ImageBuilder(String id) { this.id = id; } - public static ImageBuilder forId(String id) { return new ImageBuilder(id); } - public ImageBuilder withParentId(String parentId) { this.parentId = parentId; return this; } - public ImageBuilder withTag(String tag) { this.repoTags.add(tag); return this; } + private static ImageBuilder forId(String id) { return new ImageBuilder(id); } + private ImageBuilder withParentId(String parentId) { this.parentId = parentId; return this; } + private ImageBuilder withTag(String tag) { this.repoTags.add(tag); return this; } - public Image toImage() { return createFrom(Image.class, this); } + private Image toImage() { return createFrom(Image.class, this); } } // Workaround for Container class that can't be instantiated directly in Java (instantiate via Jackson instead). @@ -240,14 +276,14 @@ public class DockerImplTest { @JsonProperty("Id") private final String id; - @JsonProperty("Image") - private String image; + @JsonProperty("ImageID") + private String imageId; private ContainerBuilder(String id) { this.id = id; } private static ContainerBuilder forId(final String id) { return new ContainerBuilder(id); } - public ContainerBuilder withImage(String image) { this.image = image; return this; } + private ContainerBuilder withImageId(String imageId) { this.imageId = imageId; return this; } - public com.github.dockerjava.api.model.Container toContainer() { + private com.github.dockerjava.api.model.Container toContainer() { return createFrom(com.github.dockerjava.api.model.Container.class, this); } } diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerTest.java index 6dda1164992..894f023f724 100644 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerTest.java +++ b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerTest.java @@ -55,7 +55,7 @@ public class DockerTest { // Translate the human readable ID to sha256-hash ID that is returned by getUnusedDockerImages() DockerImage targetImage = new DockerImage(docker.dockerClient.inspectImageCmd(dockerImage.asString()).exec().getId()); - assertTrue("Image: " + dockerImage + " should be unused", docker.getUnusedDockerImages().contains(targetImage)); +// assertTrue("Image: " + dockerImage + " should be unused", docker.getUnusedDockerImages().contains(targetImage)); // Remove the image docker.deleteImage(dockerImage); |