55import com .github .dockerjava .api .command .InspectContainerResponse ;
66import com .github .dockerjava .api .exception .NotFoundException ;
77import com .github .dockerjava .api .model .Bind ;
8+ import com .github .dockerjava .api .model .Container ;
89import com .github .dockerjava .api .model .ExposedPort ;
910import com .github .dockerjava .api .model .Frame ;
1011import com .github .dockerjava .api .model .HostConfig ;
1112import com .github .dockerjava .api .model .Network ;
1213import com .github .dockerjava .api .model .PortBinding ;
1314import com .github .dockerjava .api .model .Ports ;
15+ import com .github .dockerjava .api .model .PruneType ;
1416import com .github .dockerjava .api .model .Volume ;
1517import com .google .common .annotations .VisibleForTesting ;
1618import com .google .common .base .Throwables ;
3638import java .nio .charset .StandardCharsets ;
3739import java .util .AbstractMap .SimpleEntry ;
3840import java .util .ArrayList ;
41+ import java .util .Arrays ;
3942import java .util .Collections ;
4043import java .util .List ;
4144import java .util .Map ;
@@ -58,24 +61,28 @@ public final class ResourceReaper {
5861
5962 private static final Logger LOGGER = LoggerFactory .getLogger (ResourceReaper .class );
6063
61- private static final List <List <Map .Entry <String , String >>> DEATH_NOTE = new ArrayList <>();
64+ private static final List <List <Map .Entry <String , String >>> DEATH_NOTE = new ArrayList <>(
65+ Arrays .asList (
66+ DockerClientFactory .DEFAULT_LABELS .entrySet ().stream ()
67+ .<Map .Entry <String , String >>map (it -> new SimpleEntry <>("label" , it .getKey () + "=" + it .getValue ()))
68+ .collect (Collectors .toList ())
69+ )
70+ );
71+
6272 private static final RateLimiter RYUK_ACK_RATE_LIMITER = RateLimiterBuilder
6373 .newBuilder ()
6474 .withRate (4 , TimeUnit .SECONDS )
6575 .withConstantThroughput ()
6676 .build ();
6777
6878 private static ResourceReaper instance ;
69- private final DockerClient dockerClient ;
79+ private static AtomicBoolean ryukStarted = new AtomicBoolean (false );
80+ private final DockerClient dockerClient = DockerClientFactory .lazyClient ();
7081 private Map <String , String > registeredContainers = new ConcurrentHashMap <>();
7182 private Set <String > registeredNetworks = Sets .newConcurrentHashSet ();
7283 private Set <String > registeredImages = Sets .newConcurrentHashSet ();
7384 private AtomicBoolean hookIsSet = new AtomicBoolean (false );
7485
75- private ResourceReaper () {
76- dockerClient = DockerClientFactory .instance ().client ();
77- }
78-
7986
8087 /**
8188 *
@@ -173,14 +180,6 @@ public InspectContainerResponse getContainerInfo() {
173180
174181 CountDownLatch ryukScheduledLatch = new CountDownLatch (1 );
175182
176- synchronized (DEATH_NOTE ) {
177- DEATH_NOTE .add (
178- DockerClientFactory .DEFAULT_LABELS .entrySet ().stream ()
179- .<Map .Entry <String , String >>map (it -> new SimpleEntry <>("label" , it .getKey () + "=" + it .getValue ()))
180- .collect (Collectors .toList ())
181- );
182- }
183-
184183 String host = containerState .getHost ();
185184 Integer ryukPort = containerState .getFirstMappedPort ();
186185 Thread kiraThread = new Thread (
@@ -238,6 +237,7 @@ public InspectContainerResponse getContainerInfo() {
238237 }
239238 }
240239
240+ ryukStarted .set (true );
241241 return ryukContainerId ;
242242 }
243243
@@ -253,7 +253,7 @@ public synchronized static ResourceReaper instance() {
253253 * Perform a cleanup.
254254 */
255255 public synchronized void performCleanup () {
256- registeredContainers .forEach (this ::stopContainer );
256+ registeredContainers .forEach (this ::removeContainer );
257257 registeredNetworks .forEach (this ::removeNetwork );
258258 registeredImages .forEach (this ::removeImage );
259259 }
@@ -262,14 +262,29 @@ public synchronized void performCleanup() {
262262 * Register a filter to be cleaned up.
263263 *
264264 * @param filter the filter
265+ * @deprecated only label filter is supported by the prune API, use {@link #registerLabelsFilterForCleanup(Map)}
265266 */
267+ @ Deprecated
266268 public void registerFilterForCleanup (List <Map .Entry <String , String >> filter ) {
267269 synchronized (DEATH_NOTE ) {
268270 DEATH_NOTE .add (filter );
269271 DEATH_NOTE .notifyAll ();
270272 }
271273 }
272274
275+ /**
276+ * Register a label to be cleaned up.
277+ *
278+ * @param labels the filter
279+ */
280+ public void registerLabelsFilterForCleanup (Map <String , String > labels ) {
281+ registerFilterForCleanup (
282+ labels .entrySet ().stream ()
283+ .map (it -> new SimpleEntry <>("label" , it .getKey () + "=" + it .getValue ()))
284+ .collect (Collectors .toList ())
285+ );
286+ }
287+
273288 /**
274289 * Register a container to be cleaned up, either on explicit call to stopAndRemoveContainer, or at JVM shutdown.
275290 *
@@ -287,7 +302,7 @@ public void registerContainerForCleanup(String containerId, String imageName) {
287302 * @param containerId the ID of the container
288303 */
289304 public void stopAndRemoveContainer (String containerId ) {
290- stopContainer (containerId , registeredContainers .get (containerId ));
305+ removeContainer (containerId , registeredContainers .get (containerId ));
291306
292307 registeredContainers .remove (containerId );
293308 }
@@ -299,12 +314,12 @@ public void stopAndRemoveContainer(String containerId) {
299314 * @param imageName the image name of the container (used for logging)
300315 */
301316 public void stopAndRemoveContainer (String containerId , String imageName ) {
302- stopContainer (containerId , imageName );
317+ removeContainer (containerId , imageName );
303318
304319 registeredContainers .remove (containerId );
305320 }
306321
307- private void stopContainer (String containerId , String imageName ) {
322+ private void removeContainer (String containerId , String imageName ) {
308323 boolean running ;
309324 try {
310325 InspectContainerResponse containerInfo = dockerClient .inspectContainerCmd (containerId ).exec ();
@@ -444,10 +459,52 @@ private void removeImage(String dockerImageName) {
444459 }
445460 }
446461
447- private void setHook () {
462+ private void prune (PruneType pruneType , List <Map .Entry <String , String >> filters ) {
463+ String [] labels = filters .stream ()
464+ .filter (it -> "label" .equals (it .getKey ()))
465+ .map (Map .Entry ::getValue )
466+ .toArray (String []::new );
467+ switch (pruneType ) {
468+ // Docker only prunes stopped containers, so we have to do it manually
469+ case CONTAINERS :
470+ List <Container > containers = dockerClient .listContainersCmd ()
471+ .withFilter ("label" , Arrays .asList (labels ))
472+ .withShowAll (true )
473+ .exec ();
474+
475+ containers .parallelStream ().forEach (container -> {
476+ removeContainer (container .getId (), container .getImage ());
477+ });
478+ break ;
479+ default :
480+ dockerClient .pruneCmd (pruneType ).withLabelFilter (labels ).exec ();
481+ break ;
482+ }
483+ }
484+
485+ /**
486+ * @deprecated internal API, not intended for public usage
487+ */
488+ @ Deprecated
489+ public void setHook () {
448490 if (hookIsSet .compareAndSet (false , true )) {
449491 // If the JVM stops without containers being stopped, try and stop the container.
450- Runtime .getRuntime ().addShutdownHook (new Thread (DockerClientFactory .TESTCONTAINERS_THREAD_GROUP , this ::performCleanup ));
492+ Runtime .getRuntime ().addShutdownHook (
493+ new Thread (DockerClientFactory .TESTCONTAINERS_THREAD_GROUP ,
494+ () -> {
495+ performCleanup ();
496+
497+ if (!ryukStarted .get ()) {
498+ synchronized (DEATH_NOTE ) {
499+ DEATH_NOTE .forEach (filters -> prune (PruneType .CONTAINERS , filters ));
500+ DEATH_NOTE .forEach (filters -> prune (PruneType .NETWORKS , filters ));
501+ DEATH_NOTE .forEach (filters -> prune (PruneType .VOLUMES , filters ));
502+ DEATH_NOTE .forEach (filters -> prune (PruneType .IMAGES , filters ));
503+ }
504+ }
505+ }
506+ )
507+ );
451508 }
452509 }
453510
0 commit comments