Skip to content

Commit

Permalink
feat: Add compatibility with SemVer versions (eclipse-jkube#2301)
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Mäder <christian.maeder@nxt.engineering>
  • Loading branch information
cimnine authored and rohanKanojia committed Jul 27, 2023
1 parent 1ffc921 commit 693c162
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -499,3 +500,4 @@ Only the set of documented features are available to users.
### 0.1.0 (2019-12-19)
* Initial release


31 changes: 25 additions & 6 deletions gradle-plugin/doc/src/main/asciidoc/inc/image/_naming.adoc
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
[[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 `<name>` 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"]
|===
| 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-<timestamp>` 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-<timestamp>` 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.
|===
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}


Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -33,6 +35,7 @@
*/
class ImageNameFormatterTest {
private JavaProject project;

private ImageNameFormatter formatter;

@BeforeEach
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
[[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 Maven properties which are resolved by Maven itself.
When specifying the image name in the configuration with the `<name>` 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"]
|===
| 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-<timestamp>` 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-<timestamp>` 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.
|===

0 comments on commit 693c162

Please sign in to comment.