diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java index 24ed54dcf..ddc23dca8 100644 --- a/src/main/java/com/github/dockerjava/api/DockerClient.java +++ b/src/main/java/com/github/dockerjava/api/DockerClient.java @@ -42,6 +42,7 @@ import com.github.dockerjava.api.command.LogSwarmObjectCmd; import com.github.dockerjava.api.command.PauseContainerCmd; import com.github.dockerjava.api.command.PingCmd; +import com.github.dockerjava.api.command.PruneCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PushImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; @@ -68,6 +69,7 @@ import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.Identifier; +import com.github.dockerjava.api.model.PruneType; import com.github.dockerjava.api.model.ServiceSpec; import com.github.dockerjava.api.model.SwarmSpec; import com.github.dockerjava.core.RemoteApiVersion; @@ -391,6 +393,13 @@ public interface DockerClient extends Closeable { */ LogSwarmObjectCmd logTaskCmd(String taskId); + /** + * Command to delete unused containers/images/networks/volumes + * + * @since {@link RemoteApiVersion#VERSION_1_25} + */ + PruneCmd pruneCmd(PruneType pruneType); + @Override void close() throws IOException; diff --git a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java index 3392ce372..f9c142255 100644 --- a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java @@ -207,6 +207,13 @@ public interface DockerCmdExecFactory extends Closeable { */ ListTasksCmd.Exec listTasksCmdExec(); + /** + * Delete unused content (containers, images, volumes, networks, build relicts) + * + * @since {@link RemoteApiVersion#VERSION_1_25} + */ + PruneCmd.Exec pruneCmdExec(); + @Override void close() throws IOException; diff --git a/src/main/java/com/github/dockerjava/api/command/PruneCmd.java b/src/main/java/com/github/dockerjava/api/command/PruneCmd.java new file mode 100644 index 000000000..7a86434aa --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/PruneCmd.java @@ -0,0 +1,54 @@ +package com.github.dockerjava.api.command; + +import com.github.dockerjava.api.model.PruneResponse; +import com.github.dockerjava.api.model.PruneType; +import com.github.dockerjava.core.RemoteApiVersion; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; + +/** + * Delete unused content (containers, images, volumes, networks, build relicts) + * + * @since {@link RemoteApiVersion#VERSION_1_25} + */ +public interface PruneCmd extends SyncDockerCmd { + + @Nonnull + PruneType getPruneType(); + + @Nonnull + String getApiPath(); + + @CheckForNull + Map> getFilters(); + + PruneCmd withPruneType(final PruneType pruneType); + /** + * Prune containers created before this timestamp + * Meaningful only for CONTAINERS and IMAGES prune type + * @param until Can be Unix timestamps, date formatted timestamps, + * or Go duration strings (e.g. 10m, 1h30m) computed relative to the daemon machine’s time. + */ + PruneCmd withUntilFilter(String until); + + /** + * When set to true, prune only unused and untagged images. When set to false, all unused images are pruned. + * Meaningful only for IMAGES prune type + */ + PruneCmd withDangling(Boolean dangling); + + /** + * Prune containers with the specified labels + */ + PruneCmd withLabelFilter(String... label); + + @Override + PruneResponse exec(); + + interface Exec extends DockerCmdSyncExec { + } + +} diff --git a/src/main/java/com/github/dockerjava/api/model/PruneResponse.java b/src/main/java/com/github/dockerjava/api/model/PruneResponse.java new file mode 100644 index 000000000..d6653cdc6 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/PruneResponse.java @@ -0,0 +1,65 @@ +package com.github.dockerjava.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +import java.io.Serializable; + +/** + * Delete unused content (containers, images, volumes, networks, build relicts) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PruneResponse implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("SpaceReclaimed") + private Long spaceReclaimed; + + /** + * Default constructor for the deserialization. + */ + public PruneResponse() { + } + + /** + * Constructor. + * + * @param spaceReclaimed Space reclaimed after purification + */ + public PruneResponse(Long spaceReclaimed) { + this.spaceReclaimed = spaceReclaimed; + } + + /** + * Disk space reclaimed in bytes + */ + public Long getSpaceReclaimed() { + return spaceReclaimed; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PruneResponse {"); + sb.append(" spaceReclaimed: ").append(getSpaceReclaimed()); + sb.append("}"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Ulimit) { + Ulimit other = (Ulimit) o; + return new EqualsBuilder().append(spaceReclaimed, other.getName()).isEquals(); + } else { + return super.equals(o); + } + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(spaceReclaimed).toHashCode(); + } +} diff --git a/src/main/java/com/github/dockerjava/api/model/PruneType.java b/src/main/java/com/github/dockerjava/api/model/PruneType.java new file mode 100644 index 000000000..10d8704fd --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/PruneType.java @@ -0,0 +1,9 @@ +package com.github.dockerjava.api.model; + +public enum PruneType { + BUILD, + CONTAINERS, + IMAGES, + NETWORKS, + VOLUMES +} diff --git a/src/main/java/com/github/dockerjava/core/AbstractDockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/core/AbstractDockerCmdExecFactory.java index 91347de2b..c822b4cc2 100644 --- a/src/main/java/com/github/dockerjava/core/AbstractDockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/core/AbstractDockerCmdExecFactory.java @@ -44,6 +44,7 @@ import com.github.dockerjava.api.command.LogSwarmObjectCmd; import com.github.dockerjava.api.command.PauseContainerCmd; import com.github.dockerjava.api.command.PingCmd; +import com.github.dockerjava.api.command.PruneCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PushImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; @@ -110,6 +111,7 @@ import com.github.dockerjava.core.exec.LogContainerCmdExec; import com.github.dockerjava.core.exec.PauseContainerCmdExec; import com.github.dockerjava.core.exec.PingCmdExec; +import com.github.dockerjava.core.exec.PruneCmdExec; import com.github.dockerjava.core.exec.PullImageCmdExec; import com.github.dockerjava.core.exec.PushImageCmdExec; import com.github.dockerjava.core.exec.RemoveContainerCmdExec; @@ -492,5 +494,10 @@ public LogSwarmObjectCmd.Exec logSwarmObjectExec(String endpoint) { return new LogSwarmObjectExec(getBaseResource(), getDockerClientConfig(), endpoint); } + @Override + public PruneCmd.Exec pruneCmdExec() { + return new PruneCmdExec(getBaseResource(), getDockerClientConfig()); + } + protected abstract WebTarget getBaseResource(); } diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java index edc634cb9..edf3bfc29 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java @@ -44,6 +44,7 @@ import com.github.dockerjava.api.command.LogSwarmObjectCmd; import com.github.dockerjava.api.command.PauseContainerCmd; import com.github.dockerjava.api.command.PingCmd; +import com.github.dockerjava.api.command.PruneCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PushImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; @@ -69,6 +70,7 @@ import com.github.dockerjava.api.command.WaitContainerCmd; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.Identifier; +import com.github.dockerjava.api.model.PruneType; import com.github.dockerjava.api.model.ServiceSpec; import com.github.dockerjava.api.model.SwarmSpec; import com.github.dockerjava.core.command.AttachContainerCmdImpl; @@ -113,6 +115,7 @@ import com.github.dockerjava.core.command.LogSwarmObjectImpl; import com.github.dockerjava.core.command.PauseContainerCmdImpl; import com.github.dockerjava.core.command.PingCmdImpl; +import com.github.dockerjava.core.command.PruneCmdImpl; import com.github.dockerjava.core.command.PullImageCmdImpl; import com.github.dockerjava.core.command.PushImageCmdImpl; import com.github.dockerjava.core.command.RemoveContainerCmdImpl; @@ -579,6 +582,11 @@ public LogSwarmObjectCmd logTaskCmd(String taskId) { return new LogSwarmObjectImpl(getDockerCmdExecFactory().logSwarmObjectExec("tasks"), taskId); } + @Override + public PruneCmd pruneCmd(PruneType pruneType) { + return new PruneCmdImpl(getDockerCmdExecFactory().pruneCmdExec(), pruneType); + } + @Override public ListTasksCmd listTasksCmd() { return new ListTasksCmdImpl(getDockerCmdExecFactory().listTasksCmdExec()); diff --git a/src/main/java/com/github/dockerjava/core/command/PruneCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/PruneCmdImpl.java new file mode 100644 index 000000000..a191099ed --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/command/PruneCmdImpl.java @@ -0,0 +1,102 @@ +package com.github.dockerjava.core.command; + +import com.github.dockerjava.api.command.PruneCmd; +import com.github.dockerjava.api.model.PruneResponse; +import com.github.dockerjava.api.model.PruneType; +import com.github.dockerjava.core.util.FiltersBuilder; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Delete unused content (containers, images, volumes, networks, build relicts) + */ +public class PruneCmdImpl extends AbstrDockerCmd implements PruneCmd { + + private static final String BUILD_API_PATH = "/build/prune"; + private static final String CONTAINERS_API_PATH = "/containers/prune"; + private static final String IMAGES_API_PATH = "/images/prune"; + private static final String VOLUMES_API_PATH = "/volumes/prune"; + private static final String NETWORKS_API_PATH = "/networks/prune"; + + private FiltersBuilder filters = new FiltersBuilder(); + private PruneType pruneType; + + public PruneCmdImpl(Exec exec, PruneType pruneType) { + super(exec); + this.pruneType = pruneType; + } + + @Nonnull + @Override + public PruneType getPruneType() { + return pruneType; + } + + @Nonnull + @Override + public String getApiPath() { + String apiPath; + switch (getPruneType()) { + case BUILD: + apiPath = BUILD_API_PATH; + break; + case IMAGES: + apiPath = IMAGES_API_PATH; + break; + case NETWORKS: + apiPath = NETWORKS_API_PATH; + break; + case VOLUMES: + apiPath = VOLUMES_API_PATH; + break; + default: + apiPath = CONTAINERS_API_PATH; + break; + } + return apiPath; + } + + @CheckForNull + @Override + public Map> getFilters() { + return filters.build(); + } + + @Override + public PruneCmd withPruneType(final PruneType pruneType) { + checkNotNull(pruneType, "pruneType has not been specified"); + this.pruneType = pruneType; + return this; + } + + @Override + public PruneCmd withDangling(Boolean dangling) { + checkNotNull(dangling, "dangling has not been specified"); + filters.withFilter("dangling", dangling ? "1" : "0"); + return this; + } + + @Override + public PruneCmd withUntilFilter(final String until) { + checkNotNull(until, "until has not been specified"); + filters.withUntil(until); + return this; + } + + @Override + public PruneCmd withLabelFilter(final String... labels) { + checkNotNull(labels, "labels have not been specified"); + filters.withLabels(labels); + return this; + } + + @Override + public PruneResponse exec() { + return super.exec(); + } +} diff --git a/src/main/java/com/github/dockerjava/core/exec/PruneCmdExec.java b/src/main/java/com/github/dockerjava/core/exec/PruneCmdExec.java new file mode 100644 index 000000000..0b8832bcb --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/exec/PruneCmdExec.java @@ -0,0 +1,40 @@ +package com.github.dockerjava.core.exec; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.github.dockerjava.api.command.PruneCmd; +import com.github.dockerjava.api.model.PruneResponse; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.MediaType; +import com.github.dockerjava.core.WebTarget; +import com.github.dockerjava.core.util.FiltersEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PruneCmdExec extends AbstrSyncDockerCmdExec implements PruneCmd.Exec { + + private static final Logger LOGGER = LoggerFactory.getLogger(PruneCmdExec.class); + + + public PruneCmdExec(WebTarget baseResource, DockerClientConfig dockerClientConfig) { + super(baseResource, dockerClientConfig); + } + + @Override + protected PruneResponse execute(PruneCmd command) { + WebTarget webTarget = getBaseResource().path(command.getApiPath()); + + if (command.getFilters() != null && !command.getFilters().isEmpty()) { + webTarget = webTarget.queryParam("filters", FiltersEncoder.jsonEncode(command.getFilters())); + } + + LOGGER.trace("POST: {}", webTarget); + + PruneResponse response = webTarget.request().accept(MediaType.APPLICATION_JSON) + .post(null, new TypeReference() { }); + + LOGGER.trace("Response: {}", response); + + return response; + } + +} diff --git a/src/main/java/com/github/dockerjava/core/util/FiltersBuilder.java b/src/main/java/com/github/dockerjava/core/util/FiltersBuilder.java index 0c414f983..d942e59e0 100644 --- a/src/main/java/com/github/dockerjava/core/util/FiltersBuilder.java +++ b/src/main/java/com/github/dockerjava/core/util/FiltersBuilder.java @@ -1,5 +1,7 @@ package com.github.dockerjava.core.util; +import com.google.common.base.Strings; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -7,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.regex.Pattern; /** * Representation of Docker filters. @@ -16,6 +19,14 @@ */ public class FiltersBuilder { + private static final Pattern UNTIL_TIMESTAMP_PATTERN = + Pattern.compile("^\\d{1,10}$"); + private static final Pattern UNTIL_DATETIME_PATTERN = + Pattern.compile("^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])([Tt]([01][0-9]|2[0-3]):([0-5][0-9]):" + + "([0-5][0-9]|60)(\\.[0-9]+)?)?(([Zz])|([\\+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))?$"); + private static final Pattern UNTIL_GO_PATTERN = + Pattern.compile("^([1-9][0-9]*h)?([1-9][0-9]*m)?([1-9][0-9]*s)?$"); + private Map> filters = new HashMap>(); public FiltersBuilder() { @@ -75,6 +86,14 @@ public FiltersBuilder withLabels(Map labels) { return this; } + public FiltersBuilder withUntil(String until) throws NumberFormatException { + if (!isValidUntil(until)) { + throw new NumberFormatException("Not valid format of 'until': " + until); + } + + return withFilter("until", until); + } + private static List labelsMapToList(Map labels) { List result = new ArrayList(); for (Entry entry : labels.entrySet()) { @@ -87,6 +106,18 @@ private static List labelsMapToList(Map labels) { return result; } + private boolean isValidUntil(String until) { + if (UNTIL_DATETIME_PATTERN.matcher(until).matches()) { + return true; + } else if (!Strings.isNullOrEmpty(until) && UNTIL_GO_PATTERN.matcher(until).matches()) { + return true; + } else if (UNTIL_TIMESTAMP_PATTERN.matcher(until).matches()) { + return true; + } + + return false; + } + // CHECKSTYLE:OFF @Override public boolean equals(Object o) { diff --git a/src/main/java/com/github/dockerjava/jaxrs/JerseyDockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/jaxrs/JerseyDockerCmdExecFactory.java index 5712163d1..1582d5313 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/JerseyDockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/jaxrs/JerseyDockerCmdExecFactory.java @@ -47,6 +47,7 @@ import com.github.dockerjava.api.command.LogSwarmObjectCmd; import com.github.dockerjava.api.command.PauseContainerCmd; import com.github.dockerjava.api.command.PingCmd; +import com.github.dockerjava.api.command.PruneCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PushImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; @@ -658,6 +659,11 @@ public ListTasksCmd.Exec listTasksCmdExec() { return new ListTasksCmdExec(getBaseResource(), getDockerClientConfig()); } + @Override + public PruneCmd.Exec pruneCmdExec() { + return new PruneCmdExec(getBaseResource(), getDockerClientConfig()); + } + @Override public void close() throws IOException { checkNotNull(client, "Factory not initialized. You probably forgot to call init()!"); diff --git a/src/main/java/com/github/dockerjava/jaxrs/PruneCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/PruneCmdExec.java new file mode 100644 index 000000000..324bc8201 --- /dev/null +++ b/src/main/java/com/github/dockerjava/jaxrs/PruneCmdExec.java @@ -0,0 +1,41 @@ +package com.github.dockerjava.jaxrs; + +import com.github.dockerjava.api.command.PruneCmd; +import com.github.dockerjava.api.model.PruneResponse; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.util.FiltersEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; + +import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper; + +public class PruneCmdExec extends AbstrSyncDockerCmdExec implements PruneCmd.Exec { + + private static final Logger LOGGER = LoggerFactory.getLogger(PruneCmdExec.class); + + public PruneCmdExec(WebTarget baseResource, DockerClientConfig dockerClientConfig) { + super(baseResource, dockerClientConfig); + } + + @Override + protected PruneResponse execute(PruneCmd command) { + WebTarget webTarget = getBaseResource().path(command.getApiPath()); + + if (command.getFilters() != null && !command.getFilters().isEmpty()) { + webTarget = webTarget.queryParam("filters", urlPathSegmentEscaper().escape(FiltersEncoder.jsonEncode(command.getFilters()))); + } + + LOGGER.trace("POST: {}", webTarget); + + PruneResponse response = webTarget.request().accept(MediaType.APPLICATION_JSON) + .post(null, new GenericType() { }); + LOGGER.trace("Response: {}", response); + + return response; + } + +} diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java index e2986ef69..a3c96e54d 100644 --- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java +++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java @@ -49,6 +49,7 @@ import com.github.dockerjava.api.command.LogSwarmObjectCmd; import com.github.dockerjava.api.command.PauseContainerCmd; import com.github.dockerjava.api.command.PingCmd; +import com.github.dockerjava.api.command.PruneCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.PushImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; @@ -526,6 +527,11 @@ public ListTasksCmd.Exec listTasksCmdExec() { return delegate.listTasksCmdExec(); } + @Override + public PruneCmd.Exec pruneCmdExec() { + return delegate.pruneCmdExec(); + } + public List getContainerNames() { return new ArrayList(containerNames); }