/******************************************************************************* * Copyright (c) 2014 Red Hat. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Red Hat - Initial Contribution *******************************************************************************/ package org.eclipse.linuxtools.internal.docker.core; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import java.util.stream.Stream; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.linuxtools.docker.core.IDockerConnection; import org.eclipse.linuxtools.docker.core.IDockerImage; public class DockerImage implements IDockerImage, IAdaptable { public enum DockerImageQualifier { TOP_LEVEL, INTERMEDIATE, DANGLING; } /** The 'latest' tag. */ public static final String LATEST_TAG = "latest"; //$NON-NLS-1$ private static final String REGISTRY_HOST = "[a-zA-Z0-9]+([\\._-][a-zA-Z0-9]+)*"; //$NON-NLS-1$ private static final String REGISTRY_PORT = "[0-9]+"; //$NON-NLS-1$ private static final String REPOSITORY = "[a-z0-9]+([\\._-][a-z0-9]+)*"; //$NON-NLS-1$ private static final String NAME = "[a-z0-9]+([\\._-][a-z0-9]+)*"; //$NON-NLS-1$ private static final String TAG = "[a-zA-Z0-9]+([\\._-][a-zA-Z0-9]+)*"; //$NON-NLS-1$ /** the image name pattern. */ public static final Pattern imageNamePattern = Pattern.compile("(" //$NON-NLS-1$ + REGISTRY_HOST + "\\:" + REGISTRY_PORT + "/)?" //$NON-NLS-1$ //$NON-NLS-2$ + "((?<repository>" + REPOSITORY + "(/" + REPOSITORY + ")?)/)?" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "(?<name>" + NAME + ")" //$NON-NLS-1$ //$NON-NLS-2$ + "(\\:(?<tag>" + TAG + "))?"); //$NON-NLS-1$ //$NON-NLS-2$ // SimpleDateFormat is not thread-safe, so give one to each thread private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); //$NON-NLS-1$ } }; private final DockerConnection parent; private final String created; private final String createdDate; private final String id; private final String shortId; private final String parentId; private final List<String> repoTags; private final String repo; private final List<String> tags; private final Long size; private final Long virtualSize; private final DockerImageQualifier imageQualifier; public DockerImage(final DockerConnection parent, @Deprecated final List<String> repoTags, final String repo, final List<String> tags, final String id, final String parentId, final String created, final Long size, final Long virtualSize, final DockerImageQualifier imageQualifier) { this.parent = parent; this.repoTags = repoTags; this.repo = repo; this.tags = tags; this.id = id; this.shortId = getShortId(id); this.parentId = parentId; this.created = created; this.createdDate = created != null ? formatter.get() .format(new Date( Long.valueOf(created).longValue() * 1000)) : null; this.size = size; this.virtualSize = virtualSize; this.imageQualifier = imageQualifier; } @Deprecated public DockerImage(final DockerConnection parent, @Deprecated final List<String> repoTags, final String repo, final List<String> tags, final String id, final String parentId, final String created, final Long size, final Long virtualSize, final boolean intermediateImage, final boolean danglingImage) { this.parent = parent; this.repoTags = repoTags; this.repo = repo; this.tags = tags; this.id = id; this.shortId = getShortId(id); this.parentId = parentId; this.created = created; this.createdDate = created != null ? formatter.get().format( new Date(Long.valueOf(created).longValue() * 1000)) : null; this.size = size; this.virtualSize = virtualSize; this.imageQualifier = intermediateImage ? DockerImageQualifier.INTERMEDIATE : (danglingImage ? DockerImageQualifier.DANGLING : DockerImageQualifier.TOP_LEVEL); } /** * @param id * the complete {@link IDockerImage} id * @return the short id, excluding the {@code sha256:} prefix if present. */ private static String getShortId(final String id) { if (id.startsWith("sha256:")) { return getShortId(id.substring("sha256:".length())); } if (id.length() > 12) { return id.substring(0, 12); } else { return id; } } /** * Extracts the org/repo and all the associated tags from the given * {@code repoTags}, assuming that the given repoTags elements have the * following format: {@code [org/]repo[:tag]}. Tags are sorted by their * natural order. * * @param repoTags * the list of repo/tags to analyze * @return the tags indexed by org/repo */ public static Map<String, List<String>> extractTagsByRepo( final List<String> repoTags) { final Map<String, List<String>> results = new HashMap<>(); for (String entry : repoTags) { final int indexOfColonChar = entry.lastIndexOf(':'); final String repo = (indexOfColonChar > -1) ? entry.substring(0, indexOfColonChar) : entry; if (!results.containsKey(repo)) { results.put(repo, new ArrayList<String>()); } if (indexOfColonChar > -1) { results.get(repo).add(entry.substring(indexOfColonChar + 1)); } } // now sort the tags for (Entry<String, List<String>> entry : results.entrySet()) { Collections.sort(entry.getValue()); } return results; } /** * Extracts the list of tags in the given repo/tags, assuming that the given * repoTags elements have the following format: {@code [org/]repo[:tag]}. * * @param repoTags * the repo/tags list to analyze * @return the list of tags or empty list if none was found. */ public static List<String> extractTags(final List<String> repoTags) { if (repoTags.isEmpty()) { return Collections.emptyList(); } final List<String> tags = new ArrayList<>(); for (String repoTag : repoTags) { final int indexOfColonChar = repoTag.lastIndexOf(':'); if (indexOfColonChar == -1) { continue; } final String tag = repoTag.substring(indexOfColonChar + 1); tags.add(tag); } return tags; } /** * Extracts the tag in the given repo/tag, assuming that the given repoTag * element has the following format: {@code [org/]repo[:tag]}. * * @param repoTag * the repo/tag to analyze * @return the tag or null if none was found. */ public static String extractRepo(final String repoTag) { if (repoTag == null) { return null; } final int indexOfColonChar = repoTag.lastIndexOf(':'); if (indexOfColonChar == -1) { return repoTag; } return repoTag.substring(0, indexOfColonChar); } /** * Extracts the tag in the given repo/tag, assuming that the given repoTag * element has the following format: {@code [org/]repo[:tag]} * * @param repoTag * the repo/tag to analyze * @return the tag or <code>null</code> if none was found. */ public static String extractTag(final String repoTag) { if (repoTag == null) { return null; } final int indexOfColonChar = repoTag.lastIndexOf(':'); if (indexOfColonChar == -1) { return null; } return repoTag.substring(indexOfColonChar + 1); } /** * Duplicates the given {@code image} into as many {@link IDockerImage} has * it has distinct repoTags entries * * @param image * the source image * @return a {@link Stream} of duplicate {@link IDockerImage} */ public static Stream<IDockerImage> duplicateImageByRepo( final IDockerImage image) { return DockerImage.extractTagsByRepo(image.repoTags()).entrySet() .stream() .map(entry -> new DockerImage( (DockerConnection) image.getConnection(), image.repoTags(), entry.getKey(), entry.getValue(), image.id(), image.parentId(), image.created(), image.size(), image.virtualSize(), image.isIntermediateImage(), image.isDangling())); } // TODO: expose this in the public API (in favor of // IDockerImage#isIntermediate() and IDockerImage#isDangling() ?) public DockerImageQualifier qualifier() { return this.imageQualifier; } @Override public DockerConnection getConnection() { return parent; } @Override public List<String> repoTags() { return repoTags; } @Override public String repo() { return repo; } @Override public List<String> tags() { return tags; } @Override public String id() { return this.id; } /** * @return the short image id, ie, the first 12 figures, excluding the * <code>sha256:</code> prefix. */ // TODO: add to the API in version 3.0.0 public String shortId() { return this.shortId; } @Override public String parentId() { return parentId; } @Override public String created() { return created; } @Override public String createdDate() { return createdDate; } @Override public Long size() { return size; } @Override public Long virtualSize() { return virtualSize; } @Override public boolean isDangling() { return this.imageQualifier == DockerImageQualifier.DANGLING; } @Override public boolean isIntermediateImage() { return this.imageQualifier == DockerImageQualifier.INTERMEDIATE; } @Override public String toString() { return "Image: id=" + id() + "\n parentId=" + parentId() + "\n created=" + created() + "\n repo=" + repo() + "\n tags=" + tags() + "\n size=" + size() + "\n virtualSize=" + virtualSize(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((repo == null) ? 0 : repo.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final DockerImage other = (DockerImage) obj; if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (repo == null) { if (other.repo != null) { return false; } } else if (!repo.equals(other.repo)) { return false; } return true; } /** * Appends the <code>latest</code> tag to the given {@code imageName} * * @param imageName * the image name to check * @return the image name with the <code>latest</code> tag if none was set */ public static String setDefaultTagIfMissing(final String imageName) { if (DockerImage.extractTag(imageName) == null) { return imageName + ':' + LATEST_TAG; } return imageName; } @SuppressWarnings("unchecked") @Override public <T> T getAdapter(Class<T> adapter) { if (adapter.equals(IDockerConnection.class)) return (T) this.parent; return null; } }