diff --git a/pom.xml b/pom.xml
index b9f069f5..37448703 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,8 +104,12 @@
3.3.1
3.10.1
1.14.0
+ 3.4.1
io.cryostat.agent.shaded
+
+ 4.0.0
+ 5.0.0
@@ -299,6 +303,29 @@
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ ${org.codehaus.mojo.exec.plugin.version}
+
+
+ generate-git-version
+ generate-resources
+
+ exec
+
+
+ git
+
+ rev-parse
+ --verify
+ HEAD
+
+ ${project.build.directory}/classes/META-INF/gitinfo
+
+
+
+
org.apache.maven.plugins
maven-shade-plugin
diff --git a/src/main/java/io/cryostat/agent/Agent.java b/src/main/java/io/cryostat/agent/Agent.java
index e127bad2..e9a9a166 100644
--- a/src/main/java/io/cryostat/agent/Agent.java
+++ b/src/main/java/io/cryostat/agent/Agent.java
@@ -38,6 +38,7 @@
import javax.inject.Singleton;
import io.cryostat.agent.ConfigModule.URIRange;
+import io.cryostat.agent.VersionInfo.Semver;
import io.cryostat.agent.harvest.Harvester;
import io.cryostat.agent.insights.InsightsAgentHelper;
import io.cryostat.agent.model.PluginInfo;
@@ -49,6 +50,7 @@
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import dagger.Component;
+import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -155,7 +157,6 @@ public static void agentmain(String args, Instrumentation instrumentation) {
Thread t =
new Thread(
() -> {
- log.info("Cryostat Agent starting...");
agent.accept(aa);
});
t.setDaemon(true);
@@ -200,12 +201,30 @@ static URI selfJarLocation() throws URISyntaxException {
@Override
public void accept(AgentArgs args) {
- args.getProperties()
- .forEach(
- (k, v) -> {
- log.trace("Set system property {} = {}", k, v);
- System.setProperty(k, v);
- });
+ Map properties = new HashMap<>();
+ properties.putAll(args.getProperties());
+ properties.put("build.git.commit-hash", new BuildInfo().getGitInfo().getHash());
+ Semver agentVersion = Semver.UNKNOWN;
+ Pair serverVersionRange = Pair.of(Semver.UNKNOWN, Semver.UNKNOWN);
+ try {
+ VersionInfo versionInfo = VersionInfo.load();
+ serverVersionRange = Pair.of(versionInfo.getServerMin(), versionInfo.getServerMax());
+ agentVersion = versionInfo.getAgentVersion();
+ properties.putAll(versionInfo.asMap());
+ } catch (IOException ioe) {
+ log.warn("Unable to read versions.properties file", ioe);
+ }
+ log.info(
+ "Cryostat Agent version {} (for Cryostat server version range [{}, {}) )"
+ + " starting...",
+ agentVersion,
+ serverVersionRange.getLeft(),
+ serverVersionRange.getRight());
+ properties.forEach(
+ (k, v) -> {
+ log.trace("Set system property {} = {}", k, v);
+ System.setProperty(k, v);
+ });
AgentExitHandler agentExitHandler = null;
try {
final Client client = DaggerAgent_Client.builder().build();
diff --git a/src/main/java/io/cryostat/agent/BuildInfo.java b/src/main/java/io/cryostat/agent/BuildInfo.java
new file mode 100644
index 00000000..d1543149
--- /dev/null
+++ b/src/main/java/io/cryostat/agent/BuildInfo.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import io.cryostat.agent.util.ResourcesUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// FIXME this is adapted from Cryostat. Extract this to a reusable utility in libcryostat?
+public class BuildInfo {
+
+ private static Logger log = LoggerFactory.getLogger(BuildInfo.class);
+ private static final String RESOURCE_LOCATION = "META-INF/gitinfo";
+
+ private final GitInfo gitinfo = new GitInfo();
+
+ public GitInfo getGitInfo() {
+ return gitinfo;
+ }
+
+ public static class GitInfo {
+ public String getHash() {
+ try (BufferedReader br =
+ new BufferedReader(
+ new InputStreamReader(
+ ResourcesUtil.getResourceAsStream(RESOURCE_LOCATION),
+ StandardCharsets.UTF_8))) {
+ return br.lines()
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format(
+ "Resource file %s is empty",
+ RESOURCE_LOCATION)))
+ .trim();
+ } catch (Exception e) {
+ log.warn("Version retrieval exception", e);
+ return "unknown";
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/cryostat/agent/CryostatClient.java b/src/main/java/io/cryostat/agent/CryostatClient.java
index 3f1577c9..3b4fb043 100644
--- a/src/main/java/io/cryostat/agent/CryostatClient.java
+++ b/src/main/java/io/cryostat/agent/CryostatClient.java
@@ -44,6 +44,7 @@
import io.cryostat.agent.model.DiscoveryNode;
import io.cryostat.agent.model.PluginInfo;
import io.cryostat.agent.model.RegistrationInfo;
+import io.cryostat.agent.model.ServerHealth;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -111,6 +112,22 @@ public class CryostatClient {
log.info("Using Cryostat baseuri {}", baseUri);
}
+ public CompletableFuture serverHealth() {
+ HttpGet req = new HttpGet(baseUri.resolve("/health"));
+ log.trace("{}", req);
+ return supply(req, res -> logResponse(req, res))
+ .thenApply(
+ res -> {
+ try (InputStream is = res.getEntity().getContent()) {
+ return mapper.readValue(is, ServerHealth.class);
+ } catch (IOException e) {
+ log.error("Unable to parse response as JSON", e);
+ throw new RegistrationException(e);
+ }
+ })
+ .whenComplete((v, t) -> req.reset());
+ }
+
public CompletableFuture checkRegistration(PluginInfo pluginInfo) {
if (!pluginInfo.isInitialized()) {
return CompletableFuture.completedFuture(false);
diff --git a/src/main/java/io/cryostat/agent/Registration.java b/src/main/java/io/cryostat/agent/Registration.java
index fd529c89..8e608a15 100644
--- a/src/main/java/io/cryostat/agent/Registration.java
+++ b/src/main/java/io/cryostat/agent/Registration.java
@@ -15,6 +15,7 @@
*/
package io.cryostat.agent;
+import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
@@ -33,6 +34,7 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
+import io.cryostat.agent.VersionInfo.Semver;
import io.cryostat.agent.model.DiscoveryNode;
import io.cryostat.agent.model.PluginInfo;
import io.cryostat.agent.util.StringUtils;
@@ -197,6 +199,29 @@ void tryRegister() {
return;
}
try {
+ cryostat.serverHealth()
+ .thenAccept(
+ health -> {
+ Semver cryostatSemver = health.cryostatSemver();
+ log.debug(
+ "Connected to Cryostat server: version {} , build {}",
+ cryostatSemver,
+ health.buildInfo().git().hash());
+ try {
+ VersionInfo version = VersionInfo.load();
+ if (!version.validateServerVersion(cryostatSemver)) {
+ log.warn(
+ "Cryostat server version {} is outside of expected"
+ + " range [{}, {})",
+ cryostatSemver,
+ version.getServerMin(),
+ version.getServerMax());
+ }
+ } catch (IOException ioe) {
+ log.error("Unable to read versions.properties file", ioe);
+ }
+ })
+ .get();
URI credentialedCallback =
new URIBuilder(callback)
.setUserInfo("storedcredentials", String.valueOf(credentialId))
diff --git a/src/main/java/io/cryostat/agent/VersionInfo.java b/src/main/java/io/cryostat/agent/VersionInfo.java
new file mode 100644
index 00000000..b6f98aee
--- /dev/null
+++ b/src/main/java/io/cryostat/agent/VersionInfo.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+
+import io.cryostat.agent.util.ResourcesUtil;
+
+public class VersionInfo {
+
+ private static final String RESOURCE_LOCATION = "versions.properties";
+ static final String AGENT_VERSION_KEY = "cryostat.agent.version";
+ static final String MIN_VERSION_KEY = "cryostat.server.version.min";
+ static final String MAX_VERSION_KEY = "cryostat.server.version.max";
+
+ private final Semver agentVersion;
+ private final Semver serverMin;
+ private final Semver serverMax;
+
+ // testing only
+ VersionInfo(Semver agentVersion, Semver serverMin, Semver serverMax) {
+ this.agentVersion = agentVersion;
+ this.serverMin = serverMin;
+ this.serverMax = serverMax;
+ }
+
+ public static VersionInfo load() throws IOException {
+ Properties prop = new Properties();
+ try (InputStream is = ResourcesUtil.getResourceAsStream(RESOURCE_LOCATION)) {
+ prop.load(is);
+ }
+ Semver agentVersion = Semver.fromString(prop.getProperty(AGENT_VERSION_KEY));
+ Semver serverMin = Semver.fromString(prop.getProperty(MIN_VERSION_KEY));
+ Semver serverMax = Semver.fromString(prop.getProperty(MAX_VERSION_KEY));
+ return new VersionInfo(agentVersion, serverMin, serverMax);
+ }
+
+ public Map asMap() {
+ return Map.of(
+ AGENT_VERSION_KEY, getAgentVersion().toString(),
+ MIN_VERSION_KEY, getServerMin().toString(),
+ MAX_VERSION_KEY, getServerMax().toString());
+ }
+
+ public Semver getAgentVersion() {
+ return agentVersion;
+ }
+
+ public Semver getServerMin() {
+ return serverMin;
+ }
+
+ public Semver getServerMax() {
+ return serverMax;
+ }
+
+ public boolean validateServerVersion(Semver actual) {
+ boolean greaterEqualMin = getServerMin().compareTo(actual) <= 0;
+ boolean lesserMax = getServerMax().compareTo(actual) > 0;
+ return greaterEqualMin && lesserMax;
+ }
+
+ public static class Semver implements Comparable {
+
+ public static final Semver UNKNOWN =
+ new Semver(0, 0, 0) {
+ @Override
+ public String toString() {
+ return "unknown";
+ }
+ };
+
+ private final int major;
+ private final int minor;
+ private final int patch;
+
+ public Semver(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ }
+
+ public static Semver fromString(String in) {
+ String[] parts = in.split("\\.");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException();
+ }
+ return new Semver(
+ Integer.parseInt(parts[0]),
+ Integer.parseInt(parts[1]),
+ Integer.parseInt(parts[2]));
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+
+ public int getPatch() {
+ return patch;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d.%d.%d", major, minor, patch);
+ }
+
+ @Override
+ public int compareTo(Semver o) {
+ return Comparator.comparingInt(Semver::getMajor)
+ .thenComparing(Semver::getMinor)
+ .thenComparing(Semver::getPatch)
+ .compare(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(major, minor, patch);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Semver other = (Semver) obj;
+ return major == other.major && minor == other.minor && patch == other.patch;
+ }
+ }
+}
diff --git a/src/main/java/io/cryostat/agent/model/ServerHealth.java b/src/main/java/io/cryostat/agent/model/ServerHealth.java
new file mode 100644
index 00000000..2ec06f70
--- /dev/null
+++ b/src/main/java/io/cryostat/agent/model/ServerHealth.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent.model;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.cryostat.agent.VersionInfo.Semver;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
+public class ServerHealth {
+
+ private static final Pattern VERSION_PATTERN =
+ Pattern.compile(
+ "^v(?[\\d]+)\\.(?[\\d]+)\\.(?[\\d]+)(?:-[a-z0-9\\._-]*)?",
+ Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
+
+ private String cryostatVersion;
+ private BuildInfo build;
+
+ public ServerHealth() {}
+
+ public ServerHealth(String cryostatVersion, BuildInfo build) {
+ this.cryostatVersion = cryostatVersion;
+ this.build = build;
+ }
+
+ public void setCryostatVersion(String cryostatVersion) {
+ this.cryostatVersion = cryostatVersion;
+ }
+
+ public void setBuild(BuildInfo build) {
+ this.build = build;
+ }
+
+ public String cryostatVersion() {
+ return cryostatVersion;
+ }
+
+ public Semver cryostatSemver() {
+ Matcher m = VERSION_PATTERN.matcher(cryostatVersion());
+ if (!m.matches()) {
+ return Semver.fromString("0.0.0");
+ }
+ return new Semver(
+ Integer.parseInt(m.group("major")),
+ Integer.parseInt(m.group("minor")),
+ Integer.parseInt(m.group("patch")));
+ }
+
+ public BuildInfo buildInfo() {
+ return build;
+ }
+
+ @SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
+ public static class BuildInfo {
+ private GitInfo git;
+
+ public BuildInfo() {}
+
+ public BuildInfo(GitInfo git) {
+ this.git = git;
+ }
+
+ public void setGit(GitInfo git) {
+ this.git = git;
+ }
+
+ public GitInfo git() {
+ return git;
+ }
+ }
+
+ @SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
+ public static class GitInfo {
+ private String hash;
+
+ public GitInfo() {}
+
+ public GitInfo(String hash) {
+ this.hash = hash;
+ }
+
+ public void setHash(String hash) {
+ this.hash = hash;
+ }
+
+ public String hash() {
+ return hash;
+ }
+ }
+}
diff --git a/src/main/java/io/cryostat/agent/util/ResourcesUtil.java b/src/main/java/io/cryostat/agent/util/ResourcesUtil.java
new file mode 100644
index 00000000..44c76e85
--- /dev/null
+++ b/src/main/java/io/cryostat/agent/util/ResourcesUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent.util;
+
+import java.io.InputStream;
+
+public class ResourcesUtil {
+
+ private ResourcesUtil() {}
+
+ public static ClassLoader getClassLoader() {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ if (cl == null) {
+ cl = ClassLoader.getSystemClassLoader();
+ }
+ return cl;
+ }
+
+ public static InputStream getResourceAsStream(String location) {
+ return getClassLoader().getResourceAsStream(location);
+ }
+}
diff --git a/src/main/resources/versions.properties b/src/main/resources/versions.properties
new file mode 100644
index 00000000..fdf8c0c0
--- /dev/null
+++ b/src/main/resources/versions.properties
@@ -0,0 +1,3 @@
+cryostat.agent.version=${version}
+cryostat.server.version.min=${cryostat.server.version.min}
+cryostat.server.version.max=${cryostat.server.version.max}
diff --git a/src/test/java/io/cryostat/agent/SemverTest.java b/src/test/java/io/cryostat/agent/SemverTest.java
new file mode 100644
index 00000000..74e8ef90
--- /dev/null
+++ b/src/test/java/io/cryostat/agent/SemverTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent;
+
+import io.cryostat.agent.VersionInfo.Semver;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class SemverTest {
+
+ @ParameterizedTest
+ @CsvSource({
+ "1.0.0, 1.0.0, 0",
+ "2.0.0, 1.0.0, 1",
+ "1.0.0, 2.0.0, -1",
+ "1.0.0, 1.1.0, -1",
+ "1.1.0, 1.0.0, 1",
+ "1.0.1, 1.0.0, 1",
+ "2.0.0, 1.1.0, 1",
+ "1.0.1, 1.1.0, -1",
+ "1.1.1, 1.0.1, 1",
+ })
+ public void test(String first, String second, int result) {
+ Semver a = Semver.fromString(first);
+ Semver b = Semver.fromString(second);
+ MatcherAssert.assertThat(a.compareTo(b), Matchers.equalTo(result));
+ }
+}
diff --git a/src/test/java/io/cryostat/agent/VersionInfoTest.java b/src/test/java/io/cryostat/agent/VersionInfoTest.java
new file mode 100644
index 00000000..6ad7cb55
--- /dev/null
+++ b/src/test/java/io/cryostat/agent/VersionInfoTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent;
+
+import io.cryostat.agent.VersionInfo.Semver;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class VersionInfoTest {
+
+ static final Semver serverMin = Semver.fromString("1.0.0");
+ static final Semver serverMax = Semver.fromString("2.0.0");
+
+ @ParameterizedTest
+ @CsvSource({"1.0.0, true", "2.0.0, false", "3.0.0, false", "0.1.0, false", "1.1.0, true"})
+ public void test(String serverVersion, boolean inRange) {
+ VersionInfo info = new VersionInfo(Semver.UNKNOWN, serverMin, serverMax);
+ Semver actual = Semver.fromString(serverVersion);
+ MatcherAssert.assertThat(info.validateServerVersion(actual), Matchers.equalTo(inRange));
+ }
+}
diff --git a/src/test/java/io/cryostat/agent/model/ServerHealthTest.java b/src/test/java/io/cryostat/agent/model/ServerHealthTest.java
new file mode 100644
index 00000000..a63a6f3c
--- /dev/null
+++ b/src/test/java/io/cryostat/agent/model/ServerHealthTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * 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.cryostat.agent.model;
+
+import io.cryostat.agent.VersionInfo.Semver;
+import io.cryostat.agent.model.ServerHealth.BuildInfo;
+import io.cryostat.agent.model.ServerHealth.GitInfo;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+public class ServerHealthTest {
+
+ @Test
+ public void test() {
+ GitInfo git = new GitInfo("abcd1234");
+ BuildInfo build = new BuildInfo(git);
+ ServerHealth health = new ServerHealth("v1.2.3-snapshot", build);
+ MatcherAssert.assertThat(
+ health.cryostatSemver(), Matchers.equalTo(Semver.fromString("1.2.3")));
+ }
+}