diff --git a/docs/src/main/asciidoc/config.adoc b/docs/src/main/asciidoc/config.adoc index 181e7bf385962..3999d1e29e3d1 100644 --- a/docs/src/main/asciidoc/config.adoc +++ b/docs/src/main/asciidoc/config.adoc @@ -543,6 +543,7 @@ quarkus.datasource.jdbc.url=jdbc:mysql://${application.server}:3306/mydatabase?u It does result in one more line in this example but the value of `application.server` can be reused in other properties, diminishing the possibility of typos and providing more flexibility in property definitions. +[#combine-property-env-var] === Combining Property Expressions and Environment Variables Quarkus also supports the combination of both property expressions and environment variables. diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index a4a0da120f0c4..f4239f570723b 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -110,6 +110,22 @@ The following properties can be used to customize the container image build proc include::{generated-dir}/config/quarkus-container-image.adoc[opts=optional, leveloffset=+1] +==== Using CI Environments + +Various CI environments provide a ready to use container-image registry which can be combined with the container-image Quarkus extensions in order to +effortlessly create and push a Quarkus application to said registry. + +For example, https://gitlab.com/[GitLab] provides such a registry and in the provided CI environment, +makes available the `CI_REGISTRY_IMAGE` environment variable +(see GitLab's https://docs.gitlab.com/ee/ci/variables/[documentation]) for more information), which can be used in Quarkus like so: + +[source] +---- +quarkus.container-image.image=${CI_REGISTRY_IMAGE} +---- + +NOTE: See link:config.adoc#combine-property-env-var[this] for more information on how to combine properties with environment variables. + === Jib Options In addition to the generic container image options, the `container-image-jib` also provides the following options: diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 54277ac78d738..240b74f269bbb 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -58,9 +58,9 @@ public CapabilityBuildItem capability() { @BuildStep(onlyIf = { IsNormal.class, DockerBuild.class }, onlyIfNot = NativeBuild.class) public void dockerBuildFromJar(DockerConfig dockerConfig, - ContainerImageConfig containerImageConfig, // TODO: use to check whether we need to also push to registry + ContainerImageConfig containerImageConfig, OutputTargetBuildItem out, - ContainerImageInfoBuildItem containerImage, + ContainerImageInfoBuildItem containerImageInfo, Optional buildRequest, Optional pushRequest, BuildProducer artifactResultProducer, @@ -79,11 +79,8 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, log.info("Building docker image for jar."); - String image = containerImage.getImage(); - List additionalImageTags = containerImage.getAdditionalImageTags(); - ImageIdReader reader = new ImageIdReader(); - createContainerImage(containerImageConfig, dockerConfig, image, additionalImageTags, out, reader, false, + createContainerImage(containerImageConfig, dockerConfig, containerImageInfo, out, reader, false, pushRequest.isPresent(), packageConfig); artifactResultProducer.produce(new ArtifactResultBuildItem(null, "jar-container", Collections.emptyMap())); @@ -117,22 +114,19 @@ public void dockerBuildFromNativeImage(DockerConfig dockerConfig, log.info("Starting docker image build"); - String image = containerImage.getImage(); - List additionalImageTags = containerImage.getAdditionalImageTags(); - ImageIdReader reader = new ImageIdReader(); - createContainerImage(containerImageConfig, dockerConfig, image, additionalImageTags, out, reader, true, + createContainerImage(containerImageConfig, dockerConfig, containerImage, out, reader, true, pushRequest.isPresent(), packageConfig); artifactResultProducer.produce(new ArtifactResultBuildItem(null, "native-container", Collections.emptyMap())); } - private void createContainerImage(ContainerImageConfig containerImageConfig, DockerConfig dockerConfig, String image, - List additionalImageTags, + private void createContainerImage(ContainerImageConfig containerImageConfig, DockerConfig dockerConfig, + ContainerImageInfoBuildItem containerImageInfo, OutputTargetBuildItem out, ImageIdReader reader, boolean forNative, boolean pushRequested, PackageConfig packageConfig) { DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, forNative, packageConfig, out); - String[] dockerArgs = getDockerArgs(image, dockerfilePaths, dockerConfig); + String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, dockerConfig); log.infof("Executing the following command to build docker image: '%s %s'", DOCKER_BINARY_NAME, String.join(" ", dockerArgs)); boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, DOCKER_BINARY_NAME, dockerArgs); @@ -140,18 +134,18 @@ private void createContainerImage(ContainerImageConfig containerImageConfig, Doc throw dockerException(dockerArgs); } - log.infof("Built container image %s (%s)\n", image, reader.getImageId()); + log.infof("Built container image %s (%s)\n", containerImageInfo.getImage(), reader.getImageId()); - if (!additionalImageTags.isEmpty()) { - createAdditionalTags(image, additionalImageTags); + if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { + createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags()); } if (pushRequested || containerImageConfig.push) { String registry = "docker.io"; - if (!containerImageConfig.registry.isPresent()) { + if (!containerImageInfo.getRegistry().isPresent()) { log.info("No container image registry was set, so 'docker.io' will be used"); } else { - registry = containerImageConfig.registry.get(); + registry = containerImageInfo.getRegistry().get(); } // Check if we need to login first if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { @@ -162,8 +156,8 @@ private void createContainerImage(ContainerImageConfig containerImageConfig, Doc } } - List imagesToPush = new ArrayList<>(additionalImageTags); - imagesToPush.add(image); + List imagesToPush = new ArrayList<>(containerImageInfo.getAdditionalImageTags()); + imagesToPush.add(containerImageInfo.getImage()); for (String imageToPush : imagesToPush) { pushImage(imageToPush); } diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 91e7064a90c62..4651732ef4866 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -35,6 +35,7 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import io.quarkus.bootstrap.util.ZipUtils; +import io.quarkus.builder.Version; import io.quarkus.container.image.deployment.ContainerImageConfig; import io.quarkus.container.image.deployment.util.NativeBinaryUtil; import io.quarkus.container.spi.ContainerImageBuildRequestBuildItem; @@ -46,7 +47,6 @@ import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.pkg.PackageConfig; @@ -76,7 +76,7 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji ContainerImageInfoBuildItem containerImage, JarBuildItem sourceJar, MainClassBuildItem mainClass, - OutputTargetBuildItem outputTarget, ApplicationInfoBuildItem applicationInfo, + OutputTargetBuildItem outputTarget, Optional buildRequest, Optional pushRequest, List containerImageLabels, @@ -99,7 +99,7 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji "Package type '" + packageType + "' is not supported by the container-image-jib extension"); } handleExtraFiles(outputTarget, jibContainerBuilder); - containerize(applicationInfo, containerImageConfig, containerImage, jibContainerBuilder, + containerize(containerImageConfig, containerImage, jibContainerBuilder, pushRequest.isPresent()); artifactResultProducer.produce(new ArtifactResultBuildItem(null, "jar-container", Collections.emptyMap())); @@ -109,7 +109,6 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji public void buildFromNative(ContainerImageConfig containerImageConfig, JibConfig jibConfig, ContainerImageInfoBuildItem containerImage, NativeImageBuildItem nativeImage, - ApplicationInfoBuildItem applicationInfo, OutputTargetBuildItem outputTarget, Optional buildRequest, Optional pushRequest, @@ -129,15 +128,15 @@ public void buildFromNative(ContainerImageConfig containerImageConfig, JibConfig JibContainerBuilder jibContainerBuilder = createContainerBuilderFromNative(containerImageConfig, jibConfig, nativeImage, containerImageLabels); handleExtraFiles(outputTarget, jibContainerBuilder); - containerize(applicationInfo, containerImageConfig, containerImage, jibContainerBuilder, + containerize(containerImageConfig, containerImage, jibContainerBuilder, pushRequest.isPresent()); artifactResultProducer.produce(new ArtifactResultBuildItem(null, "native-container", Collections.emptyMap())); } - private JibContainer containerize(ApplicationInfoBuildItem applicationInfo, ContainerImageConfig containerImageConfig, + private JibContainer containerize(ContainerImageConfig containerImageConfig, ContainerImageInfoBuildItem containerImage, JibContainerBuilder jibContainerBuilder, boolean pushRequested) { - Containerizer containerizer = createContainerizer(containerImageConfig, containerImage, applicationInfo, pushRequested); + Containerizer containerizer = createContainerizer(containerImageConfig, containerImage, pushRequested); for (String additionalTag : containerImage.getAdditionalTags()) { containerizer.withAdditionalTag(additionalTag); } @@ -156,16 +155,10 @@ private JibContainer containerize(ApplicationInfoBuildItem applicationInfo, Cont private Containerizer createContainerizer(ContainerImageConfig containerImageConfig, ContainerImageInfoBuildItem containerImage, - ApplicationInfoBuildItem applicationInfo, boolean pushRequested) { + boolean pushRequested) { Containerizer containerizer; - ImageReference imageReference = getImageReference(containerImageConfig, containerImage, applicationInfo); - - for (String additionalTag : containerImage.getAdditionalTags()) { - if (!ImageReference.isValidTag(additionalTag)) { - throw new IllegalArgumentException( - "The supplied container-image additional tag '" + additionalTag + "' is invalid"); - } - } + ImageReference imageReference = ImageReference.of(containerImage.getRegistry().orElse(null), + containerImage.getRepository(), containerImage.getTag()); if (pushRequested || containerImageConfig.push) { if (!containerImageConfig.registry.isPresent()) { @@ -178,6 +171,7 @@ private Containerizer createContainerizer(ContainerImageConfig containerImageCon containerizer = Containerizer.to(DockerDaemonImage.named(imageReference)); } containerizer.setToolName("Quarkus"); + containerizer.setToolVersion(Version.getVersion()); containerizer.addEventHandler(LogEvent.class, (e) -> { if (!e.getMessage().isEmpty()) { log.log(toJBossLoggingLevel(e.getLevel()), e.getMessage()); @@ -212,29 +206,6 @@ private Logger.Level toJBossLoggingLevel(LogEvent.Level level) { } } - private ImageReference getImageReference(ContainerImageConfig containerImageConfig, - ContainerImageInfoBuildItem containerImage, - ApplicationInfoBuildItem applicationInfo) { - - String registry = containerImageConfig.registry.orElse(null); - if ((registry != null) && !ImageReference.isValidRegistry(registry)) { - throw new IllegalArgumentException("The supplied container-image registry '" + registry + "' is invalid"); - } - - String repository = (containerImageConfig.getEffectiveGroup().map(s -> s + "/").orElse("")) - + containerImageConfig.name.orElse(applicationInfo.getName()); - if (!ImageReference.isValidRepository(repository)) { - throw new IllegalArgumentException("The supplied container-image repository '" + repository + "' is invalid"); - } - - final String tag = containerImage.getTag(); - if (!ImageReference.isValidTag(tag)) { - throw new IllegalArgumentException("The supplied container-image tag '" + tag + "' is invalid"); - } - - return ImageReference.of(registry, repository, tag); - } - /** * We don't use Jib's JavaContainerBuilder here because we need to support the custom fast-jar format * We create the following layers (least likely to change to most likely to change): diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java index edc379e8753e4..465dbd64ea837 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java @@ -39,6 +39,14 @@ public class ContainerImageConfig { @ConfigItem public Optional registry; + /** + * Represents the entire image string. + * If set, then {@code group}, {@code name}, {@code registry}, {@code tags}, {@code additionalTags} + * are ignored + */ + @ConfigItem + public Optional image; + /** * The username to use to authenticate with the registry where the built image will be pushed */ diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageProcessor.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageProcessor.java new file mode 100644 index 0000000000000..dd6670868f4f5 --- /dev/null +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageProcessor.java @@ -0,0 +1,64 @@ +package io.quarkus.container.image.deployment; + +import java.util.Collections; +import java.util.Optional; + +import io.quarkus.container.spi.ContainerImageInfoBuildItem; +import io.quarkus.container.spi.ImageReference; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; + +public class ContainerImageProcessor { + + @BuildStep + public ContainerImageInfoBuildItem publishImageInfo(ApplicationInfoBuildItem app, + ContainerImageConfig containerImageConfig, Capabilities capabilities) { + + ensureSingleContainerImageExtension(capabilities); + + // additionalTags are used even containerImageConfig.image is set because that string cannot contain multiple tags + if (containerImageConfig.additionalTags.isPresent()) { + for (String additionalTag : containerImageConfig.additionalTags.get()) { + if (!ImageReference.isValidTag(additionalTag)) { + throw new IllegalArgumentException( + "The supplied additional container-image tag '" + additionalTag + "' is invalid"); + } + } + } + + // if the user supplied the entire image string, use it + if (containerImageConfig.image.isPresent()) { + ImageReference imageReference = ImageReference.parse(containerImageConfig.image.get()); + String repository = imageReference.getRepository(); + return new ContainerImageInfoBuildItem(Optional.of(imageReference.getRegistry()), repository, + imageReference.getTag(), containerImageConfig.additionalTags.orElse(Collections.emptyList())); + } + + String registry = containerImageConfig.registry.orElse(null); + if ((registry != null) && !ImageReference.isValidRegistry(registry)) { + throw new IllegalArgumentException("The supplied container-image registry '" + registry + "' is invalid"); + } + + String effectiveName = containerImageConfig.name.orElse(app.getName()); + String repository = (containerImageConfig.getEffectiveGroup().map(s -> s + "/").orElse("")) + effectiveName; + if (!ImageReference.isValidRepository(repository)) { + throw new IllegalArgumentException("The supplied combination of container-image group '" + + containerImageConfig.getEffectiveGroup().orElse("") + "' and name '" + effectiveName + "' is invalid"); + } + + final String effectiveTag = containerImageConfig.tag.orElse(app.getVersion()); + if (!ImageReference.isValidTag(effectiveTag)) { + throw new IllegalArgumentException("The supplied container-image tag '" + effectiveTag + "' is invalid"); + } + + return new ContainerImageInfoBuildItem(containerImageConfig.registry, + containerImageConfig.getEffectiveGroup(), + effectiveName, effectiveTag, + containerImageConfig.additionalTags.orElse(Collections.emptyList())); + } + + private void ensureSingleContainerImageExtension(Capabilities capabilities) { + ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities); + } +} diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerProcessor.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerProcessor.java deleted file mode 100644 index 62411055b8b6e..0000000000000 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerProcessor.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkus.container.image.deployment; - -import java.util.Collections; - -import io.quarkus.container.spi.ContainerImageInfoBuildItem; -import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; - -public class ContainerProcessor { - - @BuildStep - public ContainerImageInfoBuildItem publishImageInfo(ApplicationInfoBuildItem app, - ContainerImageConfig containerImageConfig, Capabilities capabilities) { - - ensureSingleContainerImageExtension(capabilities); - - return new ContainerImageInfoBuildItem(containerImageConfig.registry, - containerImageConfig.getEffectiveGroup(), - containerImageConfig.name.orElse(app.getName()), - containerImageConfig.tag.orElse(app.getVersion()), - containerImageConfig.additionalTags.orElse(Collections.emptyList())); - } - - private void ensureSingleContainerImageExtension(Capabilities capabilities) { - ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities); - } -} diff --git a/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ContainerImageInfoBuildItem.java b/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ContainerImageInfoBuildItem.java index 7e6f5b2895696..cf49ccf9c618f 100644 --- a/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ContainerImageInfoBuildItem.java +++ b/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ContainerImageInfoBuildItem.java @@ -20,19 +20,34 @@ public final class ContainerImageInfoBuildItem extends SimpleBuildItem { public final Optional registry; private final String imagePrefix; + private final String repository; private final String tag; private final Set additionalTags; - public ContainerImageInfoBuildItem(Optional registry, Optional group, String name, String tag, - List additionalTags) { + public ContainerImageInfoBuildItem(Optional registry, String repository, String tag, List additionalTags) { this.registry = registry; + this.repository = repository; StringBuilder sb = new StringBuilder(); registry.ifPresent(r -> sb.append(r).append(SLASH)); - group.ifPresent(s -> sb.append(s).append(SLASH)); - this.imagePrefix = sb.append(name).toString(); + sb.append(repository); + this.imagePrefix = sb.toString(); + this.tag = tag; + this.additionalTags = new HashSet<>(additionalTags); + } + + public ContainerImageInfoBuildItem(Optional registry, Optional group, String name, String tag, + List additionalTags) { + this.registry = registry; + + StringBuilder imagePrefixSB = new StringBuilder(); + StringBuilder repositorySB = new StringBuilder(); + registry.ifPresent(r -> imagePrefixSB.append(r).append(SLASH)); + group.ifPresent(s -> repositorySB.append(s).append(SLASH)); + repositorySB.append(name); + this.imagePrefix = imagePrefixSB.append(this.repository = repositorySB.toString()).toString(); this.tag = tag; this.additionalTags = new HashSet<>(additionalTags); } @@ -56,4 +71,8 @@ public List getAdditionalImageTags() { public Set getAdditionalTags() { return additionalTags; } + + public String getRepository() { + return repository; + } } diff --git a/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ImageReference.java b/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ImageReference.java new file mode 100644 index 0000000000000..fc2db64e875f2 --- /dev/null +++ b/extensions/container-image/spi/src/main/java/io/quarkus/container/spi/ImageReference.java @@ -0,0 +1,214 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.quarkus.container.spi; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Strings; + +/** + * This is basically a simplified version of {@code com.google.cloud.tools.jib.api.ImageReference} + */ +public class ImageReference { + + private static final String DOCKER_HUB_REGISTRY = "registry-1.docker.io"; + private static final String DEFAULT_TAG = "latest"; + private static final String LIBRARY_REPOSITORY_PREFIX = "library/"; + + /** + * Matches all sequences of alphanumeric characters possibly separated by any number of dashes in + * the middle. + */ + private static final String REGISTRY_COMPONENT_REGEX = "(?:[a-zA-Z\\d]|(?:[a-zA-Z\\d][a-zA-Z\\d-]*[a-zA-Z\\d]))"; + + /** + * Matches sequences of {@code REGISTRY_COMPONENT_REGEX} separated by a dot, with an optional + * {@code :port} at the end. + */ + private static final String REGISTRY_REGEX = String.format("%s(?:\\.%s)*(?::\\d+)?", REGISTRY_COMPONENT_REGEX, + REGISTRY_COMPONENT_REGEX); + + /** + * Matches all sequences of alphanumeric characters separated by a separator. + * + *

