diff options
author | Martin Polden <mpolden@mpolden.no> | 2022-05-12 15:48:22 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2022-05-13 14:46:35 +0200 |
commit | 8327b0e89d21a7122019a3e342baa880f23a9898 (patch) | |
tree | 0758c28acec995475a406a1dba21e7affabd008f /node-repository | |
parent | c8671df2c8ca29a037e148f758e6092be6a6128c (diff) |
Use cloud account from model in host provisioning
Diffstat (limited to 'node-repository')
15 files changed, 178 insertions, 54 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 e6408a0345b..c3c4771ec1d 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 @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; @@ -55,6 +56,7 @@ public final class Node implements Nodelike { private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final Optional<String> switchHostname; private final List<TrustStoreItem> trustStoreItems; + private final Optional<CloudAccount> cloudAccount; /** Record of the last event of each type happening to this node */ private final History history; @@ -84,7 +86,8 @@ 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, List<TrustStoreItem> trustStoreItems) { + Optional<String> switchHostname, List<TrustStoreItem> trustStoreItems, + Optional<CloudAccount> cloudAccount) { 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"); @@ -101,7 +104,8 @@ 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()); + this.trustStoreItems = Objects.requireNonNull(trustStoreItems).stream().distinct().collect(Collectors.toUnmodifiableList()); + this.cloudAccount = Objects.requireNonNull(cloudAccount); if (state == State.active) requireNonEmpty(ipConfig.primary(), "Active node " + hostname + " must have at least one valid IP address"); @@ -115,6 +119,11 @@ public final class Node implements Nodelike { if (!ipConfig.pool().ipSet().isEmpty()) throw new IllegalArgumentException("A child node cannot have an IP address pool"); if (modelName.isPresent()) throw new IllegalArgumentException("A child node cannot have model name set"); if (switchHostname.isPresent()) throw new IllegalArgumentException("A child node cannot have switch hostname set"); + if (cloudAccount().isPresent()) throw new IllegalArgumentException("A child node cannot have cloud account set"); + } + + if (cloudAccount.isPresent() && exclusiveToApplicationId.isEmpty()) { + throw new IllegalArgumentException("Host in a custom cloud account must be exclusive to an application"); } if (type != NodeType.host && reservedTo.isPresent()) @@ -218,6 +227,11 @@ public final class Node implements Nodelike { return trustStoreItems; } + /** Returns the cloud account of this host. This is empty if the host is in the zone's default account */ + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + /** * Returns a copy of this where wantToFail is set to true and history is updated to reflect this. */ @@ -306,13 +320,15 @@ 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, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** Returns a node with the flavor assigned to the given value */ @@ -320,31 +336,37 @@ 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, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this with given id set */ public Node withId(String id) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, Optional.empty(), reservedTo, + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -378,55 +400,66 @@ 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, trustStoreItems); + Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + Optional.empty(), history, type, reports, modelName, reservedTo, + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } 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, trustStoreItems); + allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** 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, trustStoreItems); + allocation, history, type, reports, modelName, Optional.empty(), + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + Optional.ofNullable(exclusiveTo), switchHostname, trustStoreItems, cloudAccount); } /** 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), trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, Optional.ofNullable(switchHostname), trustStoreItems, cloudAccount); } /** Returns a copy of this node with switch hostname unset */ @@ -469,18 +502,20 @@ 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, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } public Node with(Reports reports) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } 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); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { @@ -602,6 +637,7 @@ public final class Node implements Nodelike { } public static class Builder { + private final String id; private final String hostname; private final Flavor flavor; @@ -620,6 +656,7 @@ public final class Node implements Nodelike { private Reports reports; private History history; private List<TrustStoreItem> trustStoreItems; + private CloudAccount cloudAccount; private Builder(String id, String hostname, Flavor flavor, State state, NodeType type) { this.id = id; @@ -694,14 +731,21 @@ public final class Node implements Nodelike { return this; } + public Builder cloudAccount(CloudAccount cloudAccount) { + this.cloudAccount = cloudAccount; + 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), - Optional.ofNullable(trustStoreItems).orElseGet(List::of)); + Optional.ofNullable(trustStoreItems).orElseGet(List::of), + Optional.ofNullable(cloudAccount)); } + } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java index 6eaee7b33de..0fa406ab0ef 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java @@ -245,10 +245,11 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { Version osVersion = nodeRepository().osVersions().targetFor(NodeType.host).orElse(Version.emptyVersion); List<Integer> provisionIndices = nodeRepository().database().readProvisionIndices(count); List<Node> hosts = hostProvisioner.provisionHosts(provisionIndices, NodeType.host, nodeResources, - ApplicationId.defaultId(), osVersion, HostSharing.shared, Optional.empty()) - .stream() - .map(ProvisionedHost::generateHost) - .collect(Collectors.toList()); + ApplicationId.defaultId(), osVersion, HostSharing.shared, + Optional.empty(), Optional.empty()) + .stream() + .map(ProvisionedHost::generateHost) + .collect(Collectors.toList()); nodeRepository().nodes().addNodes(hosts, Agent.DynamicProvisioningMaintainer); return hosts; } catch (NodeAllocationException | IllegalArgumentException | IllegalStateException e) { @@ -293,7 +294,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { // build() requires a version, even though it is not (should not be) used .vespaVersion(Vtag.currentVersion) .build(); - NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), nodeResources, false, true); + NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), nodeResources, false, true, Optional.empty()); int wantedGroups = 1; NodePrioritizer prioritizer = new NodePrioritizer(nodesAndHosts, applicationId, clusterSpec, nodeSpec, wantedGroups, 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 13dd458c041..b4e304155a6 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 @@ -208,7 +208,8 @@ 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.trustedCertificates()); + node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname(), + node.trustedCertificates(), node.cloudAccount()); 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 1e20b26ccd9..083b707b3c5 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 @@ -9,6 +9,7 @@ import com.google.common.util.concurrent.UncheckedExecutionException; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.DockerImage; @@ -98,6 +99,7 @@ public class NodeSerializer { private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType"; private static final String switchHostnameKey = "switchHostname"; private static final String trustedCertificatesKey = "trustedCertificates"; + private static final String cloudAccountKey = "cloudAccount"; // Node resource fields private static final String flavorKey = "flavor"; @@ -191,6 +193,7 @@ public class NodeSerializer { node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm())); node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name())); trustedCertificatesToSlime(node.trustedCertificates(), object.setArray(trustedCertificatesKey)); + node.cloudAccount().ifPresent(cloudAccount -> object.setString(cloudAccountKey, cloudAccount.value())); } private void toSlime(Flavor flavor, Cursor object) { @@ -287,7 +290,8 @@ public class NodeSerializer { SlimeUtils.optionalString(object.field(exclusiveToApplicationIdKey)).map(ApplicationId::fromSerializedForm), SlimeUtils.optionalString(object.field(exclusiveToClusterTypeKey)).map(ClusterSpec.Type::from), SlimeUtils.optionalString(object.field(switchHostnameKey)), - trustedCertificatesFromSlime(object)); + trustedCertificatesFromSlime(object), + SlimeUtils.optionalString(object.field(cloudAccountKey)).map(CloudAccount::new)); } private Status statusFromSlime(Inspector object) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index 763fe1cab6f..16ee8281b9a 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -107,7 +107,8 @@ public class GroupPreparer { application, osVersion, sharing, - Optional.of(cluster.type()))) + Optional.of(cluster.type()), + requestedNodes.cloudAccount())) .orElseGet(List::of); // At this point we have started provisioning of the hosts, the first priority is to make sure that diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java index ce6ac9d5f5f..b849fccfaa5 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -41,6 +42,8 @@ public interface HostProvisioner { * @param osVersion the OS version to use. If this version does not exist, implementations may choose a suitable * fallback version. * @param sharing puts requirements on sharing or exclusivity of the host to be provisioned. + * @param clusterType provision host exclusively for this cluster type + * @param cloudAccount the cloud account to use * @return list of {@link ProvisionedHost} describing the provisioned nodes */ List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, @@ -49,7 +52,8 @@ public interface HostProvisioner { ApplicationId applicationId, Version osVersion, HostSharing sharing, - Optional<ClusterSpec.Type> clusterType); + Optional<ClusterSpec.Type> clusterType, + Optional<CloudAccount> cloudAccount); /** * Continue provisioning of given list of Nodes. diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java index 5dce931427d..24743b47c8e 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java @@ -17,9 +17,7 @@ import com.yahoo.config.provision.ProvisionLogger; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.Zone; import com.yahoo.transaction.Mutex; -import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; -import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -107,7 +105,7 @@ public class NodeRepositoryProvisioner implements Provisioner { groups = target.groups(); resources = getNodeResources(cluster, target.nodeResources(), application); - nodeSpec = NodeSpec.from(target.nodes(), resources, exclusive, actual.canFail()); + nodeSpec = NodeSpec.from(target.nodes(), resources, exclusive, actual.canFail(), requested.cloudAccount()); } else { groups = 1; // type request with multiple groups is not supported diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java index b6c392c84db..cbd15a979d1 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.provisioning; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeFlavors; @@ -68,6 +69,9 @@ public interface NodeSpec { /** Returns true if nodes with non-active parent hosts should be rejected */ boolean rejectNonActiveParent(); + /** Returns the cloud account to use when fulfilling this spec or empty if none is explicitly requested */ + Optional<CloudAccount> cloudAccount(); + /** * Returns true if a node with given current resources and current spare host resources can be resized * in-place to resources in this spec. @@ -77,8 +81,8 @@ public interface NodeSpec { return false; } - static NodeSpec from(int nodeCount, NodeResources resources, boolean exclusive, boolean canFail) { - return new CountNodeSpec(nodeCount, resources, exclusive, canFail); + static NodeSpec from(int nodeCount, NodeResources resources, boolean exclusive, boolean canFail, Optional<CloudAccount> cloudAccount) { + return new CountNodeSpec(nodeCount, resources, exclusive, canFail, cloudAccount); } static NodeSpec from(NodeType type) { @@ -92,12 +96,14 @@ public interface NodeSpec { private final NodeResources requestedNodeResources; private final boolean exclusive; private final boolean canFail; + private final Optional<CloudAccount> cloudAccount; - private CountNodeSpec(int count, NodeResources resources, boolean exclusive, boolean canFail) { + private CountNodeSpec(int count, NodeResources resources, boolean exclusive, boolean canFail, Optional<CloudAccount> cloudAccount) { this.count = count; this.requestedNodeResources = Objects.requireNonNull(resources, "Resources must be specified"); - this.exclusive = exclusive; + this.exclusive = exclusive || cloudAccount.isPresent(); // Implicitly exclusive if using a custom cloud account this.canFail = canFail; + this.cloudAccount = Objects.requireNonNull(cloudAccount); } @Override @@ -144,7 +150,7 @@ public interface NodeSpec { @Override public NodeSpec fraction(int divisor) { - return new CountNodeSpec(count/divisor, requestedNodeResources, exclusive, canFail); + return new CountNodeSpec(count/divisor, requestedNodeResources, exclusive, canFail, cloudAccount); } @Override @@ -175,6 +181,11 @@ public interface NodeSpec { } @Override + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + + @Override public String toString() { return "request for " + count + " nodes with " + requestedNodeResources; } } @@ -243,6 +254,11 @@ public interface NodeSpec { } @Override + public Optional<CloudAccount> cloudAccount() { + return Optional.empty(); // Type spec does not support custom cloud accounts + } + + @Override public String toString() { return "request for all nodes of type '" + type + "'"; } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java index dbc92b1dbdf..ef6c0da9169 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java @@ -76,8 +76,8 @@ class Preparer { if (requestedNodes.rejectNonActiveParent()) { NodeList activeHosts = allNodesAndHosts.nodes().state(Node.State.active).parents().nodeType(requestedNodes.type().hostType()); accepted = accepted.stream() - .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent()) - .collect(Collectors.toList()); + .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent()) + .collect(Collectors.toList()); } replace(acceptedNodes, accepted); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java index b5fd8c8111f..65071ad848d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; @@ -34,10 +35,11 @@ public class ProvisionedHost { private final List<Address> nodeAddresses; private final NodeResources nodeResources; private final Version osVersion; + private final Optional<CloudAccount> cloudAccount; public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType, Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, - List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion) { + List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion, Optional<CloudAccount> cloudAccount) { this.id = Objects.requireNonNull(id, "Host id must be set"); this.hostHostname = Objects.requireNonNull(hostHostname, "Host hostname must be set"); this.hostFlavor = Objects.requireNonNull(hostFlavor, "Host flavor must be set"); @@ -47,6 +49,7 @@ public class ProvisionedHost { this.nodeAddresses = validateNodeAddresses(nodeAddresses); this.nodeResources = Objects.requireNonNull(nodeResources, "Node resources must be set"); this.osVersion = Objects.requireNonNull(osVersion, "OS version must be set"); + this.cloudAccount = Objects.requireNonNull(cloudAccount, "Cloud account must be set"); if (!hostType.isHost()) throw new IllegalArgumentException(hostType + " is not a host"); } @@ -60,11 +63,12 @@ public class ProvisionedHost { /** Generate {@link Node} instance representing the provisioned physical host */ public Node generateHost() { - Node.Builder builder = Node - .create(id, IP.Config.of(Set.of(), Set.of(), nodeAddresses), hostHostname, hostFlavor, hostType) - .status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion)))); + Node.Builder builder = Node.create(id, IP.Config.of(Set.of(), Set.of(), nodeAddresses), hostHostname, hostFlavor, + hostType) + .status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion)))); exclusiveToApplicationId.ifPresent(builder::exclusiveToApplicationId); exclusiveToClusterType.ifPresent(builder::exclusiveToClusterType); + cloudAccount.ifPresent(builder::cloudAccount); return builder.build(); } @@ -82,6 +86,7 @@ public class ProvisionedHost { public List<Address> nodeAddresses() { return nodeAddresses; } public NodeResources nodeResources() { return nodeResources; } public Version osVersion() { return osVersion; } + public Optional<CloudAccount> cloudAccount() { return cloudAccount; } public String nodeHostname() { return nodeAddresses.get(0).hostname(); } @@ -98,12 +103,13 @@ public class ProvisionedHost { exclusiveToClusterType.equals(that.exclusiveToClusterType) && nodeAddresses.equals(that.nodeAddresses) && nodeResources.equals(that.nodeResources) && - osVersion.equals(that.osVersion); + osVersion.equals(that.osVersion) && + cloudAccount.equals(that.cloudAccount); } @Override public int hashCode() { - return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion); + return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion, cloudAccount); } @Override @@ -118,6 +124,7 @@ public class ProvisionedHost { ", nodeAddresses=" + nodeAddresses + ", nodeResources=" + nodeResources + ", osVersion=" + osVersion + + ", cloudAccount=" + cloudAccount + '}'; } 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 729ea97d627..3659166c9da 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 @@ -183,6 +183,7 @@ class NodesResponse extends SlimeJsonResponse { node.switchHostname().ifPresent(switchHostname -> object.setString("switchHostname", switchHostname)); nodeRepository.archiveUris().archiveUriFor(node).ifPresent(uri -> object.setString("archiveUri", uri)); trustedCertsToSlime(node.trustedCertificates(), object); + node.cloudAccount().ifPresent(cloudAccount -> object.setString("cloudAccount", cloudAccount.value())); } private void toSlime(ApplicationId id, Cursor object) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java index 0a125c5fb95..c09376ff103 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.testutils; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; @@ -56,7 +57,8 @@ public class MockHostProvisioner implements HostProvisioner { @Override public List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, NodeType hostType, NodeResources resources, ApplicationId applicationId, Version osVersion, HostSharing sharing, - Optional<ClusterSpec.Type> clusterType) { + Optional<ClusterSpec.Type> clusterType, + Optional<CloudAccount> cloudAccount) { Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream().filter(f -> compatible(f, resources)) .findFirst() .orElseThrow(() -> new NodeAllocationException("No host flavor matches " + resources))); @@ -67,11 +69,12 @@ public class MockHostProvisioner implements HostProvisioner { hostHostname, hostFlavor, hostType, - Optional.empty(), + sharing == HostSharing.exclusive ? Optional.of(applicationId) : Optional.empty(), Optional.empty(), createAddressesForHost(hostType, hostFlavor, index), resources, - osVersion)); + osVersion, + cloudAccount)); } provisionedHosts.addAll(hosts); return hosts; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java index 64cee87d942..30d0f673fe1 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java @@ -5,10 +5,13 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.Cloud; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterResources; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.Flavor; +import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -542,6 +545,35 @@ public class DynamicProvisioningMaintainerTest { assertEquals(nodesBefore, nodesAfter); } + @Test + public void custom_cloud_account() { + DynamicProvisioningTester tester = new DynamicProvisioningTester(Cloud.builder().dynamicProvisioning(true).build(), + new MockNameResolver().mockAnyLookup()); + ProvisioningTester provisioningTester = tester.provisioningTester; + ApplicationId applicationId = ApplicationId.from("t1", "a1", "i1"); + + // Deployment requests capacity in custom account + ClusterSpec spec = ProvisioningTester.contentClusterSpec(); + ClusterResources resources = new ClusterResources(2, 1, new NodeResources(16, 24, 100, 1)); + CloudAccount cloudAccount = new CloudAccount("012345678912"); + Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount)); + List<HostSpec> prepared = provisioningTester.prepare(applicationId, spec, capacity); + + // Hosts are provisioned in requested account + tester.maintainer.maintain(); + List<ProvisionedHost> newHosts = tester.hostProvisioner.provisionedHosts(); + assertEquals(2, newHosts.size()); + assertTrue(newHosts.stream().allMatch(host -> host.cloudAccount().get().equals(cloudAccount))); + for (var host : newHosts) { + provisioningTester.nodeRepository().nodes().setReady(host.hostHostname(), Agent.operator, getClass().getSimpleName()); + } + provisioningTester.prepareAndActivateInfraApplication(DynamicProvisioningTester.tenantHostApp, NodeType.host); + NodeList activeHosts = provisioningTester.nodeRepository().nodes().list(Node.State.active).nodeType(NodeType.host); + assertEquals(2, activeHosts.size()); + assertTrue(activeHosts.stream().allMatch(host -> host.cloudAccount().get().equals(cloudAccount))); + assertEquals(2, provisioningTester.activate(applicationId, prepared).size()); + } + private void assertCfghost3IsActive(DynamicProvisioningTester tester) { assertEquals(5, tester.nodeRepository.nodes().list(Node.State.active).size()); assertEquals(3, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.confighost).size()); 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 67b9569d8bf..bc7d3104a2f 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 @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.component.Vtag; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.InstanceName; @@ -488,6 +489,17 @@ public class NodeSerializerTest { assertEquals(trustStoreItems, node.trustedCertificates()); } + @Test + public void cloud_account_serialization() { + CloudAccount account = new CloudAccount("012345678912"); + Node node = Node.create("id", "host1.example.com", nodeFlavors.getFlavorOrThrow("default"), State.provisioned, NodeType.host) + .cloudAccount(account) + .exclusiveToApplicationId(ApplicationId.defaultId()) + .build(); + node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(node)); + assertEquals(account, node.cloudAccount().get()); + } + 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/provisioning/DynamicProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java index 97b664b6ba4..436cf880f1c 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java @@ -73,7 +73,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, null); // Provision shared hosts prepareAndActivate(application1, clusterSpec("mycluster"), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(100, 101, 102, 103), NodeType.host, resources, application1, - Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content)); + Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content), Optional.empty()); // Total of 8 nodes should now be in node-repo, 4 active hosts and 4 active nodes assertEquals(8, tester.nodeRepository().nodes().list().size()); @@ -97,7 +97,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, application3); prepareAndActivate(application3, clusterSpec("mycluster", true), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(104, 105, 106, 107), NodeType.host, resources, application3, - Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content)); + Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content), Optional.empty()); // Total of 20 nodes should now be in node-repo, 8 active hosts and 12 active nodes assertEquals(20, tester.nodeRepository().nodes().list().size()); @@ -477,7 +477,7 @@ public class DynamicProvisioningTest { return provisionedHost; }) .collect(Collectors.toList()); - }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any()); + }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any(), any()); } } |