diff --git a/doc/changelog.md b/doc/changelog.md index 61dcc1e98..659860a9f 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -1,6 +1,14 @@ # ChangeLog -* **0.28-SNAPSHOT** +* **0.29-SNAPSHOT** + - 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)) + - Allow docker:save to attach image archive as a project artifact ([#1210](https://github.com/fabric8io/docker-maven-plugin/pull/1210)) + - 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)) - Reintroduce minimal API-VERSION parameter in order to support docker versions below apiVersion 1.25 - docs: Correct default image naming - Proxy settings are being ignored ([#1148](https://github.com/fabric8io/docker-maven-plugin/issues/1148)) @@ -10,12 +18,12 @@ - Update to jnr-unixsocket 0.22 - Support for new docker build --pull option (#1191) - Enhance @sha256 digest for tags in FROM (image_name:image_tag@sha256) ([#541](https://github.com/fabric8io/docker-maven-plugin/issues/541)) - - Support docker SHELL setting for runCmds (#1157) - - Added 'autoRemove' option for running containers (#1179) - - Added support for AWS EC2 instance roles when pushing to AWS ECR (#1186) - - Introduce `contextDir` configuration option which would be used to specify docker build context (#1189) - - Add support for auto-pulling multiple base image for multi stage builds (#1057) - - Fix usage of credential helper that do not support 'version' command (#1159) + - Support docker SHELL setting for runCmds ([#1157](https://github.com/fabric8io/docker-maven-plugin/issues/1157)) + - Added 'autoRemove' option for running containers ([#1179](https://github.com/fabric8io/docker-maven-plugin/issues/1179)) + - Added support for AWS EC2 instance roles when pushing to AWS ECR ([#1186](https://github.com/fabric8io/docker-maven-plugin/issues/1186)) + - Introduce `contextDir` configuration option which would be used to specify docker build context ([#1189](https://github.com/fabric8io/docker-maven-plugin/issues/1189)) + - Add support for auto-pulling multiple base image for multi stage builds ([#1057](https://github.com/fabric8io/docker-maven-plugin/issues/1057)) + - Fix usage of credential helper that do not support 'version' command ([#1159](https://github.com/fabric8io/docker-maven-plugin/issues/1159)) Please note that `dockerFileDir` is now deprecated in favor of `contextDir` which also allows absolute paths to Dockerfile with `dockerFile` and it will be removed in 1.0.0. It's still supported in this release but users are suggested to migrate to diff --git a/pom.xml b/pom.xml index f09031eae..e09830889 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.fabric8 docker-maven-plugin - 0.28-SNAPSHOT + 0.29-SNAPSHOT maven-plugin docker-maven-plugin diff --git a/samples/cargo-jolokia/pom.xml b/samples/cargo-jolokia/pom.xml index 424386e93..a2c589f26 100644 --- a/samples/cargo-jolokia/pom.xml +++ b/samples/cargo-jolokia/pom.xml @@ -22,13 +22,13 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml io.fabric8 dmp-sample-cargo-jolokia - 0.28-SNAPSHOT + 0.29-SNAPSHOT http://www.jolokia.org diff --git a/samples/custom-net/pom.xml b/samples/custom-net/pom.xml index 7cac9167d..6f687a8ce 100644 --- a/samples/custom-net/pom.xml +++ b/samples/custom-net/pom.xml @@ -13,12 +13,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-custom-net - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/data-jolokia/pom.xml b/samples/data-jolokia/pom.xml index e5fa0b44e..561ebf6ba 100644 --- a/samples/data-jolokia/pom.xml +++ b/samples/data-jolokia/pom.xml @@ -22,12 +22,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-data-jolokia - 0.28-SNAPSHOT + 0.29-SNAPSHOT docker diff --git a/samples/docker-compose/pom.xml b/samples/docker-compose/pom.xml index 9c88dde58..a3c5fe98a 100644 --- a/samples/docker-compose/pom.xml +++ b/samples/docker-compose/pom.xml @@ -22,12 +22,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-docker-compose - 0.28-SNAPSHOT + 0.29-SNAPSHOT http://www.jolokia.org diff --git a/samples/dockerfile/pom.xml b/samples/dockerfile/pom.xml index d3806f6dd..858e2edca 100644 --- a/samples/dockerfile/pom.xml +++ b/samples/dockerfile/pom.xml @@ -12,12 +12,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dockerfile - 0.28-SNAPSHOT + 0.29-SNAPSHOT war dmp-sample-dockerfile diff --git a/samples/dockerignore/pom.xml b/samples/dockerignore/pom.xml index d5b848738..6ffd3e2cc 100644 --- a/samples/dockerignore/pom.xml +++ b/samples/dockerignore/pom.xml @@ -10,12 +10,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-dockerignore - 0.28-SNAPSHOT + 0.29-SNAPSHOT docker-build diff --git a/samples/healthcheck/pom.xml b/samples/healthcheck/pom.xml index cfbe83cc3..75e2ff7d0 100644 --- a/samples/healthcheck/pom.xml +++ b/samples/healthcheck/pom.xml @@ -12,12 +12,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-healthcheck - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/helloworld/pom.xml b/samples/helloworld/pom.xml index a68ebf034..418c27b66 100644 --- a/samples/helloworld/pom.xml +++ b/samples/helloworld/pom.xml @@ -12,13 +12,13 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-helloworld - 0.28-SNAPSHOT + 0.29-SNAPSHOT jar dmp-sample-helloworld diff --git a/samples/log/pom.xml b/samples/log/pom.xml index 64722c06d..536d72b6d 100644 --- a/samples/log/pom.xml +++ b/samples/log/pom.xml @@ -13,14 +13,14 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml io.fabric8 dmp-sample-log - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/multi-wait/pom.xml b/samples/multi-wait/pom.xml index ee908d297..d519fa223 100644 --- a/samples/multi-wait/pom.xml +++ b/samples/multi-wait/pom.xml @@ -3,12 +3,12 @@ 4.0.0 io.fabric8 dmp-sample-multi-wait - 0.28-SNAPSHOT + 0.29-SNAPSHOT io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml diff --git a/samples/net/pom.xml b/samples/net/pom.xml index 535e80d28..ef2a647dc 100644 --- a/samples/net/pom.xml +++ b/samples/net/pom.xml @@ -16,12 +16,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-net - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/pom.xml b/samples/pom.xml index 49ab90ba1..7e60a5b42 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -21,7 +21,7 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT pom http://www.jolokia.org diff --git a/samples/properties/pom.xml b/samples/properties/pom.xml index d9e7c6fa4..42e573ef5 100644 --- a/samples/properties/pom.xml +++ b/samples/properties/pom.xml @@ -10,12 +10,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-properties - 0.28-SNAPSHOT + 0.29-SNAPSHOT docker-build diff --git a/samples/run-java/pom.xml b/samples/run-java/pom.xml index 37b9b9850..20740e9fd 100644 --- a/samples/run-java/pom.xml +++ b/samples/run-java/pom.xml @@ -7,14 +7,14 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml io.fabric8.dmp.samples dmp-sample-run-java jar - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/smallest/pom.xml b/samples/smallest/pom.xml index 62147d197..fb2f11507 100644 --- a/samples/smallest/pom.xml +++ b/samples/smallest/pom.xml @@ -4,13 +4,13 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml fabric8io dmp-sample-smallest - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/volume/pom.xml b/samples/volume/pom.xml index 2554ffa22..23ce71565 100644 --- a/samples/volume/pom.xml +++ b/samples/volume/pom.xml @@ -15,12 +15,12 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml dmp-sample-volume - 0.28-SNAPSHOT + 0.29-SNAPSHOT diff --git a/samples/zero-config/pom.xml b/samples/zero-config/pom.xml index 0994cb811..6b7deffe7 100644 --- a/samples/zero-config/pom.xml +++ b/samples/zero-config/pom.xml @@ -7,7 +7,7 @@ io.fabric8.dmp.samples dmp-sample-parent - 0.28-SNAPSHOT + 0.29-SNAPSHOT ../pom.xml @@ -15,7 +15,7 @@ io.fabric8.dmp.samples demp-sample-zero-config jar - 0.28-SNAPSHOT + 0.29-SNAPSHOT ${project.build.directory}/${project.build.finalName}.jar diff --git a/src/main/asciidoc/inc/_docker-save.adoc b/src/main/asciidoc/inc/_docker-save.adoc index 3d6b9b0ef..7de421b57 100644 --- a/src/main/asciidoc/inc/_docker-save.adoc +++ b/src/main/asciidoc/inc/_docker-save.adoc @@ -10,6 +10,31 @@ If the option `saveFile` is not set, the file name is calculated automatically: Please note that the exported image contains all image layers and can be quite large (also, it takes a bit to export the image). +.Controlling image compression +The file name extension is used to select a compression method for the output. +[cols="3,2,1"] +|=== +| Extensions | Compression | Type + +| .tar or unrecognized +| No compression +| .tar + +| .tar.gz, .tgz +| GZIP compression +| .tar.gz + +| .tar.bz, .tar.bz2, .tar.bzip2 +| BZIP2 compression +| .tar.bz + +|=== + +.Attaching the saved image as an artifact +If `saveClassifier` is set, the saved archive will be attached to the project using the provided classifier and the type determined from the file name. The placeholder `%a` will be replaced with the image alias. + +Note that using overriding the default to use `docker` or `docker-%a` may lead to a conflict if a source archive is also attached with <<{plugin}:source>>. + .Save options [cols="1,5,1"] |=== @@ -27,6 +52,10 @@ Please note that the exported image contains all image layers and can be quite l | The filename to save. | `docker.save.file` or `docker.file` or `file` +| *saveClassifier* +| If set, attach the the saved archive to the project with the provided classifier. A placeholder of `%a` will be replaced with the image alias. +| `docker.save.classifier` + | *skipSave* | A boolean flag whether to skip execution of the goal. | `docker.skip.save` diff --git a/src/main/asciidoc/inc/build/_configuration.adoc b/src/main/asciidoc/inc/build/_configuration.adoc index f857e054f..44027f38a 100644 --- a/src/main/asciidoc/inc/build/_configuration.adoc +++ b/src/main/asciidoc/inc/build/_configuration.adoc @@ -25,6 +25,9 @@ https://docs.docker.com/engine/reference/api/docker_remote_api_v1.24/#build-imag | *cleanup* | Cleanup dangling (untagged) images after each build (including any containers created from them). Default is `try` which tries to remove the old image, but doesn't fail the build if this is not possible because e.g. the image is still used by a running container. Use `remove` if you want to fail the build and `none` if no cleanup is requested. +| [[context-dir]]*contextDir* +| Path to a directory used for the build's context. You can specify the `Dockerfile` to use with *dockerFile*, which by default is the Dockerfile found in the `contextDir`. The Dockerfile can be also located outside of the `contextDir`, if provided with an absolute file path. See <> for details. + | <> | A command to execute by default (i.e. if no command is provided when a container for this image is started). See <> for details. @@ -34,9 +37,8 @@ https://docs.docker.com/engine/reference/api/docker_remote_api_v1.24/#build-imag | *dockerFile* | Path to a `Dockerfile` which also triggers _Dockerfile mode_. See <> for details. -| *dockerFileDir* -| Path to a directory holding a `Dockerfile` and switch on _Dockerfile mode_. See <> for details. - +| *dockerFileDir* (_deprecated_ in favor of *<>*) +| Path to a directory holding a `Dockerfile` and switch on _Dockerfile mode_. See <> for details. _This option is deprecated in favor of _contextDir_ and will be removed for the next major release_. | *dockerArchive* | Path to a saved image archive which is then imported. See <> for details. @@ -75,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 a014635a1..b0059266f 100644 --- a/src/main/asciidoc/inc/build/_overview.adoc +++ b/src/main/asciidoc/inc/build/_overview.adoc @@ -12,9 +12,9 @@ When using this mode, the Dockerfile is created on the fly with all instructions Alternatively an external Dockerfile template or Docker archive can be used. This mode is switched on by using one of these three configuration options within * *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. -* *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. -* *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). +* *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` 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 f3d5b991b..61833b0f2 100644 --- a/src/main/asciidoc/inc/external/_property_configuration.adoc +++ b/src/main/asciidoc/inc/external/_property_configuration.adoc @@ -178,6 +178,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/AbstractDockerMojo.java b/src/main/java/io/fabric8/maven/docker/AbstractDockerMojo.java index 012457dbf..c2360ef84 100644 --- a/src/main/java/io/fabric8/maven/docker/AbstractDockerMojo.java +++ b/src/main/java/io/fabric8/maven/docker/AbstractDockerMojo.java @@ -37,11 +37,13 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Settings; +import org.apache.maven.shared.utils.logging.MessageUtils; import org.codehaus.plexus.PlexusConstants; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.context.Context; import org.codehaus.plexus.context.ContextException; import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable; +import org.fusesource.jansi.Ansi; /** * Base class for this plugin. @@ -205,34 +207,40 @@ public abstract class AbstractDockerMojo extends AbstractMojo implements Context @Override public void execute() throws MojoExecutionException, MojoFailureException { if (!skip) { - log = new AnsiLogger(getLog(), useColor, verbose, !settings.getInteractiveMode(), getLogPrefix()); - authConfigFactory.setLog(log); - imageConfigResolver.setLog(log); + boolean ansiRestore = Ansi.isEnabled(); + log = new AnsiLogger(getLog(), useColorForLogging(), verbose, !settings.getInteractiveMode(), getLogPrefix()); - LogOutputSpecFactory logSpecFactory = new LogOutputSpecFactory(useColor, logStdout, logDate); + try { + authConfigFactory.setLog(log); + imageConfigResolver.setLog(log); - ConfigHelper.validateExternalPropertyActivation(project, images); + LogOutputSpecFactory logSpecFactory = new LogOutputSpecFactory(useColor, logStdout, logDate); - DockerAccess access = null; - try { - // The 'real' images configuration to use (configured images + externally resolved images) - this.minimalApiVersion = initImageConfiguration(getBuildTimestamp()); - if (isDockerAccessRequired()) { - DockerAccessFactory.DockerAccessContext dockerAccessContext = getDockerAccessContext(); - access = dockerAccessFactory.createDockerAccess(dockerAccessContext); + ConfigHelper.validateExternalPropertyActivation(project, images); + + DockerAccess access = null; + try { + // The 'real' images configuration to use (configured images + externally resolved images) + this.minimalApiVersion = initImageConfiguration(getBuildTimestamp()); + if (isDockerAccessRequired()) { + DockerAccessFactory.DockerAccessContext dockerAccessContext = getDockerAccessContext(); + access = dockerAccessFactory.createDockerAccess(dockerAccessContext); + } + ServiceHub serviceHub = serviceHubFactory.createServiceHub(project, session, access, log, logSpecFactory); + executeInternal(serviceHub); + } catch (IOException | ExecException exp) { + logException(exp); + throw new MojoExecutionException(log.errorMessage(exp.getMessage()), exp); + } catch (MojoExecutionException exp) { + logException(exp); + throw exp; + } finally { + if (access != null) { + access.shutdown(); + } } - ServiceHub serviceHub = serviceHubFactory.createServiceHub(project, session, access, log, logSpecFactory); - executeInternal(serviceHub); - } catch (IOException | ExecException exp) { - logException(exp); - throw new MojoExecutionException(log.errorMessage(exp.getMessage()), exp); - } catch (MojoExecutionException exp) { - logException(exp); - throw exp; } finally { - if (access != null) { - access.shutdown(); - } + Ansi.setEnabled(ansiRestore); } } } @@ -302,6 +310,15 @@ protected String getLogPrefix() { return AnsiLogger.DEFAULT_LOG_PREFIX; } + /** + * Determine whether to enable colorized log messages + * @return true if log statements should be colorized + */ + private boolean useColorForLogging() { + return useColor && MessageUtils.isColorEnabled() + && !(EnvUtil.isWindows() && !EnvUtil.isMaven350OrLater(session)); + } + // Resolve and customize image configuration private String initImageConfiguration(Date buildTimeStamp) { // Resolve images diff --git a/src/main/java/io/fabric8/maven/docker/SaveMojo.java b/src/main/java/io/fabric8/maven/docker/SaveMojo.java index 7a89c2b2e..62659c8e1 100644 --- a/src/main/java/io/fabric8/maven/docker/SaveMojo.java +++ b/src/main/java/io/fabric8/maven/docker/SaveMojo.java @@ -6,6 +6,7 @@ import java.util.Properties; import io.fabric8.maven.docker.config.ArchiveCompression; +import io.fabric8.maven.docker.util.EnvUtil; import io.fabric8.maven.docker.util.ImageName; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; @@ -21,7 +22,7 @@ public class SaveMojo extends AbstractDockerMojo { // Used when not automatically determined - private final static ArchiveCompression STANDARD_ARCHIVE_COMPRESSION = ArchiveCompression.gzip; + private static final ArchiveCompression STANDARD_ARCHIVE_COMPRESSION = ArchiveCompression.gzip; @Component private MavenProjectHelper projectHelper; @@ -38,12 +39,19 @@ public class SaveMojo extends AbstractDockerMojo { @Parameter(property = "docker.skip.save", defaultValue = "false") private boolean skipSave; + @Parameter(property = "docker.save.classifier") + private String saveClassifier; + @Override protected void executeInternal(ServiceHub serviceHub) throws DockerAccessException, MojoExecutionException { - if (skipSave) { + + List images = getResolvedImages(); + if (skipSaveFor(images)) { return; } - String imageName = getImageName(); + + ImageConfiguration image = getImageToSave(images); + String imageName = image.getName(); String fileName = getFileName(imageName); ensureSaveDir(fileName); log.info("Saving image %s to %s", imageName, fileName); @@ -51,8 +59,31 @@ protected void executeInternal(ServiceHub serviceHub) throws DockerAccessExcepti throw new MojoExecutionException("No image " + imageName + " exists"); } - serviceHub.getDockerAccess().saveImage(imageName, fileName, ArchiveCompression.fromFileName(fileName)); + long time = System.currentTimeMillis(); + ArchiveCompression compression = ArchiveCompression.fromFileName(fileName); + serviceHub.getDockerAccess().saveImage(imageName, fileName, compression); + log.info("%s: Saved image to %s in %s", imageName, fileName, EnvUtil.formatDurationTill(time)); + String classifier = getClassifier(image); + if(classifier != null) { + projectHelper.attachArtifact(project, compression.getFileSuffix(), classifier, new File(fileName)); + } + } + + private boolean skipSaveFor(List images) { + if (skipSave) { + log.info("docker:save skipped because `skipSave` config is set to true"); + return true; + } + + if (saveName == null && + saveAlias == null && + images.stream().allMatch(i -> i.getBuildConfiguration() == null)) { + log.info("docker:save skipped because no image has a build configuration defined"); + return true; + } + + return false; } private String getFileName(String iName) throws MojoExecutionException { @@ -88,7 +119,7 @@ private String completeCalculatedFileName(String file) throws MojoExecutionExcep } private void ensureSaveDir(String fileName) throws MojoExecutionException { - File saveDir = new File(fileName).getParentFile(); + File saveDir = new File(fileName).getAbsoluteFile().getParentFile(); if (!saveDir.exists()) { if (!saveDir.mkdirs()) { throw new MojoExecutionException("Can not create directory " + saveDir + " for storing save file"); @@ -96,13 +127,12 @@ private void ensureSaveDir(String fileName) throws MojoExecutionException { } } - private String getImageName() throws MojoExecutionException { - List images = getResolvedImages(); + private ImageConfiguration getImageToSave(List images) throws MojoExecutionException { // specify image by name or alias if (saveName == null && saveAlias == null) { List buildImages = getImagesWithBuildConfig(images); if (buildImages.size() == 1) { - return buildImages.get(0).getName(); + return buildImages.get(0); } throw new MojoExecutionException("More than one image with build configuration is defined. Please specify the image with 'docker.name' or 'docker.alias'."); } @@ -111,12 +141,12 @@ private String getImageName() throws MojoExecutionException { } for (ImageConfiguration ic : images) { if (equalName(ic) || equalAlias(ic)) { - return ic.getName(); + return ic; } } throw new MojoExecutionException(saveName != null ? - "Can not find image with name '" + saveName + "'" : - "Can not find image with alias '"+ saveAlias + "'"); + "Can not find image with name '" + saveName + "'" : + "Can not find image with alias '"+ saveAlias + "'"); } private List getImagesWithBuildConfig(List images) { @@ -129,6 +159,15 @@ private List getImagesWithBuildConfig(List 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); + 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 da98af3bb..385cc1317 100644 --- a/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java +++ b/src/main/java/io/fabric8/maven/docker/config/ArchiveCompression.java @@ -18,7 +18,7 @@ import java.io.IOException; import java.io.OutputStream; -import java.util.zip.GZIPOutputStream; +import java.util.zip.Deflater; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; import org.codehaus.plexus.archiver.tar.TarArchiver; @@ -81,4 +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, 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 c670b48f1..a2625fc35 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 */ @@ -164,6 +172,10 @@ public boolean isDockerFileMode() { return dockerFileFile != null; } + public String getLoadNamePattern() { + return loadNamePattern; + } + public File getContextDir() { return contextDir != null ? new File(contextDir) : getDockerFile().getParentFile(); } @@ -379,6 +391,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; @@ -601,10 +618,6 @@ private File findDockerFileFile(Logger log) { } if (dockerFile != null) { - if (EnvUtil.isWindows() && !EnvUtil.isValidWindowsFileName(dockerFile)) { - throw new IllegalArgumentException(String.format("Invalid Windows file name %s for ", dockerFile)); - } - File dFile = new File(dockerFile); if (dockerFileDir == null && contextDir == null) { return dFile; 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 846e94e8c..b3614eed3 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 @@ -78,6 +78,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 77724e704..62bce11a1 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 @@ -157,6 +157,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 31033990c..e5b83789c 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 { @@ -107,14 +112,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)); @@ -154,6 +169,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/AnsiLogger.java b/src/main/java/io/fabric8/maven/docker/util/AnsiLogger.java index 99593f3f8..9387b03b1 100644 --- a/src/main/java/io/fabric8/maven/docker/util/AnsiLogger.java +++ b/src/main/java/io/fabric8/maven/docker/util/AnsiLogger.java @@ -198,8 +198,7 @@ private void flush() { } private void initializeColor(boolean useColor) { - // sl4j simple logger used by Maven seems to escape ANSI escapes on Windows - this.useAnsi = useColor && System.console() != null && !log.isDebugEnabled() && !isWindows(); + this.useAnsi = useColor && !log.isDebugEnabled(); if (useAnsi) { AnsiConsole.systemInstall(); Ansi.setEnabled(true); @@ -209,11 +208,6 @@ private void initializeColor(boolean useColor) { } } - private boolean isWindows() { - String os = System.getProperty("os.name"); - return os != null && os.toLowerCase().startsWith("windows"); - } - private void println(String txt) { System.out.println(txt); } @@ -243,20 +237,33 @@ private String format(String message, Object[] params) { // Emphasize parts encloses in "[[*]]" tags private String evaluateEmphasis(String message, Ansi.Color msgColor) { - // Split with delimiters [[.]]. See also http://stackoverflow.com/a/2206545/207604 - String prepared = message.replaceAll("\\[\\[(.)]]","[[]]$1[[]]"); - String[] parts = prepared.split("\\[\\[]]"); + // Split but keep the content by splitting on [[ and ]] separately when they + // are followed or preceded by their counterpart. This lets the split retain + // the character in the center. + String[] parts = message.split("(\\[\\[(?=.]])|(?<=\\[\\[.)]])"); if (parts.length == 1) { return message; } + // The split up string is comprised of a leading plain part, followed + // by groups of colorization that are color-part plain-part. + // To avoid emitting needless color changes, we skip the set or reset + // if the subsequent part is empty. String msgColorS = ansi().fg(msgColor).toString(); StringBuilder ret = new StringBuilder(parts[0]); - boolean colorOpen = true; - for (int i = 1; i < parts.length; i+=2) { - ret.append(colorOpen ? getEmphasisColor(parts[i]) : msgColorS); - colorOpen = !colorOpen; - if (i+1 < parts.length) { - ret.append(parts[i+1]); + + for (int i = 1; i < parts.length; i += 4) { + boolean colorPart = i + 1 < parts.length && parts[i + 1].length() > 0; + boolean plainPart = i + 3 < parts.length && parts[i + 3].length() > 0; + + if (colorPart) { + ret.append(getEmphasisColor(parts[i])); + ret.append(parts[i + 1]); + if(plainPart) { + ret.append(msgColorS); + } + } + if (plainPart) { + ret.append(parts[i + 3]); } } return ret.toString(); diff --git a/src/main/java/io/fabric8/maven/docker/util/EnvUtil.java b/src/main/java/io/fabric8/maven/docker/util/EnvUtil.java index f491e2a40..500d071b0 100644 --- a/src/main/java/io/fabric8/maven/docker/util/EnvUtil.java +++ b/src/main/java/io/fabric8/maven/docker/util/EnvUtil.java @@ -11,6 +11,7 @@ import com.google.common.base.*; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.io.FileUtils; @@ -434,33 +435,10 @@ public static boolean isWindows() { return System.getProperty("os.name").toLowerCase().contains("windows"); } - /** - * Validate that the provided filename is a valid Windows filename. - * - * The validation of the Windows filename is copied from stackoverflow: https://stackoverflow.com/a/6804755 - * - * @param filename the filename - * @return filename is a valid Windows filename - */ - public static boolean isValidWindowsFileName(String filename) { - - Pattern pattern = Pattern.compile( - "# Match a valid Windows filename (unspecified file system). \n" + - "^ # Anchor to start of string. \n" + - "(?! # Assert filename is not: CON, PRN, \n" + - " (?: # AUX, NUL, COM1, COM2, COM3, COM4, \n" + - " CON|PRN|AUX|NUL| # COM5, COM6, COM7, COM8, COM9, \n" + - " COM[1-9]|LPT[1-9] # LPT1, LPT2, LPT3, LPT4, LPT5, \n" + - " ) # LPT6, LPT7, LPT8, and LPT9... \n" + - " (?:\\.[^.]*)? # followed by optional extension \n" + - " $ # and end of string \n" + - ") # End negative lookahead assertion. \n" + - "[^<>:\"/\\\\|?*\\x00-\\x1F]* # Zero or more valid filename chars.\n" + - "[^<>:\"/\\\\|?*\\x00-\\x1F .] # Last char is not a space or dot. \n" + - "$ # Anchor to end of string. ", - Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.COMMENTS); - Matcher matcher = pattern.matcher(filename); - return matcher.matches(); + public static boolean isMaven350OrLater(MavenSession mavenSession) { + // Maven enforcer and help:evaluate goals both use mavenSession.getSystemProperties(), + // and it turns out that System.getProperty("maven.version") does not return the value. + String mavenVersion = mavenSession.getSystemProperties().getProperty("maven.version", "3"); + return greaterOrEqualsVersion(mavenVersion, "3.5.0"); } - } 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/SaveMojoTest.java b/src/test/java/io/fabric8/maven/docker/SaveMojoTest.java new file mode 100644 index 000000000..8aa347f9e --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/SaveMojoTest.java @@ -0,0 +1,331 @@ +package io.fabric8.maven.docker; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.junit.Test; + +import io.fabric8.maven.docker.access.DockerAccess; +import io.fabric8.maven.docker.access.DockerAccessException; +import io.fabric8.maven.docker.config.ArchiveCompression; +import io.fabric8.maven.docker.config.BuildImageConfiguration; +import io.fabric8.maven.docker.config.ImageConfiguration; +import io.fabric8.maven.docker.service.QueryService; +import io.fabric8.maven.docker.service.ServiceHub; +import io.fabric8.maven.docker.util.Logger; +import mockit.Deencapsulation; +import mockit.Expectations; +import mockit.Injectable; +import mockit.Mocked; +import mockit.Tested; + +public class SaveMojoTest { + + @Injectable + Logger log; + + @Tested(fullyInitialized = false) + private SaveMojo saveMojo; + + @Mocked + ServiceHub serviceHub; + + @Mocked + QueryService queryService; + + @Mocked + DockerAccess dockerAccess; + + @Mocked + MavenProject mavenProject; + + @Mocked + Build mavenBuild; + + @Mocked + MavenProjectHelper mavenProjectHelper; + + @Test + public void saveWithoutNameAliasOrFile() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image = new ImageConfiguration.Builder() + .name("example:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + queryService.hasImage("example:latest"); result = true; + dockerAccess.saveImage("example:latest", anyString, ArchiveCompression.gzip); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + }}; + + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveAndAttachWithoutNameAliasOrFile() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image = new ImageConfiguration.Builder() + .name("example:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + mavenProject.getBuild(); result = mavenBuild; + mavenBuild.getDirectory(); result = "mock-target"; + queryService.hasImage("example:latest"); result = true; + dockerAccess.saveImage("example:latest", anyString, ArchiveCompression.gzip); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + mavenProjectHelper.attachArtifact(mavenProject, "tar.gz", "archive", new File("mock-target/example-latest.tar.gz")); + }}; + + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "saveClassifier", "archive"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveWithFile() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image = new ImageConfiguration.Builder() + .name("example:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + queryService.hasImage("example:latest"); result = true; + dockerAccess.saveImage("example:latest", "destination/archive-name.tar.bz2", ArchiveCompression.bzip2); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + }}; + + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "saveFile", "destination/archive-name.tar.bz2"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveAndAttachWithFile() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image = new ImageConfiguration.Builder() + .name("example:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + queryService.hasImage("example:latest"); result = true; + dockerAccess.saveImage("example:latest", "destination/archive-name.tar.bz2", ArchiveCompression.bzip2); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + mavenProjectHelper.attachArtifact(mavenProject, "tar.bz", "archive", new File("destination/archive-name.tar.bz2")); + }}; + + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "saveFile", "destination/archive-name.tar.bz2"); + Deencapsulation.setField(saveMojo, "saveClassifier", "archive"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveWithAlias() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image1 = new ImageConfiguration.Builder() + .name("example1:latest") + .alias("example1") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + ImageConfiguration image2 = new ImageConfiguration.Builder() + .name("example2:latest") + .alias("example2") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + mavenProject.getBuild(); result = mavenBuild; + mavenProject.getVersion(); result = "1.2.3-SNAPSHOT"; + mavenBuild.getDirectory(); result = "mock-target"; + queryService.hasImage("example2:latest"); result = true; + dockerAccess.saveImage("example2:latest", "mock-target/example2-1.2.3-SNAPSHOT.tar.gz", ArchiveCompression.gzip); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + }}; + + Deencapsulation.setField(saveMojo, "images", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "resolvedImages", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "saveAlias", "example2"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveAndAttachWithAlias() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image1 = new ImageConfiguration.Builder() + .name("example1:latest") + .alias("example1") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + ImageConfiguration image2 = new ImageConfiguration.Builder() + .name("example2:latest") + .alias("example2") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + mavenProject.getBuild(); result = mavenBuild; + mavenProject.getVersion(); result = "1.2.3-SNAPSHOT"; + mavenBuild.getDirectory(); result = "mock-target"; + queryService.hasImage("example2:latest"); result = true; + dockerAccess.saveImage("example2:latest", "mock-target/example2-1.2.3-SNAPSHOT.tar.gz", ArchiveCompression.gzip); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + mavenProjectHelper.attachArtifact(mavenProject, "tar.gz", "archive-example2", new File("mock-target/example2-1.2.3-SNAPSHOT.tar.gz")); + }}; + + Deencapsulation.setField(saveMojo, "images", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "resolvedImages", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "saveAlias", "example2"); + Deencapsulation.setField(saveMojo, "saveClassifier", "archive-%a"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void saveAndAttachWithAliasButAlsoClassifier() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image1 = new ImageConfiguration.Builder() + .name("example1:latest") + .alias("example1") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + ImageConfiguration image2 = new ImageConfiguration.Builder() + .name("example2:latest") + .alias("example2") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + new Expectations() {{ + mavenProject.getProperties(); result = new Properties(); + mavenProject.getBuild(); result = mavenBuild; + mavenProject.getVersion(); result = "1.2.3-SNAPSHOT"; + mavenBuild.getDirectory(); result = "mock-target"; + queryService.hasImage("example2:latest"); result = true; + dockerAccess.saveImage("example2:latest", "mock-target/example2-1.2.3-SNAPSHOT.tar.gz", ArchiveCompression.gzip); + serviceHub.getQueryService(); result = queryService; + serviceHub.getDockerAccess(); result = dockerAccess; + mavenProjectHelper.attachArtifact(mavenProject, "tar.gz", "preferred", new File("mock-target/example2-1.2.3-SNAPSHOT.tar.gz")); + }}; + + Deencapsulation.setField(saveMojo, "images", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "resolvedImages", Arrays.asList(image1, image2)); + Deencapsulation.setField(saveMojo, "saveAlias", "example2"); + Deencapsulation.setField(saveMojo, "saveClassifier", "preferred"); + Deencapsulation.setField(saveMojo, "project", mavenProject); + Deencapsulation.setField(saveMojo, "projectHelper", mavenProjectHelper); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void noFailureWithEmptyImageList() throws DockerAccessException, MojoExecutionException { + Deencapsulation.setField(saveMojo, "images", Collections.emptyList()); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.emptyList()); + + saveMojo.executeInternal(serviceHub); + } + + @Test + public void noFailureWithEmptyBuildImageList() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image = new ImageConfiguration.Builder() + .name("example:latest") + .build(); + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(image)); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(image)); + + saveMojo.executeInternal(serviceHub); + } + + @Test(expected = MojoExecutionException.class) + public void failureWithMultipleBuildImageList() throws DockerAccessException, MojoExecutionException { + ImageConfiguration image1 = new ImageConfiguration.Builder() + .name("example1:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + ImageConfiguration image2 = new ImageConfiguration.Builder() + .name("example2:latest") + .buildConfig(new BuildImageConfiguration.Builder() + .from("scratch") + .build()) + .build(); + + List images = Arrays.asList(image1, image2); + Deencapsulation.setField(saveMojo, "images", images); + Deencapsulation.setField(saveMojo, "resolvedImages", images); + + saveMojo.executeInternal(serviceHub); + } + + @Test(expected = MojoExecutionException.class) + public void failureWithSaveAliasAndName() throws DockerAccessException, MojoExecutionException { + Deencapsulation.setField(saveMojo, "saveAlias", "not-null"); + Deencapsulation.setField(saveMojo, "saveName", "not-null"); + Deencapsulation.setField(saveMojo, "images", Collections.singletonList(new ImageConfiguration())); + Deencapsulation.setField(saveMojo, "resolvedImages", Collections.singletonList(new ImageConfiguration())); + + saveMojo.executeInternal(serviceHub); + } +} diff --git a/src/test/java/io/fabric8/maven/docker/config/BuildImageConfigurationTest.java b/src/test/java/io/fabric8/maven/docker/config/BuildImageConfigurationTest.java index 45d7728e3..3dc6a6b81 100644 --- a/src/test/java/io/fabric8/maven/docker/config/BuildImageConfigurationTest.java +++ b/src/test/java/io/fabric8/maven/docker/config/BuildImageConfigurationTest.java @@ -86,7 +86,7 @@ public void DockerfileDirAndDockerfileAlsoSetButDockerfileIsAbsoluteExceptionThr BuildImageConfiguration config = new BuildImageConfiguration.Builder(). dockerFileDir("/tmp/"). - dockerFile("/Dockerfile").build(); + dockerFile(new File("Dockerfile").getAbsolutePath()).build(); config.initAndValidate(logger); } 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/AnsiLoggerTest.java b/src/test/java/io/fabric8/maven/docker/util/AnsiLoggerTest.java index f86707970..5abc18d14 100644 --- a/src/test/java/io/fabric8/maven/docker/util/AnsiLoggerTest.java +++ b/src/test/java/io/fabric8/maven/docker/util/AnsiLoggerTest.java @@ -16,45 +16,224 @@ * limitations under the License. */ -import org.apache.maven.plugin.logging.SystemStreamLog; +import static org.junit.Assert.assertEquals; + +import org.apache.maven.monitor.logging.DefaultLog; +import org.codehaus.plexus.logging.console.ConsoleLogger; import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiConsole; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; -import static org.junit.Assert.assertEquals; - /** * @author roland * @since 07/10/16 */ public class AnsiLoggerTest { + @BeforeClass + public static void installAnsi() { + AnsiConsole.systemInstall(); + } + + @Before + public void forceAnsiPassthrough() { + // Because the AnsiConsole keeps a per-VM counter of calls to systemInstall, it is + // difficult to force it to pass through escapes to stdout during test. + // Additionally, running the test in a suite (e.g. with mvn test) means other + // code may have already initialized or manipulated the AnsiConsole. + // Hence we just reset the stdout/stderr references to those captured by AnsiConsole + // during its static initialization and restore them after tests. + System.setOut(AnsiConsole.system_out); + System.setErr(AnsiConsole.system_err); + } + + @AfterClass + public static void restoreAnsiPassthrough() { + AnsiConsole.systemUninstall(); + System.setOut(AnsiConsole.out); + System.setErr(AnsiConsole.err); + } @Test - public void emphasize() { + public void emphasizeDebug() { + TestLog testLog = new TestLog() { + @Override + public boolean isDebugEnabled() { + return true; + } + }; + + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + logger.debug("Debug messages do not interpret [[*]]%s[[*]]", "emphasis"); + assertEquals("T>Debug messages do not interpret [[*]]emphasis[[*]]", + testLog.getMessage()); + } + + @Test + public void emphasizeInfoWithDebugEnabled() { + TestLog testLog = new TestLog() { + @Override + public boolean isDebugEnabled() { + return true; + } + }; + + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + logger.info("Info messages do not apply [[*]]%s[[*]] when debug is enabled", "color codes"); + assertEquals("T>Info messages do not apply color codes when debug is enabled", + testLog.getMessage()); + } + + @Test + public void emphasizeInfo() { TestLog testLog = new TestLog(); AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); - Ansi ansi = Ansi.ansi(); - logger.info("Yet another [[*]]Test[[*]] %s","emphasis"); - assertEquals(ansi.a("T>") - .fg(AnsiLogger.COLOR_INFO) - .a("Yet another ") + Ansi ansi = new Ansi(); + logger.info("Info messages [[*]]show[[*]] %s","emphasis"); + assertEquals(ansi.fg(AnsiLogger.COLOR_INFO) + .a("T>") + .a("Info messages ") .fgBright(AnsiLogger.COLOR_EMPHASIS) - .a("Test") + .a("show") .fg(AnsiLogger.COLOR_INFO) .a(" emphasis") .reset().toString(), testLog.getMessage()); } + @Test + public void emphasizeInfoSpecificColor() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + Ansi ansi = new Ansi(); + logger.info("Specific [[C]]color[[C]] %s","is possible"); + assertEquals(ansi.fg(AnsiLogger.COLOR_INFO) + .a("T>") + .a("Specific ") + .fg(Ansi.Color.CYAN) + .a("color") + .fg(AnsiLogger.COLOR_INFO) + .a(" is possible") + .reset().toString(), + testLog.getMessage()); + } + + @Test + public void emphasizeInfoIgnoringEmpties() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + Ansi ansi = new Ansi(); + // Note that the closing part of the emphasis does not need to match the opening. + // E.g. [[b]]Blue[[*]] works just like [[b]]Blue[[b]] + logger.info("[[b]][[*]]Skip[[*]][[*]]ping [[m]]empty strings[[/]] %s[[*]][[c]][[c]][[*]]","is possible"); + assertEquals(ansi.fg(AnsiLogger.COLOR_INFO) + .a("T>") + .a("Skipping ") + .fgBright(Ansi.Color.MAGENTA) + .a("empty strings") + .fg(AnsiLogger.COLOR_INFO) + .a(" is possible") + .reset().toString(), + testLog.getMessage()); + } + + @Test + public void emphasizeInfoSpecificBrightColor() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + Ansi ansi = new Ansi(); + logger.info("Lowercase enables [[c]]bright version[[c]] of %d colors",Ansi.Color.values().length - 1); + assertEquals(ansi.fg(AnsiLogger.COLOR_INFO) + .a("T>") + .a("Lowercase enables ") + .fgBright(Ansi.Color.CYAN) + .a("bright version") + .fg(AnsiLogger.COLOR_INFO) + .a(" of 8 colors") + .reset().toString(), + testLog.getMessage()); + } + + @Test + public void emphasizeInfoWithoutColor() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, false, false, false, "T>"); + logger.info("Disabling color causes logger to [[*]]interpret and remove[[*]] %s","emphasis"); + assertEquals("T>Disabling color causes logger to interpret and remove emphasis", + testLog.getMessage()); + } + + @Test + public void emphasizeWarning() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + Ansi ansi = new Ansi(); + logger.warn("%s messages support [[*]]emphasis[[*]] too","Warning"); + assertEquals(ansi.fg(AnsiLogger.COLOR_WARNING) + .a("T>") + .a("Warning messages support ") + .fgBright(AnsiLogger.COLOR_EMPHASIS) + .a("emphasis") + .fg(AnsiLogger.COLOR_WARNING) + .a(" too") + .reset().toString(), + testLog.getMessage()); + } + + @Test + public void emphasizeError() { + TestLog testLog = new TestLog(); + AnsiLogger logger = new AnsiLogger(testLog, true, false, false, "T>"); + Ansi ansi = new Ansi(); + logger.error("Error [[*]]messages[[*]] could emphasise [[*]]%s[[*]]","many things"); + assertEquals(ansi.fg(AnsiLogger.COLOR_ERROR) + .a("T>") + .a("Error ") + .fgBright(AnsiLogger.COLOR_EMPHASIS) + .a("messages") + .fg(AnsiLogger.COLOR_ERROR) + .a(" could emphasise ") + .fgBright(AnsiLogger.COLOR_EMPHASIS) + .a("many things") + .reset() + .toString(), + testLog.getMessage()); + } + - private class TestLog extends SystemStreamLog { + private class TestLog extends DefaultLog { private String message; + public TestLog() { + super(new ConsoleLogger()); + } + + @Override + public void debug(CharSequence content) { + this.message = content.toString(); + super.debug(content); + } + @Override public void info(CharSequence content) { this.message = content.toString(); super.info(content); } + @Override + public void warn(CharSequence content) { + this.message = content.toString(); + super.warn(content); + } + + @Override + public void error(CharSequence content) { + this.message = content.toString(); + super.error(content); + } + void reset() { message = null; } diff --git a/src/test/java/io/fabric8/maven/docker/util/EnvUtilTest.java b/src/test/java/io/fabric8/maven/docker/util/EnvUtilTest.java index 1aa8d93c4..e0b3c8647 100644 --- a/src/test/java/io/fabric8/maven/docker/util/EnvUtilTest.java +++ b/src/test/java/io/fabric8/maven/docker/util/EnvUtilTest.java @@ -175,14 +175,6 @@ public void fixupPath() throws Exception { } - @Test - public void isValidWindowsFileName() { - - assertFalse(EnvUtil.isValidWindowsFileName("/Dockerfile")); - assertTrue(EnvUtil.isValidWindowsFileName("Dockerfile")); - assertFalse(EnvUtil.isValidWindowsFileName("Dockerfile/")); - } - private Properties getTestProperties(String ... vals) { Properties ret = new Properties(); for (int i = 0; i < vals.length; i+=2) { 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("^([^:]|:(?=.*:))*(?