+ * A separator is either an underscore, a dot, two underscores, or any number of dashes. + */ + private static final String REPOSITORY_COMPONENT_REGEX = "[a-z\\d]+(?:(?:[_.]|__|-+)[a-z\\d]+)*"; + + /** Matches all repetitions of {@code REPOSITORY_COMPONENT_REGEX} separated by a backslash. */ + private static final String REPOSITORY_REGEX = String.format("(?:%s/)*%s", REPOSITORY_COMPONENT_REGEX, + REPOSITORY_COMPONENT_REGEX); + + /** Matches a tag of max length 128. */ + private static final String TAG_REGEX = "[\\w][\\w.-]{0,127}"; + + /** Pattern matches a SHA-256 hash - 32 bytes in lowercase hexadecimal. */ + private static final String HASH_REGEX = String.format("[a-f0-9]{%d}", 64); + + /** The algorithm prefix for the digest string. */ + private static final String DIGEST_PREFIX = "sha256:"; + + /** Pattern matches a SHA-256 digest - a SHA-256 hash prefixed with "sha256:". */ + private static final String DIGEST_REGEX = DIGEST_PREFIX + HASH_REGEX; + + /** + * Matches a full image reference, which is the registry, repository, and tag/digest separated by + * backslashes. The repository is required, but the registry and tag/digest are optional. + */ + private static final String REFERENCE_REGEX = String.format( + "^(?:(%s)/)?(%s)(?::(%s))?(?:@(%s))?$", + REGISTRY_REGEX, REPOSITORY_REGEX, TAG_REGEX, DIGEST_REGEX); + + private static final Pattern REFERENCE_PATTERN = Pattern.compile(REFERENCE_REGEX); + + private final String registry; + private final String repository; + private final String tag; + private final String digest; + + /** + * Returns {@code true} if {@code registry} is a valid registry string. For example, a valid + * registry could be {@code gcr.io} or {@code localhost:5000}. + * + * @param registry the registry to check + * @return {@code true} if is a valid registry; {@code false} otherwise + */ + public static boolean isValidRegistry(String registry) { + return registry.matches(REGISTRY_REGEX); + } + + /** + * Returns {@code true} if {@code repository} is a valid repository string. For example, a valid + * repository could be {@code distroless} or {@code my/container-image/repository}. + * + * @param repository the repository to check + * @return {@code true} if is a valid repository; {@code false} otherwise + */ + public static boolean isValidRepository(String repository) { + return repository.matches(REPOSITORY_REGEX); + } + + /** + * Returns {@code true} if {@code tag} is a valid tag string. For example, a valid tag could be + * {@code v120.5-release}. + * + * @param tag the tag to check + * @return {@code true} if is a valid tag; {@code false} otherwise + */ + public static boolean isValidTag(String tag) { + return tag.matches(TAG_REGEX); + } + + /** + * Parses a string {@code reference} into an {@link ImageReference}. + * + *

+ * Image references should generally be in the form: {@code /:} For + * example, an image reference could be {@code gcr.io/distroless/java:debug}. + * + *

+ * See https://docs.docker.com/engine/reference/commandline/tag/#extended-description + * for a description of valid image reference format. Note, however, that the image reference is + * referred confusingly as {@code tag} on that page. + * + * @param reference the string to parse + * @return an {@link ImageReference} parsed from the string + * @throws IllegalArgumentException if {@code reference} is formatted incorrectly + */ + public static ImageReference parse(String reference) { + + Matcher matcher = REFERENCE_PATTERN.matcher(reference); + + if (!matcher.find() || matcher.groupCount() < 4) { + throw new IllegalArgumentException("Reference " + reference + " is invalid"); + } + + String registry = matcher.group(1); + String repository = matcher.group(2); + String tag = matcher.group(3); + String digest = matcher.group(4); + + // If no registry was matched, use Docker Hub by default. + if (Strings.isNullOrEmpty(registry)) { + registry = DOCKER_HUB_REGISTRY; + } + + if (Strings.isNullOrEmpty(repository)) { + throw new IllegalArgumentException("Reference " + reference + " is invalid: The repository was not set"); + } + /* + * If a registry was matched but it does not contain any dots or colons, it should actually be + * part of the repository unless it is "localhost". + * + * See https://github.com/docker/distribution/blob/245ca4659e09e9745f3cc1217bf56e946509220c/reference/normalize.go#L62 + */ + if (!registry.contains(".") && !registry.contains(":") && !"localhost".equals(registry)) { + repository = registry + "/" + repository; + registry = DOCKER_HUB_REGISTRY; + } + + /* + * For Docker Hub, if the repository is only one component, then it should be prefixed with + * 'library/'. + * + * See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-from-docker-hub + */ + if (DOCKER_HUB_REGISTRY.equals(registry) && repository.indexOf('/') < 0) { + repository = LIBRARY_REPOSITORY_PREFIX + repository; + } + + if (Strings.isNullOrEmpty(tag) && Strings.isNullOrEmpty(digest)) { + tag = DEFAULT_TAG; + } + if (Strings.isNullOrEmpty(tag)) { + tag = null; + } + if (Strings.isNullOrEmpty(digest)) { + digest = null; + } + + return new ImageReference(registry, repository, tag, digest); + } + + private ImageReference( + String registry, String repository, String tag, String digest) { + this.registry = registry; + this.repository = repository; + this.tag = tag; + this.digest = digest; + } + + public String getRegistry() { + return registry; + } + + public String getRepository() { + return repository; + } + + public String getTag() { + return tag; + } + + public String getDigest() { + return digest; + } +} diff --git a/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/MultipleContainerImageExtensionTest.java b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/MultipleContainerImageExtensionTest.java index 2cd26e194dffa..31f75197dc047 100644 --- a/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/MultipleContainerImageExtensionTest.java +++ b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/MultipleContainerImageExtensionTest.java @@ -18,7 +18,7 @@ public class MultipleContainerImageExtensionTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("MultipleContainerImage") + .setApplicationName("multiple-container-image") .setApplicationVersion("0.1-SNAPSHOT") .setExpectedException(IllegalStateException.class) .setForcedDependencies( diff --git a/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/SingleContainerImageExtensionTest.java b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/SingleContainerImageExtensionTest.java index 812fa037f1930..f1764c2066362 100644 --- a/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/SingleContainerImageExtensionTest.java +++ b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/SingleContainerImageExtensionTest.java @@ -20,7 +20,7 @@ public class SingleContainerImageExtensionTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("SingleContainerImage") + .setApplicationName("single-container-image") .setApplicationVersion("0.1-SNAPSHOT") .setForcedDependencies( Collections.singletonList( diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGitlabLikeImageTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGitlabLikeImageTest.java new file mode 100644 index 0000000000000..27bef2daec67c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithGitlabLikeImageTest.java @@ -0,0 +1,65 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithGitlabLikeImageTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("gitlab-like-image-test") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-with-gitlab-like-image.properties") + .setLogFileName("k8s.log") + .setForcedDependencies( + Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-container-image-jib", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("gitlab-like-image-test"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getContainers()) + .singleElement().satisfies(c -> { + assertThat(c.getImage()).isEqualTo("gitlab.acme.org:1111/group/subgroup/project:latest"); + }); + }); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-gitlab-like-image.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-gitlab-like-image.properties new file mode 100644 index 0000000000000..c2122ab28c8e4 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-gitlab-like-image.properties @@ -0,0 +1,2 @@ +quarkus.kubernetes.deployment-target=kubernetes +quarkus.container-image.image=gitlab.acme.org:1111/group/subgroup/project