diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-09-23 13:38:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-23 13:38:31 +0200 |
commit | 8246ef8688fa17a7e5783d1c81695f7431218263 (patch) | |
tree | 8646bd4224f50947c68117a7ba678e7bdc07cdda /node-repository | |
parent | c7be1a493a89a65e40194266fd8a24bf3505b6f9 (diff) | |
parent | 7f69cf4acc3a6c785dff0ab956f90e87dbbbaa87 (diff) |
Merge pull request #19246 from vespa-engine/mortent/persist-host-truststore
persist host truststore information in node repository
Diffstat (limited to 'node-repository')
8 files changed, 189 insertions, 25 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index 240e041a504..df3ac00ce7f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -18,13 +18,16 @@ import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.NodeAcl; import com.yahoo.vespa.hosted.provision.node.Reports; import com.yahoo.vespa.hosted.provision.node.Status; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.time.Instant; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * A node in the node repository. The identity of a node is given by its id. @@ -50,6 +53,7 @@ public final class Node implements Nodelike { private final Optional<ApplicationId> exclusiveToApplicationId; private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final Optional<String> switchHostname; + private final List<TrustStoreItem> trustStoreItems; /** Record of the last event of each type happening to this node */ private final History history; @@ -79,7 +83,7 @@ public final class Node implements Nodelike { Flavor flavor, Status status, State state, Optional<Allocation> allocation, History history, NodeType type, Reports reports, Optional<String> modelName, Optional<TenantName> reservedTo, Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, - Optional<String> switchHostname) { + Optional<String> switchHostname, List<TrustStoreItem> trustStoreItems) { this.id = Objects.requireNonNull(id, "A node must have an ID"); this.hostname = requireNonEmptyString(hostname, "A node must have a hostname"); this.ipConfig = Objects.requireNonNull(ipConfig, "A node must a have an IP config"); @@ -96,6 +100,7 @@ public final class Node implements Nodelike { this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId cannot be null"); this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType cannot be null"); this.switchHostname = requireNonEmptyString(switchHostname, "switchHostname cannot be null"); + this.trustStoreItems = trustStoreItems.stream().distinct().collect(Collectors.toUnmodifiableList()); if (state == State.active) requireNonEmpty(ipConfig.primary(), "Active node " + hostname + " must have at least one valid IP address"); @@ -207,6 +212,11 @@ public final class Node implements Nodelike { return switchHostname; } + /** Returns the trusted certificates for this host if any. */ + public List<TrustStoreItem> trustedCertificates() { + return trustStoreItems; + } + /** * Returns a copy of this where wantToFail is set to true and history is updated to reflect this. */ @@ -295,13 +305,13 @@ public final class Node implements Nodelike { /** Returns a node with the status assigned to the given value */ public Node with(Status status) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a node with the type assigned to the given value */ public Node with(NodeType type) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a node with the flavor assigned to the given value */ @@ -309,31 +319,31 @@ public final class Node implements Nodelike { if (flavor.equals(this.flavor)) return this; History updateHistory = history.with(new History.Event(History.Event.Type.resized, agent, instant)); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, updateHistory, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with the reboot generation set to generation */ public Node withReboot(Generation generation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status.withReboot(generation), state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with the openStackId set */ public Node withOpenStackId(String openStackId) { return new Node(openStackId, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with model name set to given value */ public Node withModelName(String modelName) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with model name cleared */ public Node withoutModelName() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -364,55 +374,55 @@ public final class Node implements Nodelike { */ public Node with(Allocation allocation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a new Node without an allocation. */ public Node withoutAllocation() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node with IP config set to the given value. */ public Node with(IP.Config ipConfig) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node with the parent hostname assigned to the given value. */ public Node withParentHostname(String parentHostname) { return new Node(id, ipConfig, hostname, Optional.of(parentHostname), flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withReservedTo(TenantName tenant) { if (type != NodeType.host) throw new IllegalArgumentException("Only host nodes can be reserved, " + hostname + " has type " + type); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node which is not reserved to a tenant */ public Node withoutReservedTo() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withExclusiveToApplicationId(ApplicationId exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withExclusiveToClusterType(ClusterSpec.Type exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname, trustStoreItems); } /** Returns a copy of this node with switch hostname set to given value */ public Node withSwitchHostname(String switchHostname) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname)); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname), trustStoreItems); } /** Returns a copy of this node with switch hostname unset */ @@ -450,12 +460,18 @@ public final class Node implements Nodelike { /** Returns a copy of this node with the given history. */ public Node with(History history) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node with(Reports reports) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + } + + public Node with(List<TrustStoreItem> trustStoreItems) { + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems); } private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { @@ -594,6 +610,7 @@ public final class Node implements Nodelike { private Status status; private Reports reports; private History history; + private List<TrustStoreItem> trustStoreItems; private Builder(String id, String hostname, Flavor flavor, State state, NodeType type) { this.id = id; @@ -663,12 +680,18 @@ public final class Node implements Nodelike { return this; } + public Builder trustedCertificates(List<TrustStoreItem> trustStoreItems) { + this.trustStoreItems = trustStoreItems; + return this; + } + public Node build() { return new Node(id, Optional.ofNullable(ipConfig).orElse(IP.Config.EMPTY), hostname, Optional.ofNullable(parentHostname), - flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), - Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), - Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), - Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname)); + flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), + Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), + Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), + Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname), + Optional.ofNullable(trustStoreItems).orElseGet(List::of)); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java new file mode 100644 index 00000000000..6fb94d0bc62 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java @@ -0,0 +1,68 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.provision.node; + +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Inspector; + +import java.time.Instant; +import java.util.Objects; + +/** + * Contains the fingerprint and expiry of certificates in a hosts truststore. + * + * @author mortent + */ +public class TrustStoreItem { + private static final String FINGERPRINT_FIELD = "fingerprint"; + private static final String EXPIRY_FIELD = "expiry"; + + private final String fingerprint; + private final Instant expiry; + + public TrustStoreItem(String fingerprint, Instant expiry) { + this.fingerprint = fingerprint; + this.expiry = expiry; + } + + public String fingerprint() { + return fingerprint; + } + + public Instant expiry() { + return expiry; + } + + public void toSlime(Cursor trustedCertificatesRoot) { + Cursor object = trustedCertificatesRoot.addObject(); + object.setString(FINGERPRINT_FIELD, fingerprint); + object.setLong(EXPIRY_FIELD, expiry.toEpochMilli()); + } + + public static TrustStoreItem fromSlime(Inspector inspector) { + String fingerprint = inspector.field(FINGERPRINT_FIELD).asString(); + Instant expiry = Instant.ofEpochMilli(inspector.field(EXPIRY_FIELD).asLong()); + return new TrustStoreItem(fingerprint, expiry); + } + + @Override + public String toString() { + return "TrustedCertificate{" + + "fingerprint='" + fingerprint + '\'' + + ", expiry=" + expiry + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrustStoreItem that = (TrustStoreItem) o; + return Objects.equals(fingerprint, that.fingerprint) && Objects.equals(expiry, that.expiry); + } + + @Override + public int hashCode() { + return Objects.hash(fingerprint, expiry); + } +}
\ No newline at end of file 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 d5a4c459ef3..7e252391cb3 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 @@ -210,7 +210,7 @@ public class CuratorDatabaseClient { toState.isAllocated() ? node.allocation() : Optional.empty(), node.history().recordStateTransition(node.state(), toState, agent, clock.instant()), node.type(), node.reports(), node.modelName(), node.reservedTo(), - node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname()); + node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname(), node.trustedCertificates()); writeNode(toState, curatorTransaction, node, newNode); writtenNodes.add(newNode); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java index 19bbe92eff6..868837daeeb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java @@ -36,6 +36,7 @@ import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.OsVersion; import com.yahoo.vespa.hosted.provision.node.Reports; import com.yahoo.vespa.hosted.provision.node.Status; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -95,6 +96,7 @@ public class NodeSerializer { private static final String exclusiveToApplicationIdKey = "exclusiveTo"; private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType"; private static final String switchHostnameKey = "switchHostname"; + private static final String trustedCertificatesKey = "trustedCertificates"; // Node resource fields private static final String flavorKey = "flavor"; @@ -122,6 +124,10 @@ public class NodeSerializer { // Network port fields private static final String networkPortsKey = "networkPorts"; + // Trusted certificates fields + private static final String fingerprintKey = "fingerprint"; + private static final String expiresKey = "expires"; + // A cache of deserialized Node objects. The cache is keyed on the hash of serialized node data. // // Deserializing a Node from slime is expensive, and happens frequently. Node instances that have already been @@ -182,6 +188,7 @@ public class NodeSerializer { node.reservedTo().ifPresent(tenant -> object.setString(reservedToKey, tenant.value())); node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm())); node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name())); + trustedCertificatesToSlime(node.trustedCertificates(), object.setArray(trustedCertificatesKey)); } private void toSlime(Flavor flavor, Cursor object) { @@ -236,6 +243,14 @@ public class NodeSerializer { }); } + private void trustedCertificatesToSlime(List<TrustStoreItem> trustStoreItems, Cursor array) { + trustStoreItems.forEach(cert -> { + Cursor object = array.addObject(); + object.setString(fingerprintKey, cert.fingerprint()); + object.setLong(expiresKey, cert.expiry().toEpochMilli()); + }); + } + // ---------------- Deserialization -------------------------------------------------- public Node fromJson(Node.State state, byte[] data) { @@ -269,7 +284,8 @@ public class NodeSerializer { reservedToFromSlime(object.field(reservedToKey)), exclusiveToApplicationIdFromSlime(object.field(exclusiveToApplicationIdKey)), exclusiveToClusterTypeFromSlime(object.field(exclusiveToClusterTypeKey)), - switchHostnameFromSlime(object.field(switchHostnameKey))); + switchHostnameFromSlime(object.field(switchHostnameKey)), + trustedCertificatesFromSlime(object)); } private Status statusFromSlime(Inspector object) { @@ -419,6 +435,13 @@ public class NodeSerializer { return Optional.of(ClusterSpec.Type.from(object.asString())); } + private List<TrustStoreItem> trustedCertificatesFromSlime(Inspector object) { + return SlimeUtils.entriesStream(object.field(trustedCertificatesKey)) + .map(elem -> new TrustStoreItem(elem.field(fingerprintKey).asString(), + Instant.ofEpochMilli(elem.field(expiresKey).asLong()))) + .collect(Collectors.toList()); + } + // ----------------- Enum <-> string mappings ---------------------------------------- /** Returns the event type, or null if this event type should be ignored */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 8d37c13d2bc..5dfacc2c3d2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -25,6 +25,7 @@ import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.Report; import com.yahoo.vespa.hosted.provision.node.Reports; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.io.IOException; import java.io.InputStream; @@ -186,6 +187,8 @@ public class NodePatcher implements AutoCloseable { return node.withExclusiveToClusterType(SlimeUtils.optionalString(value).map(ClusterSpec.Type::valueOf).orElse(null)); case "switchHostname": return value.type() == Type.NIX ? node.withoutSwitchHostname() : node.withSwitchHostname(value.asString()); + case "trustStore": + return nodeWithTrustStore(node, value); default : throw new IllegalArgumentException("Could not apply field '" + name + "' on a node: No such modifiable field"); } @@ -230,6 +233,14 @@ public class NodePatcher implements AutoCloseable { return patchedNode; } + private Node nodeWithTrustStore(Node node, Inspector inspector) { + List<TrustStoreItem> trustStoreItems = + SlimeUtils.entriesStream(inspector) + .map(TrustStoreItem::fromSlime) + .collect(Collectors.toList()); + return node.with(trustStoreItems); + } + private Set<String> asStringSet(Inspector field) { if ( ! field.type().equals(Type.ARRAY)) throw new IllegalArgumentException("Expected an ARRAY value, got a " + field.type()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java index 2cf671514c4..80100128379 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java @@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Address; import com.yahoo.vespa.hosted.provision.node.History; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import com.yahoo.vespa.orchestrator.Orchestrator; import com.yahoo.vespa.orchestrator.status.HostInfo; import com.yahoo.vespa.orchestrator.status.HostStatus; @@ -182,6 +183,7 @@ class NodesResponse extends SlimeJsonResponse { node.modelName().ifPresent(modelName -> object.setString("modelName", modelName)); node.switchHostname().ifPresent(switchHostname -> object.setString("switchHostname", switchHostname)); nodeRepository.archiveUris().archiveUriFor(node).ifPresent(uri -> object.setString("archiveUri", uri)); + trustedCertsToSlime(node.trustedCertificates(), object); } private void toSlime(ApplicationId id, Cursor object) { @@ -228,6 +230,12 @@ class NodesResponse extends SlimeJsonResponse { addresses.forEach(address -> addressesArray.addString(address.hostname())); } + private void trustedCertsToSlime(List<TrustStoreItem> trustStoreItems, Cursor object) { + if (trustStoreItems.isEmpty()) return; + Cursor array = object.setArray("trustStore"); + trustStoreItems.forEach(cert -> cert.toSlime(array)); + } + private String lastElement(String path) { if (path.endsWith("/")) path = path.substring(0, path.length()-1); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java index 158a1d6e5ac..921b78252d3 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java @@ -29,6 +29,7 @@ import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.Report; import com.yahoo.vespa.hosted.provision.node.Reports; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import com.yahoo.vespa.hosted.provision.provisioning.FlavorConfigBuilder; import org.junit.Test; @@ -462,6 +463,16 @@ public class NodeSerializerTest { assertEquals(exclusiveToCluster, node.exclusiveToClusterType().get()); } + @Test + public void truststore_serialization() { + Node node = nodeSerializer.fromJson(State.active, nodeSerializer.toJson(createNode())); + assertEquals(List.of(), node.trustedCertificates()); + List<TrustStoreItem> trustStoreItems = List.of(new TrustStoreItem("foo", Instant.parse("2023-09-01T23:59:59Z")), new TrustStoreItem("bar", Instant.parse("2025-05-20T23:59:59Z"))); + node = node.with(trustStoreItems); + node = nodeSerializer.fromJson(State.active, nodeSerializer.toJson(node)); + assertEquals(trustStoreItems, node.trustedCertificates()); + } + private byte[] createNodeJson(String hostname, String... ipAddress) { String ipAddressJsonPart = ""; if (ipAddress.length > 0) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java index 6c052a6c364..88a8a22913e 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java @@ -1023,6 +1023,26 @@ public class NodesV2ApiTest { tester.assertPartialResponse(new Request("http://localhost:8080/nodes/v2/node/host4.yahoo.com"), "archiveUri", false); } + @Test + public void trusted_certificates_patch() throws IOException { + String url = "http://localhost:8080/nodes/v2/node/dockerhost1.yahoo.com"; + tester.assertPartialResponse(new Request(url), "\"trustStore\":[]", false); // initially empty list + + String trustStore = "\"trustStore\":[" + + "{" + + "\"fingerprint\":\"foo\"," + + "\"expiry\":1632302251000" + + "}," + + "{" + + "\"fingerprint\":\"bar\"," + + "\"expiry\":1758532706000" + + "}" + + "]"; + assertResponse(new Request(url, Utf8.toBytes("{"+trustStore+"}"), Request.Method.PATCH), + "{\"message\":\"Updated dockerhost1.yahoo.com\"}"); + tester.assertPartialResponse(new Request(url), trustStore, true); + } + private static String asDockerNodeJson(String hostname, String parentHostname, String... ipAddress) { return asDockerNodeJson(hostname, NodeType.tenant, parentHostname, ipAddress); } |