summaryrefslogtreecommitdiffstats
path: root/node-repository
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-05-12 15:48:22 +0200
committerMartin Polden <mpolden@mpolden.no>2022-05-13 14:46:35 +0200
commit8327b0e89d21a7122019a3e342baa880f23a9898 (patch)
tree0758c28acec995475a406a1dba21e7affabd008f /node-repository
parentc8671df2c8ca29a037e148f758e6092be6a6128c (diff)
Use cloud account from model in host provisioning
Diffstat (limited to 'node-repository')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java90
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java11
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java26
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java19
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java1
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java9
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java32
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java12
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java6
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());
}
}