From 948ed9893243d487de4c42c74dae995d3e2d478d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 15 Oct 2024 16:18:40 -0400 Subject: [PATCH] feat(versions): record own build info/version as properties, validate server version (#508) --- pom.xml | 27 +++ src/main/java/io/cryostat/agent/Agent.java | 33 +++- .../java/io/cryostat/agent/BuildInfo.java | 61 +++++++ .../io/cryostat/agent/CryostatClient.java | 17 ++ .../java/io/cryostat/agent/Registration.java | 25 +++ .../java/io/cryostat/agent/VersionInfo.java | 157 ++++++++++++++++++ .../io/cryostat/agent/model/ServerHealth.java | 107 ++++++++++++ .../io/cryostat/agent/util/ResourcesUtil.java | 35 ++++ src/main/resources/versions.properties | 3 + .../java/io/cryostat/agent/SemverTest.java | 44 +++++ .../io/cryostat/agent/VersionInfoTest.java | 37 +++++ .../agent/model/ServerHealthTest.java | 36 ++++ 12 files changed, 575 insertions(+), 7 deletions(-) create mode 100644 src/main/java/io/cryostat/agent/BuildInfo.java create mode 100644 src/main/java/io/cryostat/agent/VersionInfo.java create mode 100644 src/main/java/io/cryostat/agent/model/ServerHealth.java create mode 100644 src/main/java/io/cryostat/agent/util/ResourcesUtil.java create mode 100644 src/main/resources/versions.properties create mode 100644 src/test/java/io/cryostat/agent/SemverTest.java create mode 100644 src/test/java/io/cryostat/agent/VersionInfoTest.java create mode 100644 src/test/java/io/cryostat/agent/model/ServerHealthTest.java 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"))); + } +}