diff options
author | Martin Polden <mpolden@mpolden.no> | 2020-05-08 13:47:16 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2020-05-11 10:33:15 +0200 |
commit | 05b8c4bbe5e7887c3c447ad79c644ecacfc65831 (patch) | |
tree | 46be716d8f9b97e8aca1dce282a8c6e072b8b794 /node-repository | |
parent | 7d97b0955785e1a671e12b8468b55c24a7b6bc1c (diff) |
Extract OS version change to support additional fields
Diffstat (limited to 'node-repository')
9 files changed, 231 insertions, 159 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersionChange.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersionChange.java new file mode 100644 index 00000000000..12a86738f82 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersionChange.java @@ -0,0 +1,49 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.os; + +import com.google.common.collect.ImmutableSortedMap; +import com.yahoo.component.Version; +import com.yahoo.config.provision.NodeType; + +import java.util.Map; +import java.util.Objects; + +/** + * The OS version change being deployed in a {@link com.yahoo.vespa.hosted.provision.NodeRepository}. + * + * @author mpolden + */ +public class OsVersionChange { + + public static final OsVersionChange NONE = new OsVersionChange(Map.of()); + + private final Map<NodeType, Version> targets; + + public OsVersionChange(Map<NodeType, Version> targets) { + this.targets = ImmutableSortedMap.copyOf(Objects.requireNonNull(targets)); + } + + /** Version targets for this */ + public Map<NodeType, Version> targets() { + return targets; + } + + /** Returns a copy of this with target versions set to given value */ + public OsVersionChange with(Map<NodeType, Version> targets) { + return new OsVersionChange(targets); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OsVersionChange that = (OsVersionChange) o; + return targets.equals(that.targets); + } + + @Override + public int hashCode() { + return Objects.hash(targets); + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java index f45e6d86613..efe69953f27 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java @@ -7,12 +7,14 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.persistence.CuratorDatabaseClient; -import java.util.Map; +import java.util.HashMap; import java.util.Optional; +import java.util.function.UnaryOperator; import java.util.logging.Logger; /** - * Thread-safe class that manages target OS versions for nodes in this repository. + * Thread-safe class that manages an OS version change for nodes in this repository. An {@link Upgrader} decides how a + * {@link OsVersionChange} is applied to nodes. * * A version target is initially inactive. Activation decision is taken by * {@link com.yahoo.vespa.hosted.provision.maintenance.OsUpgradeActivator}. @@ -37,19 +39,29 @@ public class OsVersions { this.upgrader = upgrader; // Read and write all versions to make sure they are stored in the latest version of the serialized format - try (var lock = db.lockOsVersions()) { - db.writeOsVersions(db.readOsVersions()); + try (var lock = db.lockOsVersionChange()) { + db.writeOsVersionChange(db.readOsVersionChange()); } } - /** Returns the current target versions for each node type */ - public Map<NodeType, Version> targets() { - return db.readOsVersions(); + /** Returns the current OS version change */ + public OsVersionChange readChange() { + return db.readOsVersionChange(); + } + + /** Write the current OS version change with the result of the given operation applied */ + public void writeChange(UnaryOperator<OsVersionChange> operation) { + try (var lock = db.lockOsVersionChange()) { + OsVersionChange change = readChange(); + OsVersionChange newChange = operation.apply(change); + if (newChange.equals(change)) return; // Nothing changed + db.writeOsVersionChange(newChange); + } } /** Returns the current target version for given node type, if any */ public Optional<Version> targetFor(NodeType type) { - return Optional.ofNullable(targets().get(type)); + return Optional.ofNullable(readChange().targets().get(type)); } /** @@ -58,26 +70,26 @@ public class OsVersions { */ public void removeTarget(NodeType nodeType) { require(nodeType); - try (Lock lock = db.lockOsVersions()) { - var osVersions = db.readOsVersions(); - osVersions.remove(nodeType); + writeChange((change) -> { + var targets = new HashMap<>(change.targets()); + targets.remove(nodeType); upgrader.disableUpgrade(nodeType); - db.writeOsVersions(osVersions); - } + return change.with(targets); + }); } - /** Set the target OS version for nodes of given type */ + /** Set the target OS version and upgrade budget for nodes of given type */ public void setTarget(NodeType nodeType, Version newTarget, boolean force) { require(nodeType); if (newTarget.isEmpty()) { - throw new IllegalArgumentException("Invalid target version: " + newTarget.toFullString()); + throw new IllegalArgumentException("Invalid target version: " + newTarget.toFullString()); } - try (Lock lock = db.lockOsVersions()) { - var osVersions = db.readOsVersions(); - var oldTarget = Optional.ofNullable(osVersions.get(nodeType)); + writeChange((change) -> { + var targets = new HashMap<>(change.targets()); + var oldTarget = Optional.ofNullable(targets.get(nodeType)); if (oldTarget.filter(v -> v.equals(newTarget)).isPresent()) { - return; // Old target matches new target, nothing to do + return change; // Old target matches new target, nothing to do } if (!force && oldTarget.filter(v -> v.isAfter(newTarget)).isPresent()) { @@ -86,21 +98,20 @@ public class OsVersions { + oldTarget.get()); } - osVersions.put(nodeType, newTarget); - db.writeOsVersions(osVersions); + targets.put(nodeType, newTarget); log.info("Set OS target version for " + nodeType + " nodes to " + newTarget.toFullString()); - } + return change.with(targets); + }); } /** Resume or halt upgrade of given node type */ public void resumeUpgradeOf(NodeType nodeType, boolean resume) { require(nodeType); - try (Lock lock = db.lockOsVersions()) { - var osVersions = db.readOsVersions(); - var currentVersion = osVersions.get(nodeType); - if (currentVersion == null) return; // No target version set for this type + try (Lock lock = db.lockOsVersionChange()) { + var targetVersion = readChange().targets().get(nodeType); + if (targetVersion == null) return; // No target version set for this type if (resume) { - upgrader.upgrade(nodeType, currentVersion); + upgrader.upgrade(nodeType, targetVersion); } else { upgrader.disableUpgrade(nodeType); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index 4defbb55485..367271564ea 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -1,4 +1,4 @@ -// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.persistence; import com.google.common.util.concurrent.UncheckedTimeoutException; @@ -12,7 +12,6 @@ import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Zone; -import java.util.logging.Level; import com.yahoo.path.Path; import com.yahoo.transaction.NestedTransaction; import com.yahoo.transaction.Transaction; @@ -27,6 +26,7 @@ import com.yahoo.vespa.hosted.provision.lb.LoadBalancer; import com.yahoo.vespa.hosted.provision.lb.LoadBalancerId; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.Status; +import com.yahoo.vespa.hosted.provision.os.OsVersionChange; import java.time.Clock; import java.time.Duration; @@ -488,19 +488,19 @@ public class CuratorDatabaseClient implements JobControl.Db { // OS versions ----------------------------------------------------------- - public Map<NodeType, Version> readOsVersions() { - return read(osVersionsPath, OsVersionsSerializer::fromJson).orElseGet(TreeMap::new); + public OsVersionChange readOsVersionChange() { + return read(osVersionsPath, OsVersionChangeSerializer::fromJson).orElse(OsVersionChange.NONE); } - public void writeOsVersions(Map<NodeType, Version> versions) { + public void writeOsVersionChange(OsVersionChange change) { NestedTransaction transaction = new NestedTransaction(); CuratorTransaction curatorTransaction = db.newCuratorTransactionIn(transaction); curatorTransaction.add(CuratorOperations.setData(osVersionsPath.getAbsolute(), - OsVersionsSerializer.toJson(versions))); + OsVersionChangeSerializer.toJson(change))); transaction.commit(); } - public Lock lockOsVersions() { + public Lock lockOsVersionChange() { return db.lock(lockPath.append("osVersionsLock"), defaultLockTimeout); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializer.java new file mode 100644 index 00000000000..b2b329725f5 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializer.java @@ -0,0 +1,75 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.persistence; + +import com.yahoo.component.Version; +import com.yahoo.config.provision.NodeType; +import com.yahoo.slime.ArrayTraverser; +import com.yahoo.slime.ObjectTraverser; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.vespa.hosted.provision.os.OsVersionChange; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.HashMap; + +/** + * Serializer for {@link OsVersionChange}. + * + * @author mpolden + */ +public class OsVersionChangeSerializer { + + private static final String TARGETS_FIELD = "targets"; + private static final String NODE_TYPE_FIELD = "nodeType"; + private static final String VERSION_FIELD = "version"; + + private OsVersionChangeSerializer() {} + + public static byte[] toJson(OsVersionChange change) { + var slime = new Slime(); + var object = slime.setObject(); + var targetsObject = object.setArray(TARGETS_FIELD); + change.targets().forEach((nodeType, osVersion) -> { + var targetObject = targetsObject.addObject(); + targetObject.setString(NODE_TYPE_FIELD, NodeSerializer.toString(nodeType)); + targetObject.setString(VERSION_FIELD, osVersion.toFullString()); + // TODO(mpolden): Stop writing old format after May 2020 + var versionObject = object.setObject(NodeSerializer.toString(nodeType)); + versionObject.setString(VERSION_FIELD, osVersion.toFullString()); + }); + try { + return SlimeUtils.toJsonBytes(slime); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static OsVersionChange fromJson(byte[] data) { + var targets = new HashMap<NodeType, Version>(); + var inspector = SlimeUtils.jsonToSlime(data).get(); + // TODO(mpolden): Remove reading of old format after May 2020 + inspector.traverse((ObjectTraverser) (key, value) -> { + if (isNodeType(key)) { + var version = Version.fromString(value.field(VERSION_FIELD).asString()); + targets.put(NodeSerializer.nodeTypeFromString(key), version); + } + }); + inspector.field(TARGETS_FIELD).traverse((ArrayTraverser) (idx, arrayInspector) -> { + var version = Version.fromString(arrayInspector.field(VERSION_FIELD).asString()); + var nodeType = NodeSerializer.nodeTypeFromString(arrayInspector.field(NODE_TYPE_FIELD).asString()); + targets.put(nodeType, version); + }); + return new OsVersionChange(targets); + } + + private static boolean isNodeType(String name) { + try { + NodeType.valueOf(name); + return true; + } catch (IllegalArgumentException ignored) { + return false; + } + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializer.java deleted file mode 100644 index fd430350b5c..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializer.java +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.provision.persistence; - -import com.yahoo.component.Version; -import com.yahoo.config.provision.NodeType; -import com.yahoo.slime.ObjectTraverser; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.vespa.hosted.provision.node.OsVersion; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Map; -import java.util.TreeMap; - -/** - * Serializer for a map of {@link NodeType} and {@link OsVersion}. - * - * @author mpolden - */ -public class OsVersionsSerializer { - - private static final String VERSION_FIELD = "version"; - - private OsVersionsSerializer() {} - - public static byte[] toJson(Map<NodeType, Version> versions) { - var slime = new Slime(); - var object = slime.setObject(); - versions.forEach((nodeType, osVersion) -> { - var versionObject = object.setObject(NodeSerializer.toString(nodeType)); - versionObject.setString(VERSION_FIELD, osVersion.toFullString()); - }); - try { - return SlimeUtils.toJsonBytes(slime); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public static Map<NodeType, Version> fromJson(byte[] data) { - var versions = new TreeMap<NodeType, Version>(); // Use TreeMap to sort by node type - var inspector = SlimeUtils.jsonToSlime(data).get(); - inspector.traverse((ObjectTraverser) (key, value) -> { - if (isNodeType(key)) { - var version = Version.fromString(value.field(VERSION_FIELD).asString()); - versions.put(NodeSerializer.nodeTypeFromString(key), version); - } - }); - return versions; - } - - private static boolean isNodeType(String name) { - try { - NodeType.valueOf(name); - return true; - } catch (IllegalArgumentException ignored) { - return false; - } - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/UpgradeResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/UpgradeResponse.java index 16858ec6963..be87a984941 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/UpgradeResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/UpgradeResponse.java @@ -39,7 +39,7 @@ public class UpgradeResponse extends HttpResponse { infrastructureVersions.getTargetVersions().forEach((nodeType, version) -> versionsObject.setString(nodeType.name(), version.toFullString())); Cursor osVersionsObject = root.setObject("osVersions"); - osVersions.targets().forEach((nodeType, osVersion) -> osVersionsObject.setString(nodeType.name(), osVersion.toFullString())); + osVersions.readChange().targets().forEach((nodeType, osVersion) -> osVersionsObject.setString(nodeType.name(), osVersion.toFullString())); Cursor dockerImagesObject = root.setObject("dockerImages"); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java index b8b09199ba2..e1d3eea58fd 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.os; import com.yahoo.component.Version; @@ -36,12 +36,11 @@ public class OsVersionsTest { @Test public void test_versions() { var versions = new OsVersions(tester.nodeRepository(), new DelegatingUpgrader(tester.nodeRepository(), Integer.MAX_VALUE)); - tester.makeReadyNodes(10, "default", NodeType.host); - tester.prepareAndActivateInfraApplication(infraApplication, NodeType.host); + provisionInfraApplication(10); Supplier<List<Node>> hostNodes = () -> tester.nodeRepository().getNodes(NodeType.host); // Upgrade OS - assertTrue("No versions set", versions.targets().isEmpty()); + assertTrue("No versions set", versions.readChange().targets().isEmpty()); var version1 = Version.fromString("7.1"); versions.setTarget(NodeType.host, version1, false); assertEquals(version1, versions.targetFor(NodeType.host).get()); @@ -81,9 +80,8 @@ public class OsVersionsTest { int totalNodes = 20; int maxActiveUpgrades = 5; var versions = new OsVersions(tester.nodeRepository(), new DelegatingUpgrader(tester.nodeRepository(), maxActiveUpgrades)); - tester.makeReadyNodes(totalNodes, "default", NodeType.host); + provisionInfraApplication(totalNodes); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().list().state(Node.State.active).nodeType(NodeType.host); - tester.prepareAndActivateInfraApplication(infraApplication, NodeType.host); // 5 nodes have no version. The other 15 are spread across different versions var hostNodesList = hostNodes.get().asList(); @@ -128,8 +126,7 @@ public class OsVersionsTest { @Test public void test_newer_upgrade_aborts_upgrade_to_stale_version() { var versions = new OsVersions(tester.nodeRepository(), new DelegatingUpgrader(tester.nodeRepository(), Integer.MAX_VALUE)); - tester.makeReadyNodes(10, "default", NodeType.host); - tester.prepareAndActivateInfraApplication(infraApplication, NodeType.host); + provisionInfraApplication(10); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().list().nodeType(NodeType.host); // Some nodes are targeting an older version @@ -145,6 +142,11 @@ public class OsVersionsTest { assertEquals(version2, minVersion(hostNodes.get(), OsVersion::wanted)); } + private void provisionInfraApplication(int nodeCount) { + tester.makeReadyNodes(nodeCount, "default", NodeType.host); + tester.prepareAndActivateInfraApplication(infraApplication, NodeType.host); + } + private Version minVersion(NodeList nodes, Function<OsVersion, Optional<Version>> versionField) { return nodes.asList().stream() .map(Node::status) diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializerTest.java new file mode 100644 index 00000000000..b26c0f9055f --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionChangeSerializerTest.java @@ -0,0 +1,51 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.persistence; + +import com.yahoo.component.Version; +import com.yahoo.config.provision.NodeType; +import com.yahoo.vespa.hosted.provision.os.OsVersionChange; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author mpolden + */ +public class OsVersionChangeSerializerTest { + + @Test + public void serialization() { + var change = new OsVersionChange(Map.of( + NodeType.host, Version.fromString("1.2.3"), + NodeType.proxyhost, Version.fromString("4.5.6"), + NodeType.confighost, Version.fromString("7.8.9") + )); + var serialized = OsVersionChangeSerializer.fromJson(OsVersionChangeSerializer.toJson(change)); + assertEquals(serialized, change); + } + + @Test + public void legacy_serialization() { + // Read old format + var change = new OsVersionChange(Map.of( + NodeType.host, Version.fromString("1.2.3"), + NodeType.proxyhost, Version.fromString("4.5.6"), + NodeType.confighost, Version.fromString("7.8.9") + )); + var legacyFormat = "{\"host\":{\"version\":\"1.2.3\"},\"proxyhost\":{\"version\":\"4.5.6\"},\"confighost\":{\"version\":\"7.8.9\"}}"; + assertEquals(change, OsVersionChangeSerializer.fromJson(legacyFormat.getBytes(StandardCharsets.UTF_8))); + + // Write format supported by both old and new serializer + var oldFormat = "{\"targets\":[{\"nodeType\":\"host\",\"version\":\"1.2.3\"}," + + "{\"nodeType\":\"proxyhost\",\"version\":\"4.5.6\"}," + + "{\"nodeType\":\"confighost\",\"version\":\"7.8.9\"}]," + + "\"host\":{\"version\":\"1.2.3\"}," + + "\"proxyhost\":{\"version\":\"4.5.6\"}," + + "\"confighost\":{\"version\":\"7.8.9\"}}"; + assertEquals(oldFormat, new String(OsVersionChangeSerializer.toJson(change), StandardCharsets.UTF_8)); + } + +} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializerTest.java deleted file mode 100644 index 36dbf26c0d3..00000000000 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/OsVersionsSerializerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.provision.persistence; - -import com.yahoo.component.Version; -import com.yahoo.config.provision.NodeType; -import org.junit.Test; - -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import static org.junit.Assert.assertEquals; - -/** - * @author mpolden - */ -public class OsVersionsSerializerTest { - - @Test - public void serialization() { - var versions = Map.of( - NodeType.host, Version.fromString("1.2.3"), - NodeType.proxyhost, Version.fromString("4.5.6"), - NodeType.confighost, Version.fromString("7.8.9") - ); - var serialized = OsVersionsSerializer.fromJson(OsVersionsSerializer.toJson(versions)); - assertEquals(serialized, versions); - } - - @Test - public void ignores_unknown_keys() { - var jsonWithUnknownKeys = "{\n" + - " \"foo\": \"bar\",\n" + - " " + - "\"host\": {\n" + - " \"version\": \"1.2.3\"\n" + - " },\n" + - " " + - "\"proxyhost\": {\n" + - " \"version\": \"4.5.6\"\n" + - " },\n" + - " " + - "\"confighost\": {\n" + - " \"version\": \"7.8.9\"\n" + - " }\n" + - "}"; - var versions = Map.of( - NodeType.host, Version.fromString("1.2.3"), - NodeType.proxyhost, Version.fromString("4.5.6"), - NodeType.confighost, Version.fromString("7.8.9") - ); - assertEquals(versions, OsVersionsSerializer.fromJson(jsonWithUnknownKeys.getBytes(StandardCharsets.UTF_8))); - } - -} |