From 693c162d0d5457b5898c4b1ac7d711e2a3ffde1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=A4der?= Date: Tue, 18 Oct 2022 11:17:38 +0200 Subject: [PATCH] feat: Add compatibility with SemVer versions (#2301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Mäder --- CHANGELOG.md | 2 + .../src/main/asciidoc/inc/image/_naming.adoc | 31 ++++-- .../docker/helper/ImageNameFormatter.java | 100 +++++++++++++++--- .../docker/helper/ImageNameFormatterTest.java | 96 +++++++++++++++++ .../src/main/asciidoc/inc/image/_naming.adoc | 32 ++++-- 5 files changed, 235 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe27853d4..35a2fac15b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Usage: * Fix #2219: Kind/Filename mappings include optional apiVersion configuration * Fix #2224: Quarkus native base image read from properties (configurable) * Fix #2228: Quarkus native base image uses UBI 8.7 +* Fix #2301: Add compatibility with SemVer versions * Fix #2302 Bump Kubernetes Client version to 6.8.0 ### 1.13.1 (2023-06-16) @@ -499,3 +500,4 @@ Only the set of documented features are available to users. ### 0.1.0 (2019-12-19) * Initial release + diff --git a/gradle-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc b/gradle-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc index 5706d4b5bc..c7cc797e66 100644 --- a/gradle-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc +++ b/gradle-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc @@ -1,5 +1,8 @@ [[image-name]] -When specifying the image name in the configuration with the `name` field you can use several placeholders which are replaced during runtime by this plugin. In addition you can use regular Gradle properties which are resolved by Gradle itself. +When specifying the image name in the configuration with the `` field, then you can use several placeholders. +These placeholders are replaced during the execution by this plugin. +In addition, you can use regular Gradle properties. +These properties are resolved by Gradle itself. .Image Names [cols="1,5"] @@ -7,17 +10,33 @@ When specifying the image name in the configuration with the `name` field you ca | Placeholder | Description | *%g* -| The last part of the Gradle group name, sanitized so that it can be used as username on GitHub. Only the part after the last dot is used. E.g. for a group id `org.eclipse.jkube` this placeholder would insert `jkube` +| The last part of the Gradle group name. +The name gets sanitized, so that it can be used as username on GitHub. +Only the part after the last dot is used. +For example, given the group id `org.eclipse.jkube`, this placeholder would insert `jkube`. | *%a* -| A sanitized version of the artefact id so that it can be used as part of an Docker image name. I.e. it is converted to all lower case (as required by Docker) +| A sanitized version of the artefact id, so that it can be used as part of a Docker image name. +This means primarily, that it is converted to all lower case (as required by Docker). | *%v* -| The project version. +| A sanitized version of the project version. Replaces `+` with `-` in `${project.version}` to comply with the Docker tag convention. +(A different replacement symbol can be defined by setting the `jkube.image.tag.semver_plus_substitution` property.) +For example, the version '1.2.3b' becomes the exact same Docker tag, '1.2.3b'. +But '1.2.3+internal' becomes the `1.2.3-internal` Docker tag. | *%l* -| If the project version ends with `-SNAPSHOT` then this placeholder is `latest`, otherwise its the full version (same as `%v`) +| If the https://semver.org/spec/v2.0.0.html#spec-item-9[pre-release part] of the project version ends with `-SNAPSHOT`, then this placeholder resolves to `latest`. +Otherwise, it's the same as `%v`. + +If the `${project.version}` contains a https://semver.org/spec/v2.0.0.html#spec-item-10[build metadata part] (i.e. everything after the `+`), then the `+` is substituted and the rest is appended. +For example, the project version `1.2.3-SNAPSHOT+internal` becomes the `latest-internal` Docker tag. | *%t* -| If the project version ends with `-SNAPSHOT` this placeholder resolves to `snapshot-` where timestamp has the date format `yyMMdd-HHmmss-SSSS` (eg `snapshot-`). This feature is especially useful during development in oder to avoid conflicts when images are to be updated which are still in use. You need to take care yourself of cleaning up old images afterwards, though. +| If the project version ends with `-SNAPSHOT`, this placeholder resolves to `snapshot-` where timestamp has the date format `yyMMdd-HHmmss-SSSS` (eg `snapshot-`). +This feature is especially useful during development in oder to avoid conflicts when images are to be updated which are still in use. +You need to take care yourself of cleaning up old images afterwards, though. + +If the `${project.version}` contains a https://semver.org/spec/v2.0.0.html#spec-item-10[build metadata part] (i.e. everything after the `+`), then the `+` is substituted and the rest is appended. +For example, the project version `1.2.3-SNAPSHOT+internal` becomes the `snapshot-221018-113000-0000-internal` Docker tag. |=== diff --git a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatter.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatter.java index 4a25ac4447..e652694776 100644 --- a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatter.java +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatter.java @@ -13,9 +13,6 @@ */ package org.eclipse.jkube.kit.build.service.docker.helper; -import com.google.common.base.Strings; -import org.eclipse.jkube.kit.common.JavaProject; - import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; @@ -24,6 +21,9 @@ import static org.eclipse.jkube.kit.common.JKubeFileInterpolator.DEFAULT_FILTER; import static org.eclipse.jkube.kit.common.JKubeFileInterpolator.interpolate; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jkube.kit.common.JavaProject; + /** * Replace placeholders in an image name with certain properties found in the * project @@ -38,6 +38,15 @@ public class ImageNameFormatter implements ConfigHelper.NameFormatter { * Used with format modifier %g */ public static final String DOCKER_IMAGE_USER = "jkube.image.user"; + /** + * Property to lookup the replacement symbol for SemVer's '+', which is not allowed + * in the tag component of a complete container name. + */ + public static final String SEMVER_PLUS_SUBSTITUTION = "jkube.image.tag.semver_plus_substitution"; + /** + * Default value when {@link ImageNameFormatter#SEMVER_PLUS_SUBSTITUTION} is undefined. + */ + public static final String DEFAULT_SEMVER_PLUS_SUBSTITUTE = "-"; private final FormatParameterReplacer formatParamReplacer; @@ -93,6 +102,10 @@ private AbstractLookup(JavaProject project) { protected String getProperty(String key) { return project.getProperties().getProperty(key); } + + protected String getProperty(String key, String defaultValue) { + return project.getProperties().getProperty(key, defaultValue); + } } @@ -156,23 +169,84 @@ private DefaultTagLookup(JavaProject project, Mode mode, Date now) { } public String lookup() { - final String tag = getProperty(DOCKER_IMAGE_TAG); - if (!Strings.isNullOrEmpty(tag)) { - return tag; - } else if (project.isSnapshot() && mode == Mode.SNAPSHOT_WITH_TIMESTAMP) { - return "snapshot-" + new SimpleDateFormat("yyMMdd-HHmmss-SSSS").format(now); - } else if (project.isSnapshot() && mode == Mode.SNAPSHOT_LATEST) { - return "latest"; + final String userProvidedTag = getProperty(DOCKER_IMAGE_TAG); + if (!StringUtils.isBlank(userProvidedTag)) { + return userProvidedTag; + } + + String plusSubstitute = getProperty(SEMVER_PLUS_SUBSTITUTION, "-").trim(); + if ("+".equals(plusSubstitute)) { + plusSubstitute = DEFAULT_SEMVER_PLUS_SUBSTITUTE; + } + + String tag = generateTag(plusSubstitute); + return sanitizeTag(tag, plusSubstitute); + } + + private String generateTag(String plusSubstitute) { + final String version = project.getVersion(); + if (mode == Mode.PLAIN) { + return version; + } + + final String prerelease; + final String buildmetadata; + + final int indexOfPlus = version.indexOf('+'); + if (indexOfPlus >= 0) { + prerelease = version.substring(0, indexOfPlus); + buildmetadata = plusSubstitute + version.substring(indexOfPlus + 1); // '+' is not allowed in a container tag + } else { + prerelease = version; + buildmetadata = ""; + } + + if (!prerelease.endsWith("-SNAPSHOT")) { + return version; + } + + switch (mode) { + case SNAPSHOT_WITH_TIMESTAMP: + return "snapshot-" + new SimpleDateFormat("yyMMdd-HHmmss-SSSS").format(now) + buildmetadata; + case SNAPSHOT_LATEST: + return "latest" + buildmetadata; + default: + throw new IllegalStateException("mode is '" + mode.name() + "', which is not implemented."); + } + } + + private static String sanitizeTag(String tagName, String plusSubstitute) { + StringBuilder ret = new StringBuilder(tagName.length()); + + for (char c : tagName.toCharArray()) { + final boolean wordCharacter = Character.isLetterOrDigit(c) || '_' == c; // matches '\w' + if (wordCharacter || '.' == c) { + ret.append(c); + continue; + } + + if ('-' == c) { + ret.append(c); + continue; + } + + if ('+' == c) { + ret.append(plusSubstitute); + continue; + } + + ret.append('-'); } - return project.getVersion(); + + return ret.length() <= 127 ? ret.toString() : ret.substring(0, 128); } } // ========================================================================================== - // See also ImageConfiguration#doValidate() + // See also ImageName#doValidate() private static String sanitizeName(String name) { - StringBuilder ret = new StringBuilder(); + StringBuilder ret = new StringBuilder(name.length()); int underscores = 0; boolean lastWasADot = false; for (char c : name.toCharArray()) { diff --git a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatterTest.java b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatterTest.java index 435b08f4f8..498b4a3ade 100644 --- a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatterTest.java +++ b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/helper/ImageNameFormatterTest.java @@ -15,6 +15,7 @@ import org.eclipse.jkube.kit.common.JavaProject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.eclipse.jkube.kit.config.image.ImageName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -25,6 +26,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -33,6 +35,7 @@ */ class ImageNameFormatterTest { private JavaProject project; + private ImageNameFormatter formatter; @BeforeEach @@ -112,6 +115,95 @@ void tagWithNonSnapshotArtifact() { assertThat(formatter.format("%g/%a:%t")).isEqualTo("fabric8/docker-maven-plugin:1.2.3"); } + @Test + void snapshotVersion() { + project.setArtifactId("kubernetes-maven-plugin"); + project.setGroupId("org.eclipse.jkube"); + project.setVersion("1.2.3-SNAPSHOT"); + project.setProperties(new Properties()); + assertThat(formatter.format("%g/%a:%l")) + .isEqualTo("jkube/kubernetes-maven-plugin:latest") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%v")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3-SNAPSHOT") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%t")) + .matches("^jkube/kubernetes-maven-plugin:snapshot-\\d{6}-\\d{6}-\\d{4}$") + .satisfies(ImageNameFormatterTest::validImageName); + } + + @Test + void snapshotVersionWithSemVerBuildmetadata() { + project.setArtifactId("kubernetes-maven-plugin"); + project.setGroupId("org.eclipse.jkube"); + project.setVersion("1.2.3-SNAPSHOT+semver.build_meta-data"); + project.setProperties(new Properties()); + assertThat(formatter.format("%g/%a:%l")) + .isEqualTo("jkube/kubernetes-maven-plugin:latest-semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%v")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3-SNAPSHOT-semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%t")) + .matches("^jkube/kubernetes-maven-plugin:snapshot-\\d{6}-\\d{6}-\\d{4}-semver\\.build_meta-data$") + .satisfies(ImageNameFormatterTest::validImageName); + } + + @Test + void plusSubstitute() { + final Properties properties = new Properties(); + properties.put(ImageNameFormatter.SEMVER_PLUS_SUBSTITUTION, "_"); + project.setArtifactId("kubernetes-maven-plugin"); + project.setGroupId("org.eclipse.jkube"); + project.setVersion("1.2.3+semver.build_meta-data"); + project.setProperties(properties); + assertThat(formatter.format("%g/%a:%l")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3_semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%v")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3_semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%t")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3_semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + } + + @Test + void plusSubstituteIsPlus() { + final Properties properties = new Properties(); + properties.put(ImageNameFormatter.SEMVER_PLUS_SUBSTITUTION, "+"); + project.setArtifactId("kubernetes-maven-plugin"); + project.setGroupId("org.eclipse.jkube"); + project.setVersion("1.2.3+semver.build_meta-data"); + project.setProperties(properties); + assertThat(formatter.format("%g/%a:%l")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3-semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%v")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3-semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%t")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3-semver.build_meta-data") + .satisfies(ImageNameFormatterTest::validImageName); + } + + @Test + void releaseVersion() { + project.setArtifactId("kubernetes-maven-plugin"); + project.setGroupId("org.eclipse.jkube"); + project.setVersion("1.2.3"); + project.setProperties(new Properties()); + assertThat(formatter.format("%g/%a:%l")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%v")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3") + .satisfies(ImageNameFormatterTest::validImageName); + assertThat(formatter.format("%g/%a:%t")) + .isEqualTo("jkube/kubernetes-maven-plugin:1.2.3") + .satisfies(ImageNameFormatterTest::validImageName); + } + @Test void groupIdWithProperty() { // Given @@ -131,4 +223,8 @@ void format_whenPropertyInImageName_thenResolveProperty() { // Then assertThat(result).isEqualTo("registry.gitlab.com/myproject/myrepo/mycontainer:der12"); } + + private static void validImageName(String v) { + assertThatCode(() -> ImageName.validate(v)).doesNotThrowAnyException(); + } } diff --git a/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc b/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc index 7edbc20a04..286a538d09 100644 --- a/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc +++ b/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc @@ -1,5 +1,8 @@ [[image-name]] -When specifying the image name in the configuration with the `` field you can use several placeholders which are replaced during runtime by this plugin. In addition you can use regular Maven properties which are resolved by Maven itself. +When specifying the image name in the configuration with the `` field, then you can use several placeholders. +These placeholders are replaced during the execution by this plugin. +In addition, you can use regular Maven properties. +These properties are resolved by Maven itself. .Image Names [cols="1,5"] @@ -7,18 +10,33 @@ When specifying the image name in the configuration with the `` field you | Placeholder | Description | *%g* -| The last part of the Maven group name, sanitized so that it can be used as username on GitHub. Only the part after the last dot is used. E.g. for a group id `org.eclipse.jkube` this placeholder would insert `jkube` +| The last part of the Maven group name. +The name gets sanitized, so that it can be used as username on GitHub. +Only the part after the last dot is used. +For example, given the group id `org.eclipse.jkube`, this placeholder would insert `jkube`. | *%a* -| A sanitized version of the artefact id so that it can be used as part of an Docker image name. I.e. it is converted to all lower case (as required by Docker) +| A sanitized version of the artefact id, so that it can be used as part of a Docker image name. +This means primarily, that it is converted to all lower case (as required by Docker). | *%v* -| The project version. Synonym to `${project.version}` +| A sanitized version of the project version. Replaces `+` with `-` in `${project.version}` to comply with the Docker tag convention. +(A different replacement symbol can be defined by setting the `jkube.image.tag.semver_plus_substitution` property.) +For example, the version '1.2.3b' becomes the exact same Docker tag, '1.2.3b'. +But '1.2.3+internal' becomes the `1.2.3-internal` Docker tag. | *%l* -| If the project version ends with `-SNAPSHOT` then this placeholder is `latest`, otherwise its the full version (same as `%v`) +| If the https://semver.org/spec/v2.0.0.html#spec-item-9[pre-release part] of the project version ends with `-SNAPSHOT`, then this placeholder resolves to `latest`. +Otherwise, it's the same as `%v`. + +If the `${project.version}` contains a https://semver.org/spec/v2.0.0.html#spec-item-10[build metadata part] (i.e. everything after the `+`), then the `+` is substituted and the rest is appended. +For example, the project version `1.2.3-SNAPSHOT+internal` becomes the `latest-internal` Docker tag. | *%t* -| If the project version ends with `-SNAPSHOT` this placeholder resolves to `snapshot-` where timestamp has the date format `yyMMdd-HHmmss-SSSS` (eg `snapshot-`). This feature is especially useful during development in oder to avoid conflicts when images are to be updated which are still in use. You need to take care yourself of cleaning up old images afterwards, though. -|=== +| If the project version ends with `-SNAPSHOT`, this placeholder resolves to `snapshot-` where timestamp has the date format `yyMMdd-HHmmss-SSSS` (eg `snapshot-`). +This feature is especially useful during development in oder to avoid conflicts when images are to be updated which are still in use. +You need to take care yourself of cleaning up old images afterwards, though. +If the `${project.version}` contains a https://semver.org/spec/v2.0.0.html#spec-item-10[build metadata part] (i.e. everything after the `+`), then the `+` is substituted and the rest is appended. +For example, the project version `1.2.3-SNAPSHOT+internal` becomes the `snapshot-221018-113000-0000-internal` Docker tag. +|===