diff --git a/doc/changelog.md b/doc/changelog.md index 4ff171fc1..7ac8f03c0 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -4,6 +4,7 @@ - Restore ANSI color to Maven logging if disabled during plugin execution and enable color for Windows with Maven 3.5.0 or later. Color logging is enabled by default, but disabled if the Maven CLI disables color (e.g. in batch mode) ([#1108](https://github.com/fabric8io/docker-maven-plugin/issues/1108)) - Fix NPE if docker:save is called with -Dfile=file-name-only.tar ([#1203](https://github.com/fabric8io/docker-maven-plugin/issues/1203)) - Improve GZIP compression performance for docker:save ([#1205](https://github.com/fabric8io/docker-maven-plugin/issues/1205)) + - Use pattern to detect image name in archive loaded during build and tag with image name from the project configuration ([#1207](https://github.com/fabric8io/docker-maven-plugin/issues/1207)) * **0.29.0** (2019-04-08) - Avoid failing docker:save when no images with build configuration are present ([#1185](https://github.com/fabric8io/docker-maven-plugin/issues/1185)) diff --git a/src/main/asciidoc/inc/build/_configuration.adoc b/src/main/asciidoc/inc/build/_configuration.adoc index f28481fa3..4af6c610a 100644 --- a/src/main/asciidoc/inc/build/_configuration.adoc +++ b/src/main/asciidoc/inc/build/_configuration.adoc @@ -77,6 +77,18 @@ A provided `` takes precedence over the name given here. This tag is usefu | *imagePullPolicy* | Specific pull policy for the base image. This overwrites any global pull policy. See the globale configuration option <> for the possible values and the default. +| *loadNamePattern* +a| Scan the images in the archive specified in `dockerArchive` and match the associated repository and tag information against this pattern. When a matching repository and tag is found, create a tag linking the `name` for this image to the repository and tag that matched the pattern. + +The wildcards are: + +* `?` matches a single character +* `*` matches within one component, where components are separated by slashes, or the final colon that separates the repository from the tag +* `**` matches multiple components, stopping at the final colon +* `**/` matches multiple components, but must stop at a slash, or the final colon + +When matching multiple components, `$$**/$$` is likely to be more useful than `$$**$$`. The pattern `$$**image-name:*$$` will match `my-group/my-image-name:some-tag`, whereas `$$**/image-name:*$$` will not, because the wildcard has to stop at a slash. Note that `$$**/image-name:*$$` will also match 'image-name:some-tag', since the `$$**/$$` wildcard can be empty. + | <> | Labels as described in <>. diff --git a/src/main/asciidoc/inc/build/_overview.adoc b/src/main/asciidoc/inc/build/_overview.adoc index fd9839d13..b0059266f 100644 --- a/src/main/asciidoc/inc/build/_overview.adoc +++ b/src/main/asciidoc/inc/build/_overview.adoc @@ -13,7 +13,7 @@ Alternatively an external Dockerfile template or Docker archive can be used. Thi * *contextDir* specifies docker build context if an external dockerfile is located outside of Docker build context. If not specified, Dockerfile's parent directory is used as build context. * *dockerFile* specifies a specific Dockerfile path. The Docker build context directory is set to `contextDir` if given. If not the directory by default is the directory in which the Dockerfile is stored. -* *dockerArchive* specifies a previously saved image archive to load directly. Such a tar archive can be created with `docker save`. If a `dockerArchive` is provided, no `dockerFile` or `dockerFileDir` must be given. +* *dockerArchive* specifies a previously saved image archive to load directly. Such a tar archive can be created with `docker save` or the <<{plugin}:save>> goal. If a `dockerArchive` is provided, no `dockerFile` or `dockerFileDir` must be given. * *dockerFileDir* (_deprecated_, use *contextDir*) specifies a directory containing a Dockerfile that will be used to create the image. The name of the Dockerfile is `Dockerfile` by default but can be also set with the option `dockerFile` (see below). All paths can be either absolute or relative paths (except when both `dockerFileDir` and `dockerFile` are provided in which case `dockerFile` must not be absolute). A relative path is looked up in `${project.basedir}/src/main/docker` by default. You can make it easily an absolute path by using `${project.basedir}` in your configuration. diff --git a/src/main/asciidoc/inc/external/_property_configuration.adoc b/src/main/asciidoc/inc/external/_property_configuration.adoc index 042161aa0..5e66b483f 100644 --- a/src/main/asciidoc/inc/external/_property_configuration.adoc +++ b/src/main/asciidoc/inc/external/_property_configuration.adoc @@ -175,6 +175,9 @@ when a `docker.from` or a `docker.fromExt` is set. | *docker.labels.LABEL* | Sets a label which works similarly like setting environment variables. +| *docker.loadNamePattern* +| Search the archive specified in `docker.dockerArchive` for the specified image name and creates a tag from the matched name to the build image name specified in `docker.name`. + | *docker.log.enabled* | Use logging (default: `true`) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 6c8428f3d..f60515126 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -11,7 +11,7 @@ Roland Huß; ifndef::ebook-format[:leveloffset: 1] -(C) 2015 - 2018 The original authors. +(C) 2015 - 2019 The original authors. ifdef::basebackend-html[toc::[]] diff --git a/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java b/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java index f5dfa8ce5..74bdcc97f 100644 --- a/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java +++ b/src/main/java/io/fabric8/maven/docker/access/hc/DockerAccessWithHcClient.java @@ -95,6 +95,9 @@ public class DockerAccessWithHcClient implements DockerAccess { // Minimal API version, independent of any feature used public static final String API_VERSION = "1.18"; + // Copy buffer size when saving images + private static final int COPY_BUFFER_SIZE = 65536; + // Logging private final Logger log; @@ -465,7 +468,7 @@ private ResponseHandler getImageResponseHandler(final String filename, f public Object handleResponse(HttpResponse response) throws IOException { try (InputStream stream = response.getEntity().getContent(); OutputStream out = compression.wrapOutputStream(new FileOutputStream(filename))) { - IOUtils.copy(stream, out, 65536); + IOUtils.copy(stream, out, COPY_BUFFER_SIZE); } return null; } diff --git a/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java b/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java index d31e13ce2..385cc1317 100644 --- a/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java +++ b/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java @@ -81,11 +81,14 @@ public static ArchiveCompression fromFileName(String filename) { return ArchiveCompression.none; } + private static final int GZIP_BUFFER_SIZE = 65536; + // According to https://bugs.openjdk.java.net/browse/JDK-8142920, 3 is a better default + private static final int GZIP_COMPRESSION_LEVEL = 3; + private static class GZIPOutputStream extends java.util.zip.GZIPOutputStream { private GZIPOutputStream(OutputStream out) throws IOException { - super(out, 65536); - // According to https://bugs.openjdk.java.net/browse/JDK-8142920, 3 is a better default - def.setLevel(3); + super(out, GZIP_BUFFER_SIZE); + def.setLevel(GZIP_COMPRESSION_LEVEL); } } } diff --git a/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java index a540beb65..8b69cef7e 100644 --- a/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java @@ -1,10 +1,7 @@ package io.fabric8.maven.docker.config; import java.io.File; -import java.io.IOException; import java.io.Serializable; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.*; import io.fabric8.maven.docker.util.*; @@ -52,6 +49,17 @@ public class BuildImageConfiguration implements Serializable { @Parameter private String dockerArchive; + /** + * Pattern for the image name we expect to find in the dockerArchive. + * + * If set, the archive is scanned prior to sending to Docker and checked to + * ensure a matching name is found linked to one of the images in the archive. + * After loading, the image with the matching name will be tagged with the + * image name configured in this project. + */ + @Parameter + private String loadNamePattern; + /** * How interpolation of a dockerfile should be performed */ @@ -161,6 +169,10 @@ public boolean isDockerFileMode() { return dockerFileFile != null; } + public String getLoadNamePattern() { + return loadNamePattern; + } + public File getContextDir() { return contextDir != null ? new File(contextDir) : getDockerFile().getParentFile(); } @@ -372,6 +384,11 @@ public Builder dockerArchive(String archive) { return this; } + public Builder loadNamePattern(String archiveEntryRepoTagPattern) { + config.loadNamePattern = archiveEntryRepoTagPattern; + return this; + } + public Builder filter(String filter) { config.filter = filter; return this; diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java b/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java index ed731b288..4deeea92a 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/property/ConfigKey.java @@ -77,6 +77,7 @@ public enum ConfigKey { IMAGE_PULL_POLICY_RUN("imagePullPolicy.run"), LABELS(ValueCombinePolicy.Merge), LINKS, + LOAD_NAME_PATTERN, LOG_ENABLED("log.enabled"), LOG_PREFIX("log.prefix"), LOG_DATE("log.date"), diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java index 7cb72d2f6..a7b699d13 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java @@ -156,6 +156,7 @@ private BuildImageConfiguration extractBuildConfiguration(ImageConfiguration fro .imagePullPolicy(valueProvider.getString(IMAGE_PULL_POLICY_BUILD, config == null ? null : config.getImagePullPolicy())) .contextDir(valueProvider.getString(CONTEXT_DIR, config == null ? null : config.getContextDirRaw())) .dockerArchive(valueProvider.getString(DOCKER_ARCHIVE, config == null ? null : config.getDockerArchiveRaw())) + .loadNamePattern(valueProvider.getString(LOAD_NAME_PATTERN, config == null ? null : config.getLoadNamePattern())) .dockerFile(valueProvider.getString(DOCKER_FILE, config == null ? null : config.getDockerFileRaw())) .dockerFileDir(valueProvider.getString(DOCKER_FILE_DIR, config == null ? null : config.getDockerFileDirRaw())) .buildOptions(valueProvider.getMap(BUILD_OPTIONS, config == null ? null : config.getBuildOptions())) diff --git a/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifest.java b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifest.java new file mode 100644 index 000000000..616adc48a --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifest.java @@ -0,0 +1,19 @@ +package io.fabric8.maven.docker.model; + +import java.util.List; + +import com.google.gson.JsonObject; + +public interface ImageArchiveManifest { + /** + * @return the list of images in the archive. + */ + List getEntries(); + + /** + * Return the JSON object for the named config + * @param configName + * @return + */ + JsonObject getConfig(String configName); +} diff --git a/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapter.java b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapter.java new file mode 100644 index 000000000..623d4994d --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapter.java @@ -0,0 +1,43 @@ +package io.fabric8.maven.docker.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class ImageArchiveManifestAdapter implements ImageArchiveManifest { + private List entries; + + private Map config; + + public ImageArchiveManifestAdapter(JsonElement json) { + this.entries = new ArrayList<>(); + + if(json.isJsonArray()) { + for(JsonElement entryJson : json.getAsJsonArray()) { + if(entryJson.isJsonObject()) { + this.entries.add(new ImageArchiveManifestEntryAdapter(entryJson.getAsJsonObject())); + } + } + } + + this.config = new LinkedHashMap<>(); + } + + @Override + public List getEntries() { + return this.entries; + } + + @Override + public JsonObject getConfig(String configName) { + return this.config.get(configName); + } + + public JsonObject putConfig(String configName, JsonObject config) { + return this.config.put(configName, config); + } +} diff --git a/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntry.java b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntry.java new file mode 100644 index 000000000..4ca6ccb19 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntry.java @@ -0,0 +1,28 @@ +package io.fabric8.maven.docker.model; + +import java.util.List; + +/** + * Interface representing an entry in an image archive manifest. + */ +public interface ImageArchiveManifestEntry { + /** + * @return the image id for this manifest entry + */ + String getId(); + + /** + * @return the configuration JSON path for this manifest entry + */ + String getConfig(); + + /** + * @return the repository tags associated with this manifest entry + */ + List getRepoTags(); + + /** + * @return the layer archive paths for this manifest entry + */ + List getLayers(); +} diff --git a/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapter.java b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapter.java new file mode 100644 index 000000000..1155ffbfc --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapter.java @@ -0,0 +1,67 @@ +package io.fabric8.maven.docker.model; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * Adapter to convert from JSON representation to model. + */ +public class ImageArchiveManifestEntryAdapter implements ImageArchiveManifestEntry { + public static final String CONFIG = "Config"; + public static final String REPO_TAGS = "RepoTags"; + public static final String LAYERS = "Layers"; + public static final String CONFIG_JSON_SUFFIX = ".json"; + + private String config; + private List repoTags; + private List layers; + + public ImageArchiveManifestEntryAdapter(JsonObject json) { + JsonElement field; + + if((field = json.get(CONFIG)) != null && field.isJsonPrimitive()) { + this.config = field.getAsString(); + } + + this.repoTags = new ArrayList<>(); + if ((field = json.get(REPO_TAGS)) != null && field.isJsonArray()) { + for(JsonElement item : field.getAsJsonArray()) { + if(item.isJsonPrimitive()) { + this.repoTags.add(item.getAsString()); + } + } + } + + this.layers = new ArrayList<>(); + if ((field = json.get(LAYERS)) != null && field.isJsonArray()) { + for(JsonElement item : field.getAsJsonArray()) { + if(item.isJsonPrimitive()) { + this.layers.add(item.getAsString()); + } + } + } + } + + @Override + public String getConfig() { + return config; + } + + @Override + public String getId() { + return this.config == null || !this.config.endsWith(CONFIG_JSON_SUFFIX) ? this.config : this.config.substring(0, this.config.length() - CONFIG_JSON_SUFFIX.length()); + } + + @Override + public List getRepoTags() { + return repoTags; + } + + @Override + public List getLayers() { + return layers; + } +} diff --git a/src/main/java/io/fabric8/maven/docker/service/BuildService.java b/src/main/java/io/fabric8/maven/docker/service/BuildService.java index 1b74716c7..be0e18f7e 100644 --- a/src/main/java/io/fabric8/maven/docker/service/BuildService.java +++ b/src/main/java/io/fabric8/maven/docker/service/BuildService.java @@ -5,12 +5,17 @@ import java.io.Serializable; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.LinkedList; +import java.util.regex.PatternSyntaxException; +import org.apache.maven.plugin.MojoExecutionException; +import com.google.common.collect.ImmutableMap; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + import io.fabric8.maven.docker.access.BuildOptions; import io.fabric8.maven.docker.access.DockerAccess; import io.fabric8.maven.docker.access.DockerAccessException; @@ -19,15 +24,15 @@ import io.fabric8.maven.docker.config.BuildImageConfiguration; import io.fabric8.maven.docker.config.CleanupMode; import io.fabric8.maven.docker.config.ImageConfiguration; +import io.fabric8.maven.docker.model.ImageArchiveManifest; +import io.fabric8.maven.docker.model.ImageArchiveManifestEntry; import io.fabric8.maven.docker.util.DockerFileUtil; import io.fabric8.maven.docker.util.EnvUtil; +import io.fabric8.maven.docker.util.ImageArchiveUtil; import io.fabric8.maven.docker.util.ImageName; import io.fabric8.maven.docker.util.Logger; import io.fabric8.maven.docker.util.MojoParameters; - -import com.google.common.collect.ImmutableMap; - -import org.apache.maven.plugin.MojoExecutionException; +import io.fabric8.maven.docker.util.NamePatternUtil; public class BuildService { @@ -106,14 +111,24 @@ protected void buildImage(ImageConfiguration imageConfig, MojoParameters params, oldImageId = queryService.getImageId(imageName); } - long time = System.currentTimeMillis(); - if (buildConfig.getDockerArchive() != null) { - docker.loadImage(imageName, buildConfig.getAbsoluteDockerTarPath(params)); + File tarArchive = buildConfig.getAbsoluteDockerTarPath(params); + String archiveImageName = getArchiveImageName(buildConfig, tarArchive); + + long time = System.currentTimeMillis(); + + docker.loadImage(imageName, tarArchive); log.info("%s: Loaded tarball in %s", buildConfig.getDockerArchive(), EnvUtil.formatDurationTill(time)); + + if(archiveImageName != null && !archiveImageName.equals(imageName)) { + docker.tag(archiveImageName, imageName, true); + } + return; } + long time = System.currentTimeMillis(); + File dockerArchive = archiveService.createArchive(imageName, buildConfig, params, log); log.info("%s: Created %s in %s", imageConfig.getDescription(), dockerArchive.getName(), EnvUtil.formatDurationTill(time)); @@ -152,6 +167,82 @@ private Map prepareBuildArgs(Map buildArgs, Buil return builder.build(); } + private String getArchiveImageName(BuildImageConfiguration buildConfig, File tarArchive) throws MojoExecutionException { + if(buildConfig.getLoadNamePattern() == null || buildConfig.getLoadNamePattern().length() == 0) { + return null; + } + + ImageArchiveManifest manifest; + try { + manifest = readArchiveManifest(tarArchive); + } catch (IOException | JsonParseException e) { + throw new MojoExecutionException("Unable to read image manifest in archive " + buildConfig.getDockerArchive(), e); + } + + String archiveImageName; + + try { + archiveImageName = matchArchiveImagesToPattern(buildConfig.getLoadNamePattern(), manifest); + } catch(PatternSyntaxException e) { + throw new MojoExecutionException("Unable to interpret loadNamePattern " + buildConfig.getLoadNamePattern(), e); + } + + if(archiveImageName == null) { + throw new MojoExecutionException("No image in the archive has a tag that matches pattern " + buildConfig.getLoadNamePattern()); + } + + return archiveImageName; + } + + private ImageArchiveManifest readArchiveManifest(File tarArchive) throws IOException, JsonParseException { + long time = System.currentTimeMillis(); + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(tarArchive); + + log.info("%s: Read archive manifest in %s", tarArchive, EnvUtil.formatDurationTill(time)); + + // Show the results of reading the manifest to users trying to debug their configuration + if(log.isDebugEnabled()) { + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + log.debug("Entry ID: %s has %d repo tag(s)", entry.getId(), entry.getRepoTags().size()); + for(String repoTag : entry.getRepoTags()) { + log.debug("Repo Tag: %s", repoTag); + } + } + } + + return manifest; + } + + private String matchArchiveImagesToPattern(String imageNamePattern, ImageArchiveManifest manifest) { + String imageNameRegex = NamePatternUtil.convertImageNamePattern(imageNamePattern); + log.debug("Image name regex is %s", imageNameRegex); + + Map entries = ImageArchiveUtil.findEntriesByRepoTagPattern(imageNameRegex, manifest); + + // Show the matches from the manifest to users trying to debug their configuration + if(log.isDebugEnabled()) { + for(Map.Entry entry : entries.entrySet()) { + log.debug("Repo tag pattern matched %s referring to image %s", entry.getKey(), entry.getValue().getId()); + } + } + + if(!entries.isEmpty()) { + Map.Entry matchedEntry = entries.entrySet().iterator().next(); + + if(ImageArchiveUtil.mapEntriesById(entries.values()).size() > 1) { + log.warn("Multiple image ids matched pattern %s: using tag %s associated with id %s", + imageNamePattern, matchedEntry.getKey(), matchedEntry.getValue().getId()); + } else { + log.info("Using image tag %s from archive", matchedEntry.getKey()); + } + + return matchedEntry.getKey(); + } + + return null; + } + private String getDockerfileName(BuildImageConfiguration buildConfig) { if (buildConfig.isDockerFileMode()) { return buildConfig.getDockerFile().getName(); diff --git a/src/main/java/io/fabric8/maven/docker/util/ImageArchiveUtil.java b/src/main/java/io/fabric8/maven/docker/util/ImageArchiveUtil.java new file mode 100644 index 000000000..4b842cf46 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/util/ImageArchiveUtil.java @@ -0,0 +1,229 @@ +package io.fabric8.maven.docker.util; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.lang3.tuple.Pair; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import io.fabric8.maven.docker.model.ImageArchiveManifest; +import io.fabric8.maven.docker.model.ImageArchiveManifestAdapter; +import io.fabric8.maven.docker.model.ImageArchiveManifestEntry; + +/** + * Helper functions for working with Docker image archives, as produced by + * the docker:save mojo. + */ +public class ImageArchiveUtil { + public static final String MANIFEST_JSON = "manifest.json"; + + private static InputStream createUncompressedStream(InputStream possiblyCompressed) { + if(!possiblyCompressed.markSupported()) { + possiblyCompressed = new BufferedInputStream(possiblyCompressed, 512 * 1000); + } + + try { + return new CompressorStreamFactory().createCompressorInputStream(possiblyCompressed); + } catch(CompressorException e) { + return possiblyCompressed; + } + } + + /** + * Read the (possibly compressed) image archive provided and return the archive manifest. + * + * If there is no manifest found, then null is returned. Incomplete manifests are returned + * with as much information parsed as possible. + * + * @param file + * @return the parsed manifest, or null if none found. + * @throws IOException + * @throws JsonParseException + */ + public static ImageArchiveManifest readManifest(File file) throws IOException, JsonParseException { + return readManifest(new FileInputStream(file)); + } + + + /** + * Read the (possibly compressed) image archive stream provided and return the archive manifest. + * + * If there is no manifest found, then null is returned. Incomplete manifests are returned + * with as much information parsed as possible. + * + * @param inputStream + * @return the parsed manifest, or null if none found. + * @throws IOException + * @throws JsonParseException + */ + public static ImageArchiveManifest readManifest(InputStream inputStream) throws IOException, JsonParseException { + Map parseExceptions = new LinkedHashMap<>(); + Map parsedEntries = new LinkedHashMap<>(); + + try (TarArchiveInputStream tarStream = new TarArchiveInputStream(createUncompressedStream(inputStream))) { + TarArchiveEntry tarEntry; + Gson gson = new Gson(); + + while((tarEntry = tarStream.getNextTarEntry()) != null) { + if(tarEntry.isFile() && tarEntry.getName().endsWith(".json")) { + try { + JsonElement element = gson.fromJson(new InputStreamReader(tarStream, StandardCharsets.UTF_8), JsonElement.class); + parsedEntries.put(tarEntry.getName(), element); + } catch(JsonParseException exception) { + parseExceptions.put(tarEntry.getName(), exception); + } + } + } + } + + JsonElement manifestJson = parsedEntries.get(MANIFEST_JSON); + if(manifestJson == null) { + JsonParseException parseException = parseExceptions.get(MANIFEST_JSON); + if(parseException != null) { + throw parseException; + } + + return null; + } + + ImageArchiveManifestAdapter manifest = new ImageArchiveManifestAdapter(manifestJson); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + JsonElement entryConfigJson = parsedEntries.get(entry.getConfig()); + if(entryConfigJson != null && entryConfigJson.isJsonObject()) { + manifest.putConfig(entry.getConfig(), entryConfigJson.getAsJsonObject()); + } + } + + return manifest; + } + + /** + * Search the manifest for an entry that has the repository and tag provided. + * + * @param repoTag the repository and tag to search (e.g. busybox:latest). + * @param manifest the manifest to be searched + * @return the entry found, or null if no match. + */ + public static ImageArchiveManifestEntry findEntryByRepoTag(String repoTag, ImageArchiveManifest manifest) { + if(repoTag == null || manifest == null) { + return null; + } + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + for(String entryRepoTag : entry.getRepoTags()) { + if(repoTag.equals(entryRepoTag)) { + return entry; + } + } + } + + return null; + } + + /** + * Search the manifest for an entry that has a repository and tag matching the provided pattern. + * + * @param repoTagPattern the repository and tag to search (e.g. busybox:latest). + * @param manifest the manifest to be searched + * @return a pair containing the matched tag and the entry found, or null if no match. + */ + public static Pair findEntryByRepoTagPattern(String repoTagPattern, ImageArchiveManifest manifest) throws PatternSyntaxException { + return findEntryByRepoTagPattern(repoTagPattern == null ? null : Pattern.compile(repoTagPattern), manifest); + } + + /** + * Search the manifest for an entry that has a repository and tag matching the provided pattern. + * + * @param repoTagPattern the repository and tag to search (e.g. busybox:latest). + * @param manifest the manifest to be searched + * @return a pair containing the matched tag and the entry found, or null if no match. + */ + public static Pair findEntryByRepoTagPattern(Pattern repoTagPattern, ImageArchiveManifest manifest) throws PatternSyntaxException { + if(repoTagPattern == null || manifest == null) { + return null; + } + + Matcher matcher = repoTagPattern.matcher(""); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + for(String entryRepoTag : entry.getRepoTags()) { + if(matcher.reset(entryRepoTag).find()) { + return Pair.of(entryRepoTag, entry); + } + } + } + + return null; + } + + /** + * Search the manifest for an entry that has a repository and tag matching the provided pattern. + * + * @param repoTagPattern the repository and tag to search (e.g. busybox:latest). + * @param manifest the manifest to be searched + * @return a pair containing the matched tag and the entry found, or null if no match. + */ + public static Map findEntriesByRepoTagPattern(String repoTagPattern, ImageArchiveManifest manifest) throws PatternSyntaxException { + return findEntriesByRepoTagPattern(repoTagPattern == null ? null : Pattern.compile(repoTagPattern), manifest); + } + + /** + * Search the manifest for an entry that has a repository and tag matching the provided pattern. + * + * @param repoTagPattern the repository and tag to search (e.g. busybox:latest). + * @param manifest the manifest to be searched + * @return a pair containing the matched tag and the entry found, or null if no match. + */ + public static Map findEntriesByRepoTagPattern(Pattern repoTagPattern, ImageArchiveManifest manifest) throws PatternSyntaxException { + Map entries = new LinkedHashMap<>(); + + if(repoTagPattern == null || manifest == null) { + return entries; + } + + Matcher matcher = repoTagPattern.matcher(""); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + for(String entryRepoTag : entry.getRepoTags()) { + if(matcher.reset(entryRepoTag).find()) { + entries.putIfAbsent(entryRepoTag, entry); + } + } + } + + return entries; + } + + /** + * Build a map of entries by id from an iterable of entries. + * + * @param entries + * @return a map of entries by id + */ + public static Map mapEntriesById(Iterable entries) { + Map mapped = new LinkedHashMap<>(); + + for(ImageArchiveManifestEntry entry : entries) { + mapped.put(entry.getId(), entry); + } + + return mapped; + } +} diff --git a/src/main/java/io/fabric8/maven/docker/util/NamePatternUtil.java b/src/main/java/io/fabric8/maven/docker/util/NamePatternUtil.java new file mode 100644 index 000000000..d537b8c7b --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/util/NamePatternUtil.java @@ -0,0 +1,70 @@ +package io.fabric8.maven.docker.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper functions for pattern matching for image and container names. + */ +public class NamePatternUtil { + + /** + * Accepts an Ant-ish or regular expression pattern and compiles to a regular expression. + * + * This is similar to SelectorUtils in the Maven codebase, but there the code uses the + * platform File.separator, while here we always want to work with forward slashes. + * Also, for a more natural fit with repository tags, both * and ** should stop at the colon + * that precedes the tag. + * + * Like SelectorUtils, wrapping a pattern in %regex[pattern] will create a regex from the + * pattern provided without translation. Otherwise, or if wrapped in %ant[pattern], + * then a regular expression will be created that is anchored at beginning and end, + * converts ? to [^/:], * to ([^/:]|:(?=.*:)) and ** to ([^:]|:(?=.*:))*. + * + * If ** is followed by /, the / is converted to a negative lookbehind for anything + * apart from a slash. + * + * @return a regular expression pattern created from the input pattern + */ + public static String convertImageNamePattern(String pattern) { + final String REGEX_PREFIX = "%regex[", ANT_PREFIX = "%ant[", PATTERN_SUFFIX="]"; + + if(pattern.startsWith(REGEX_PREFIX) && pattern.endsWith(PATTERN_SUFFIX)) { + return pattern.substring(REGEX_PREFIX.length(), pattern.length() - PATTERN_SUFFIX.length()); + } + + if(pattern.startsWith(ANT_PREFIX) && pattern.endsWith(PATTERN_SUFFIX)) { + pattern = pattern.substring(ANT_PREFIX.length(), pattern.length() - PATTERN_SUFFIX.length()); + } + + String[] parts = pattern.split("((?=[/:?*])|(?<=[/:?*]))"); + Matcher matcher = Pattern.compile("[A-Za-z0-9-]+").matcher(""); + + StringBuilder builder = new StringBuilder("^"); + + for(int i = 0; i < parts.length; ++i) { + if("?".equals(parts[i])) { + builder.append("[^/:]"); + } else if("*".equals(parts[i])) { + if (i + 1 < parts.length && "*".equals(parts[i + 1])) { + builder.append("([^:]|:(?=.*:))*"); + ++i; + if (i + 1 < parts.length && "/".equals(parts[i + 1])) { + builder.append("(? 0) { + builder.append(Pattern.quote(parts[i])); + } + } + + builder.append("$"); + + return builder.toString(); + } +} diff --git a/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapterTest.java b/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapterTest.java new file mode 100644 index 000000000..9333c43ae --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestAdapterTest.java @@ -0,0 +1,98 @@ +package io.fabric8.maven.docker.model; + +import org.junit.Assert; +import org.junit.Test; +import com.google.gson.JsonArray; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; + +public class ImageArchiveManifestAdapterTest { + @Test + public void createFromEmptyJsonArray() { + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(new JsonArray()); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertTrue("No entries in manifest", manifest.getEntries().isEmpty()); + } + + @Test + public void createFromJsonArrayNonObject() { + JsonArray jsonArray = new JsonArray(); + jsonArray.add(false); + jsonArray.add(new JsonArray()); + jsonArray.add(10); + + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(jsonArray); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertTrue("No entries in manifest", manifest.getEntries().isEmpty()); + } + + @Test + public void createFromEmptyJsonObject() { + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(new JsonObject()); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertTrue("No entries in manifest", manifest.getEntries().isEmpty()); + } + + @Test + public void createFromJsonNull() { + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(JsonNull.INSTANCE); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertTrue("No entries in manifest", manifest.getEntries().isEmpty()); + } + + @Test + public void createFromArrayOfObject() { + JsonArray objects = new JsonArray(); + objects.add(new JsonObject()); + + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(objects); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertFalse("Some entries in manifest", manifest.getEntries().isEmpty()); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + Assert.assertNotNull(entry); + } + } + + @Test + public void createFromArrayOfObjects() { + JsonArray objects = new JsonArray(); + objects.add(new JsonObject()); + objects.add(new JsonObject()); + objects.add(new JsonObject()); + + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(objects); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertFalse("Some entries in manifest", manifest.getEntries().isEmpty()); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + Assert.assertNotNull(entry); + } + } + + @Test + public void createFromArrayOfObjectsAndElements() { + JsonArray objects = new JsonArray(); + objects.add(new JsonObject()); + objects.add(new JsonArray()); + objects.add(new JsonObject()); + objects.add("ABC"); + objects.add(123); + objects.add(JsonNull.INSTANCE); + + ImageArchiveManifest manifest = new ImageArchiveManifestAdapter(objects); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertFalse("Some entries in manifest", manifest.getEntries().isEmpty()); + + for(ImageArchiveManifestEntry entry : manifest.getEntries()) { + Assert.assertNotNull(entry); + } + } +} diff --git a/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapterTest.java b/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapterTest.java new file mode 100644 index 000000000..744f50f6a --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/model/ImageArchiveManifestEntryAdapterTest.java @@ -0,0 +1,100 @@ +package io.fabric8.maven.docker.model; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Assert; +import org.junit.Test; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class ImageArchiveManifestEntryAdapterTest { + @Test + public void createFromEmptyJsonObject() { + ImageArchiveManifestEntryAdapter entry = new ImageArchiveManifestEntryAdapter(new JsonObject()); + + Assert.assertNotNull(entry); + Assert.assertNull(entry.getConfig()); + Assert.assertNull(entry.getId()); + Assert.assertNotNull(entry.getRepoTags()); + Assert.assertTrue(entry.getRepoTags().isEmpty()); + Assert.assertNotNull(entry.getLayers()); + Assert.assertTrue(entry.getLayers().isEmpty()); + } + + @Test + public void createFromValidJsonObject() { + JsonObject entryJson = new JsonObject(); + entryJson.addProperty(ImageArchiveManifestEntryAdapter.CONFIG, "image-id-sha256.json"); + + JsonArray repoTagsJson = new JsonArray(); + repoTagsJson.add("test/image:latest"); + entryJson.add(ImageArchiveManifestEntryAdapter.REPO_TAGS, repoTagsJson); + + JsonArray layersJson = new JsonArray(); + layersJson.add("layer-id-sha256/layer.tar"); + entryJson.add(ImageArchiveManifestEntryAdapter.LAYERS, layersJson); + + ImageArchiveManifestEntryAdapter entry = new ImageArchiveManifestEntryAdapter(entryJson); + + Assert.assertNotNull(entry); + Assert.assertEquals("image-id-sha256.json", entry.getConfig()); + Assert.assertEquals("image-id-sha256", entry.getId()); + Assert.assertNotNull(entry.getRepoTags()); + Assert.assertEquals(Collections.singletonList("test/image:latest"), entry.getRepoTags()); + Assert.assertNotNull(entry.getLayers()); + Assert.assertEquals(Collections.singletonList("layer-id-sha256/layer.tar"), entry.getLayers()); + } + + @Test + public void createFromValidJsonObjectWithAdditionalFields() { + JsonObject entryJson = new JsonObject(); + entryJson.addProperty("Random", "new feature"); + + entryJson.addProperty(ImageArchiveManifestEntryAdapter.CONFIG, "image-id-sha256.json"); + + JsonArray repoTagsJson = new JsonArray(); + repoTagsJson.add("test/image:latest"); + entryJson.add(ImageArchiveManifestEntryAdapter.REPO_TAGS, repoTagsJson); + + JsonArray layersJson = new JsonArray(); + layersJson.add("layer-id-sha256/layer.tar"); + entryJson.add(ImageArchiveManifestEntryAdapter.LAYERS, layersJson); + + ImageArchiveManifestEntryAdapter entry = new ImageArchiveManifestEntryAdapter(entryJson); + + Assert.assertNotNull(entry); + Assert.assertEquals("image-id-sha256.json", entry.getConfig()); + Assert.assertEquals("image-id-sha256", entry.getId()); + Assert.assertNotNull(entry.getRepoTags()); + Assert.assertEquals(Collections.singletonList("test/image:latest"), entry.getRepoTags()); + Assert.assertNotNull(entry.getLayers()); + Assert.assertEquals(Collections.singletonList("layer-id-sha256/layer.tar"), entry.getLayers()); + } + + @Test + public void createFromPartlyValidJsonObject() { + JsonObject entryJson = new JsonObject(); + + entryJson.addProperty(ImageArchiveManifestEntryAdapter.CONFIG, "image-id-sha256.json"); + + JsonArray repoTagsJson = new JsonArray(); + repoTagsJson.add("test/image:latest"); + entryJson.add(ImageArchiveManifestEntryAdapter.REPO_TAGS, repoTagsJson); + + JsonObject layersJson = new JsonObject(); + layersJson.addProperty("layer1", "layer-id-sha256/layer.tar"); + entryJson.add(ImageArchiveManifestEntryAdapter.LAYERS, layersJson); + + ImageArchiveManifestEntryAdapter entry = new ImageArchiveManifestEntryAdapter(entryJson); + + Assert.assertNotNull(entry); + Assert.assertEquals("image-id-sha256.json", entry.getConfig()); + Assert.assertEquals("image-id-sha256", entry.getId()); + Assert.assertNotNull(entry.getRepoTags()); + Assert.assertEquals(Collections.singletonList("test/image:latest"), entry.getRepoTags()); + Assert.assertNotNull(entry.getLayers()); + Assert.assertTrue(entry.getLayers().isEmpty()); + } + +} diff --git a/src/test/java/io/fabric8/maven/docker/util/ImageArchiveUtilTest.java b/src/test/java/io/fabric8/maven/docker/util/ImageArchiveUtilTest.java new file mode 100644 index 000000000..b9da8e1ff --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/util/ImageArchiveUtilTest.java @@ -0,0 +1,311 @@ +package io.fabric8.maven.docker.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.regex.PatternSyntaxException; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Assert; +import org.junit.Test; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import io.fabric8.maven.docker.model.ImageArchiveManifest; +import io.fabric8.maven.docker.model.ImageArchiveManifestAdapter; +import io.fabric8.maven.docker.model.ImageArchiveManifestEntry; +import io.fabric8.maven.docker.model.ImageArchiveManifestEntryAdapter; + +public class ImageArchiveUtilTest { + @Test + public void readEmptyArchive() throws IOException { + byte[] emptyTar; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream tarOutput = new TarArchiveOutputStream(baos)) { + tarOutput.finish(); + emptyTar = baos.toByteArray(); + } + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(new ByteArrayInputStream(emptyTar)); + Assert.assertNull(manifest); + } + + @Test + public void readUnrelatedArchive() throws IOException { + byte[] archiveBytes; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream tarOutput = new TarArchiveOutputStream(baos)) { + final byte[] entryData = UUID.randomUUID().toString().getBytes(); + TarArchiveEntry tarEntry = new TarArchiveEntry("unrelated.data"); + tarEntry.setSize(entryData.length); + tarOutput.putArchiveEntry(tarEntry); + tarOutput.write(entryData); + tarOutput.closeArchiveEntry(); + tarOutput.finish(); + archiveBytes = baos.toByteArray(); + } + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(new ByteArrayInputStream(archiveBytes)); + Assert.assertNull(manifest); + } + + @Test(expected = JsonParseException.class) + public void readInvalidManifestInArchive() throws IOException { + byte[] archiveBytes; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream tarOutput = new TarArchiveOutputStream(baos)) { + final byte[] entryData = ("}" + UUID.randomUUID().toString() + "{").getBytes(); + TarArchiveEntry tarEntry = new TarArchiveEntry(ImageArchiveUtil.MANIFEST_JSON); + tarEntry.setSize(entryData.length); + tarOutput.putArchiveEntry(tarEntry); + tarOutput.write(entryData); + tarOutput.closeArchiveEntry(); + tarOutput.finish(); + archiveBytes = baos.toByteArray(); + } + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(new ByteArrayInputStream(archiveBytes)); + Assert.assertNull(manifest); + } + + @Test + public void readInvalidJsonInArchive() throws IOException { + byte[] archiveBytes; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream tarOutput = new TarArchiveOutputStream(baos)) { + final byte[] entryData = ("}" + UUID.randomUUID().toString() + "{").getBytes(); + TarArchiveEntry tarEntry = new TarArchiveEntry("not-the-" + ImageArchiveUtil.MANIFEST_JSON); + tarEntry.setSize(entryData.length); + tarOutput.putArchiveEntry(tarEntry); + tarOutput.write(entryData); + tarOutput.closeArchiveEntry(); + tarOutput.finish(); + archiveBytes = baos.toByteArray(); + } + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(new ByteArrayInputStream(archiveBytes)); + Assert.assertNull(manifest); + } + + protected JsonArray createBasicManifestJson() { + JsonObject entryJson = new JsonObject(); + + entryJson.addProperty(ImageArchiveManifestEntryAdapter.CONFIG, "image-id-sha256.json"); + + JsonArray repoTagsJson = new JsonArray(); + repoTagsJson.add("test/image:latest"); + entryJson.add(ImageArchiveManifestEntryAdapter.REPO_TAGS, repoTagsJson); + + JsonArray layersJson = new JsonArray(); + layersJson.add("layer-id-sha256/layer.tar"); + entryJson.add(ImageArchiveManifestEntryAdapter.LAYERS, layersJson); + + JsonArray manifestJson = new JsonArray(); + manifestJson.add(entryJson); + + return manifestJson; + } + + @Test + public void readValidArchive() throws IOException { + final byte[] entryData = new Gson().toJson(createBasicManifestJson()).getBytes(StandardCharsets.UTF_8); + byte[] archiveBytes; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream tarOutput = new TarArchiveOutputStream(baos)) { + TarArchiveEntry tarEntry = new TarArchiveEntry(ImageArchiveUtil.MANIFEST_JSON); + tarEntry.setSize(entryData.length); + tarOutput.putArchiveEntry(tarEntry); + tarOutput.write(entryData); + tarOutput.closeArchiveEntry(); + tarOutput.finish(); + archiveBytes = baos.toByteArray(); + } + + ImageArchiveManifest manifest = ImageArchiveUtil.readManifest(new ByteArrayInputStream(archiveBytes)); + Assert.assertNotNull(manifest); + Assert.assertNotNull(manifest.getEntries()); + Assert.assertFalse(manifest.getEntries().isEmpty()); + + ImageArchiveManifestEntry entry = manifest.getEntries().get(0); + Assert.assertNotNull(entry); + Assert.assertEquals("image-id-sha256.json", entry.getConfig()); + Assert.assertEquals("image-id-sha256", entry.getId()); + Assert.assertNotNull(entry.getRepoTags()); + Assert.assertEquals(Collections.singletonList("test/image:latest"), entry.getRepoTags()); + Assert.assertNotNull(entry.getLayers()); + Assert.assertEquals(Collections.singletonList("layer-id-sha256/layer.tar"), entry.getLayers()); + } + + @Test + public void findByRepoTagEmptyManifest() { + ImageArchiveManifest empty = new ImageArchiveManifestAdapter(new JsonArray()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag("anything", empty)); + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag("anything", null)); + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag(null, null)); + } + + @Test + public void findByRepoTagNonEmptyManifest() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag("anything", nonEmpty)); + // Prefix + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag("test", nonEmpty)); + // Prefix + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTag("test/image", nonEmpty)); + } + + @Test + public void findByRepoTagSuccessfully() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + ImageArchiveManifestEntry found = ImageArchiveUtil.findEntryByRepoTag("test/image:latest", nonEmpty); + + Assert.assertNotNull(found); + Assert.assertTrue(found.getRepoTags().contains("test/image:latest")); + } + + @Test + public void findByRepoTagPatternEmptyManifest() { + ImageArchiveManifest empty = new ImageArchiveManifestAdapter(new JsonArray()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern(".*", empty)); + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern(".*", null)); + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern((String)null, null)); + } + + @Test(expected = PatternSyntaxException.class) + public void findByRepoTagPatternInvalidPattern() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern("*(?", nonEmpty)); + } + + @Test + public void findByRepoTagPatternNonEmptyManifest() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern("does/not:match", nonEmpty)); + // Anchored pattern + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern("^test/image$", nonEmpty)); + } + + @Test + public void findByRepoTagPatternSuccessfully() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + Pair found; + + // Complete match + found = ImageArchiveUtil.findEntryByRepoTagPattern("test/image:latest", nonEmpty); + Assert.assertNotNull(found); + Assert.assertEquals("test/image:latest", found.getLeft()); + Assert.assertNotNull(found.getRight()); + Assert.assertTrue(found.getRight().getRepoTags().contains("test/image:latest")); + + // Unanchored match + found = ImageArchiveUtil.findEntryByRepoTagPattern("test/image", nonEmpty); + Assert.assertNotNull(found); + Assert.assertEquals("test/image:latest", found.getLeft()); + Assert.assertNotNull(found.getRight()); + Assert.assertTrue(found.getRight().getRepoTags().contains("test/image:latest")); + + // Initial anchor + found = ImageArchiveUtil.findEntryByRepoTagPattern("^test/image", nonEmpty); + Assert.assertNotNull(found); + Assert.assertEquals("test/image:latest", found.getLeft()); + Assert.assertNotNull(found.getRight()); + Assert.assertTrue(found.getRight().getRepoTags().contains("test/image:latest")); + } + + @Test + public void findEntriesByRepoTagPatternEmptyManifest() { + ImageArchiveManifest empty = new ImageArchiveManifestAdapter(new JsonArray()); + Map entries; + + entries = ImageArchiveUtil.findEntriesByRepoTagPattern((String)null, null); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + + entries = ImageArchiveUtil.findEntriesByRepoTagPattern(".*", null); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + + entries = ImageArchiveUtil.findEntriesByRepoTagPattern((String)null, empty); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + + entries = ImageArchiveUtil.findEntriesByRepoTagPattern(".*", empty); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + } + + @Test(expected = PatternSyntaxException.class) + public void findEntriesByRepoTagPatternInvalidPattern() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + + Assert.assertNull(ImageArchiveUtil.findEntryByRepoTagPattern("*(?", nonEmpty)); + } + + @Test + public void findEntriesByRepoTagPatternNonEmptyManifest() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + Map entries; + + entries = ImageArchiveUtil.findEntriesByRepoTagPattern("does/not:match", nonEmpty); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + + // Anchored pattern + entries = ImageArchiveUtil.findEntriesByRepoTagPattern("^test/image$", nonEmpty); + Assert.assertNotNull(entries); + Assert.assertTrue(entries.isEmpty()); + } + + @Test + public void findEntriesByRepoTagPatternSuccessfully() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + Map entries; + + // Complete match + entries = ImageArchiveUtil.findEntriesByRepoTagPattern("test/image:latest", nonEmpty); + Assert.assertNotNull(entries); + Assert.assertNotNull(entries.get("test/image:latest")); + Assert.assertTrue(entries.get("test/image:latest").getRepoTags().contains("test/image:latest")); + + // Unanchored match + entries = ImageArchiveUtil.findEntriesByRepoTagPattern("test/image", nonEmpty); + Assert.assertNotNull(entries); + Assert.assertNotNull(entries.get("test/image:latest")); + Assert.assertTrue(entries.get("test/image:latest").getRepoTags().contains("test/image:latest")); + + // Initial anchor + entries = ImageArchiveUtil.findEntriesByRepoTagPattern("^test/image", nonEmpty); + Assert.assertNotNull(entries); + Assert.assertNotNull(entries.get("test/image:latest")); + Assert.assertTrue(entries.get("test/image:latest").getRepoTags().contains("test/image:latest")); + } + + @Test + public void mapEntriesByIdSuccessfully() { + ImageArchiveManifest nonEmpty = new ImageArchiveManifestAdapter(createBasicManifestJson()); + Map entries = ImageArchiveUtil.mapEntriesById(nonEmpty.getEntries()); + + Assert.assertNotNull(entries); + Assert.assertEquals(1, entries.size()); + Assert.assertNotNull(entries.get("image-id-sha256")); + Assert.assertTrue(entries.get("image-id-sha256").getRepoTags().contains("test/image:latest")); + } +} diff --git a/src/test/java/io/fabric8/maven/docker/util/NamePatternUtilTest.java b/src/test/java/io/fabric8/maven/docker/util/NamePatternUtilTest.java new file mode 100644 index 000000000..64a05adff --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/util/NamePatternUtilTest.java @@ -0,0 +1,63 @@ +package io.fabric8.maven.docker.util; + +import org.junit.Assert; +import org.junit.Test; + +public class NamePatternUtilTest { + @Test + public void convertNonPatternRepoTagPatterns() { + Assert.assertEquals("^$", NamePatternUtil.convertImageNamePattern("")); + Assert.assertEquals("^a$", NamePatternUtil.convertImageNamePattern("a")); + Assert.assertEquals("^hello$", NamePatternUtil.convertImageNamePattern("hello")); + Assert.assertEquals("^hello/world$", NamePatternUtil.convertImageNamePattern("hello/world")); + Assert.assertEquals("^hello/world:latest$", NamePatternUtil.convertImageNamePattern("hello/world:latest")); + Assert.assertEquals("^\\Qregistry.com\\E/hello/world:latest$", NamePatternUtil.convertImageNamePattern("registry.com/hello/world:latest")); + Assert.assertEquals("^\\Qregistry.com\\E:8080/hello/world:latest$", NamePatternUtil.convertImageNamePattern("registry.com:8080/hello/world:latest")); + + Assert.assertEquals("^hello/world:\\Q1.0-SNAPSHOT\\E$", NamePatternUtil.convertImageNamePattern("hello/world:1.0-SNAPSHOT")); + Assert.assertEquals("^\\Qh\\E\\\\E\\Qllo\\E/\\Qw\\Qrld\\E:\\Q1.0-SNAPSHOT\\E$", NamePatternUtil.convertImageNamePattern("h\\Ello/w\\Qrld:1.0-SNAPSHOT")); + Assert.assertEquals("^\\Qhello! [World] \\E:\\Q not really a tag, right\\E$", NamePatternUtil.convertImageNamePattern("hello! [World] : not really a tag, right")); + } + + @Test + public void convertPatternRepoTagPatterns() { + Assert.assertEquals("^[^/:]$", NamePatternUtil.convertImageNamePattern("?")); + Assert.assertEquals("^[^/:][^/:]$", NamePatternUtil.convertImageNamePattern("??")); + Assert.assertEquals("^hello[^/:][^/:]$", NamePatternUtil.convertImageNamePattern("hello??")); + Assert.assertEquals("^hello[^/:][^/:]\\Qare you there\\E$", NamePatternUtil.convertImageNamePattern("hello??are you there")); + Assert.assertEquals("^[^/:][^/:]whaaat$", NamePatternUtil.convertImageNamePattern("??whaaat")); + + Assert.assertEquals("^([^/:]|:(?=.*:))*$", NamePatternUtil.convertImageNamePattern("*")); + Assert.assertEquals("^my-company/([^/:]|:(?=.*:))*$", NamePatternUtil.convertImageNamePattern("my-company/*")); + Assert.assertEquals("^my-co([^/:]|:(?=.*:))*/([^/:]|:(?=.*:))*$", NamePatternUtil.convertImageNamePattern("my-co*/*")); + + Assert.assertEquals("^([^:]|:(?=.*:))*(?