summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2021-07-27 16:02:01 +0200
committerGitHub <noreply@github.com>2021-07-27 16:02:01 +0200
commit8a450d4b8d7553c37a6361711b424df01a876637 (patch)
treea0ebc8196259dff8d0c8a3415ecdb637a2088003
parent048672327aac65c13b29cf6b3a1076496d2b60c7 (diff)
parent72099d6016f7a4fd4b0149d61562cde8e5197004 (diff)
Merge pull request #18648 from vespa-engine/freva/shared-cc
Support reserving hosts to nodes of given cluster type
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java25
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java4
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java2
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java83
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java19
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java8
-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.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java11
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java23
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java5
-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.java9
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java28
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java8
23 files changed, 194 insertions, 79 deletions
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java b/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java
index 129e1020b04..a4e25288d1d 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java
@@ -2,10 +2,12 @@
package com.yahoo.vespa.flags.custom;
import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
/**
@@ -18,19 +20,18 @@ import java.util.Set;
public class HostResources {
private static final Set<String> validDiskSpeeds = Set.of("slow", "fast");
private static final Set<String> validStorageTypes = Set.of("remote", "local");
+ private static final Set<String> validClusterTypes = Set.of("container", "content", "combined", "admin");
private final double vcpu;
-
private final double memoryGb;
-
private final double diskGb;
-
private final double bandwidthGbps;
private final String diskSpeed;
-
private final String storageType;
+ private final Optional<String> clusterType;
+
private final int containers;
@JsonCreator
@@ -40,6 +41,7 @@ public class HostResources {
@JsonProperty("bandwidthGbps") Double bandwidthGbps,
@JsonProperty("diskSpeed") String diskSpeed,
@JsonProperty("storageType") String storageType,
+ @JsonProperty("clusterType") String clusterType,
@JsonProperty("containers") Integer containers) {
this.vcpu = requirePositive("vcpu", vcpu);
this.memoryGb = requirePositive("memoryGb", memoryGb);
@@ -47,6 +49,7 @@ public class HostResources {
this.bandwidthGbps = requirePositive("bandwidthGbps", bandwidthGbps);
this.diskSpeed = validateEnum("diskSpeed", validDiskSpeeds, diskSpeed);
this.storageType = validateEnum("storageType", validStorageTypes, storageType);
+ this.clusterType = Optional.ofNullable(clusterType).map(cType -> validateEnum("clusterType", validClusterTypes, cType));
this.containers = requirePositive("containers", containers);
}
@@ -68,9 +71,19 @@ public class HostResources {
@JsonProperty("storageType")
public String storageType() { return storageType; }
+ @JsonProperty("clusterType")
+ public String clusterTypeOrNull() { return clusterType.orElse(null); }
+
+ @JsonIgnore
+ public Optional<String> clusterType() { return clusterType; }
+
@JsonProperty("containers")
public int containers() { return containers; }
+ public boolean satisfiesClusterType(String clusterType) {
+ return this.clusterType.map(clusterType::equalsIgnoreCase).orElse(true);
+ }
+
private static double requirePositive(String name, Double value) {
requireNonNull(name, value);
if (value <= 0)
@@ -106,6 +119,7 @@ public class HostResources {
", bandwidthGbps=" + bandwidthGbps +
", diskSpeed='" + diskSpeed + '\'' +
", storageType='" + storageType + '\'' +
+ ", clusterType='" + clusterType + '\'' +
", containers=" + containers +
'}';
}
@@ -121,11 +135,12 @@ public class HostResources {
Double.compare(resources.bandwidthGbps, bandwidthGbps) == 0 &&
diskSpeed.equals(resources.diskSpeed) &&
storageType.equals(resources.storageType) &&
+ clusterType.equals(resources.clusterType) &&
containers == resources.containers;
}
@Override
public int hashCode() {
- return Objects.hash(vcpu, memoryGb, diskGb, bandwidthGbps, diskSpeed, storageType, containers);
+ return Objects.hash(vcpu, memoryGb, diskGb, bandwidthGbps, diskSpeed, storageType, clusterType, containers);
}
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java b/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java
index c952161cf72..afbb2ce00b3 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java
@@ -52,8 +52,8 @@ public class SharedHost {
}
@JsonIgnore
- public boolean isEnabled() {
- return resources.size() > 0;
+ public boolean isEnabled(String clusterType) {
+ return resources.stream().anyMatch(hr -> hr.satisfiesClusterType(clusterType));
}
@JsonIgnore
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java
index 956048f6e24..32098ca4cce 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java
@@ -17,7 +17,7 @@ class PermanentFlagsTest {
public void testSharedHostFlag() {
SharedHost sharedHost = new SharedHost(List.of(new HostResources(
4.0, 16.0, 50.0, 0.3,
- "fast", "local",
+ "fast", "local", "admin",
10)),
null);
testGeneric(PermanentFlags.SHARED_HOST, sharedHost);
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java b/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java
index f0a11f244a4..2c78dda48e5 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java
@@ -12,8 +12,8 @@ import static org.junit.Assert.assertEquals;
public class SharedHostTest {
@Test
public void serialization() throws IOException {
- verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", 5)), 6));
- verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", 5)), null));
+ verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", "container", 5)), 6));
+ verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", "admin", 5)), null));
}
private void verifySerialization(SharedHost sharedHost) throws IOException {
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 3dbafdc2aba..240e041a504 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
@@ -46,7 +47,8 @@ public final class Node implements Nodelike {
private final Reports reports;
private final Optional<String> modelName;
private final Optional<TenantName> reservedTo;
- private final Optional<ApplicationId> exclusiveTo;
+ private final Optional<ApplicationId> exclusiveToApplicationId;
+ private final Optional<ClusterSpec.Type> exclusiveToClusterType;
private final Optional<String> switchHostname;
/** Record of the last event of each type happening to this node */
@@ -76,7 +78,8 @@ public final class Node implements Nodelike {
public Node(String id, IP.Config ipConfig, String hostname, Optional<String> parentHostname,
Flavor flavor, Status status, State state, Optional<Allocation> allocation, History history, NodeType type,
Reports reports, Optional<String> modelName, Optional<TenantName> reservedTo,
- Optional<ApplicationId> exclusiveTo, Optional<String> switchHostname) {
+ Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType,
+ Optional<String> switchHostname) {
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");
@@ -90,7 +93,8 @@ public final class Node implements Nodelike {
this.reports = Objects.requireNonNull(reports, "A null reports is not permitted");
this.modelName = Objects.requireNonNull(modelName, "A null modelName is not permitted");
this.reservedTo = Objects.requireNonNull(reservedTo, "reservedTo cannot be null");
- this.exclusiveTo = Objects.requireNonNull(exclusiveTo, "exclusiveTo cannot be null");
+ 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");
if (state == State.active)
@@ -110,8 +114,11 @@ public final class Node implements Nodelike {
if (type != NodeType.host && reservedTo.isPresent())
throw new IllegalArgumentException("Only tenant hosts can be reserved to a tenant");
- if (type != NodeType.host && exclusiveTo.isPresent())
+ if (type != NodeType.host && exclusiveToApplicationId.isPresent())
throw new IllegalArgumentException("Only tenant hosts can be exclusive to an application");
+
+ if (type != NodeType.host && exclusiveToClusterType.isPresent())
+ throw new IllegalArgumentException("Only tenant hosts can be exclusive to a cluster type");
}
/** Returns the IP config of this node */
@@ -182,11 +189,18 @@ public final class Node implements Nodelike {
public Optional<TenantName> reservedTo() { return reservedTo; }
/**
- * Returns the application this node is exclusive to, if any. Only hosts can be exclusive to an application.
+ * Returns the application this host is exclusive to, if any. Only tenant hosts can be exclusive to an application.
* If this is set, resources on this host cannot be allocated to any other application. This is set during
* provisioning and applies for the entire lifetime of the host
*/
- public Optional<ApplicationId> exclusiveTo() { return exclusiveTo; }
+ public Optional<ApplicationId> exclusiveToApplicationId() { return exclusiveToApplicationId; }
+
+ /**
+ * Returns the cluster type this host is exclusive to, if any. Only tenant hosts can be exclusive to a cluster type.
+ * If this is set, resources on this host cannot be allocated to any other cluster type. This is set during
+ * provisioning and applies for the entire lifetime of the host
+ */
+ public Optional<ClusterSpec.Type> exclusiveToClusterType() { return exclusiveToClusterType; }
/** Returns the hostname of the switch this node is connected to, if any */
public Optional<String> switchHostname() {
@@ -281,13 +295,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, exclusiveTo, switchHostname);
+ reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** Returns a node with the flavor assigned to the given value */
@@ -295,31 +309,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, exclusiveTo, switchHostname);
+ reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** Returns a copy of this with a history record saying it was detected to be down at this instant */
@@ -350,50 +364,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, exclusiveTo, switchHostname);
+ Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
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), exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
/** 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(), exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
- public Node withExclusiveTo(ApplicationId exclusiveTo) {
+ public Node withExclusiveToApplicationId(ApplicationId exclusiveTo) {
return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state,
- allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname);
+ }
+
+ 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);
}
/** 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, exclusiveTo, Optional.ofNullable(switchHostname));
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname));
}
/** Returns a copy of this node with switch hostname unset */
@@ -431,12 +450,12 @@ 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, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
public Node with(Reports reports) {
return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state,
- allocation, history, type, reports, modelName, reservedTo, exclusiveTo, switchHostname);
+ allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname);
}
private static Optional<String> requireNonEmptyString(Optional<String> value, String message) {
@@ -567,7 +586,8 @@ public final class Node implements Nodelike {
private String parentHostname;
private String modelName;
private TenantName reservedTo;
- private ApplicationId exclusiveTo;
+ private ApplicationId exclusiveToApplicationId;
+ private ClusterSpec.Type exclusiveToClusterType;
private String switchHostname;
private Allocation allocation;
private IP.Config ipConfig;
@@ -598,8 +618,13 @@ public final class Node implements Nodelike {
return this;
}
- public Builder exclusiveTo(ApplicationId exclusiveTo) {
- this.exclusiveTo = exclusiveTo;
+ public Builder exclusiveToApplicationId(ApplicationId exclusiveTo) {
+ this.exclusiveToApplicationId = exclusiveTo;
+ return this;
+ }
+
+ public Builder exclusiveToClusterType(ClusterSpec.Type exclusiveTo) {
+ this.exclusiveToClusterType = exclusiveTo;
return this;
}
@@ -642,8 +667,8 @@ public final class Node implements Nodelike {
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(exclusiveTo),
- Optional.ofNullable(switchHostname));
+ Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId),
+ Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname));
}
}
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 0eb2038e233..78686d8aeed 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
@@ -206,7 +206,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer {
return nodeList.stream()
.filter(node -> Nodes.canAllocateTenantNodeTo(node, true))
.filter(node -> node.reservedTo().isEmpty())
- .filter(node -> node.exclusiveTo().isEmpty())
+ .filter(node -> node.exclusiveToApplicationId().isEmpty())
.collect(Collectors.toMap(Node::hostname, Function.identity()));
}
@@ -245,7 +245,7 @@ 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)
+ ApplicationId.defaultId(), osVersion, HostSharing.shared, Optional.empty())
.stream()
.map(ProvisionedHost::generateHost)
.collect(Collectors.toList());
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 da4ab528630..d5a4c459ef3 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.exclusiveTo(), node.switchHostname());
+ node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname());
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 dff4a66bd42..19bbe92eff6 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
@@ -10,6 +10,7 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.InstanceName;
@@ -91,7 +92,8 @@ public class NodeSerializer {
private static final String reportsKey = "reports";
private static final String modelNameKey = "modelName";
private static final String reservedToKey = "reservedTo";
- private static final String exclusiveToKey = "exclusiveTo";
+ private static final String exclusiveToApplicationIdKey = "exclusiveTo";
+ private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType";
private static final String switchHostnameKey = "switchHostname";
// Node resource fields
@@ -178,7 +180,8 @@ public class NodeSerializer {
node.reports().toSlime(object, reportsKey);
node.modelName().ifPresent(modelName -> object.setString(modelNameKey, modelName));
node.reservedTo().ifPresent(tenant -> object.setString(reservedToKey, tenant.value()));
- node.exclusiveTo().ifPresent(applicationId -> object.setString(exclusiveToKey, applicationId.serializedForm()));
+ node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm()));
+ node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name()));
}
private void toSlime(Flavor flavor, Cursor object) {
@@ -264,7 +267,8 @@ public class NodeSerializer {
Reports.fromSlime(object.field(reportsKey)),
modelNameFromSlime(object),
reservedToFromSlime(object.field(reservedToKey)),
- exclusiveToFromSlime(object.field(exclusiveToKey)),
+ exclusiveToApplicationIdFromSlime(object.field(exclusiveToApplicationIdKey)),
+ exclusiveToClusterTypeFromSlime(object.field(exclusiveToClusterTypeKey)),
switchHostnameFromSlime(object.field(switchHostnameKey)));
}
@@ -401,13 +405,20 @@ public class NodeSerializer {
return Optional.of(TenantName.from(object.asString()));
}
- private Optional<ApplicationId> exclusiveToFromSlime(Inspector object) {
+ private Optional<ApplicationId> exclusiveToApplicationIdFromSlime(Inspector object) {
if (! object.valid()) return Optional.empty();
if (object.type() != Type.STRING)
throw new IllegalArgumentException("Expected 'exclusiveTo' to be a string but is " + object);
return Optional.of(ApplicationId.fromSerializedForm(object.asString()));
}
+ private Optional<ClusterSpec.Type> exclusiveToClusterTypeFromSlime(Inspector object) {
+ if (! object.valid()) return Optional.empty();
+ if (object.type() != Type.STRING)
+ throw new IllegalArgumentException("Expected 'exclusiveToClusterType' to be a string but is " + object);
+ return Optional.of(ClusterSpec.Type.from(object.asString()));
+ }
+
// ----------------- 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/provisioning/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
index abd910485ac..868f554e2ff 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
@@ -11,7 +11,7 @@ import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.provision.NodeRepository;
-import java.util.function.Supplier;
+import java.util.function.Function;
/**
* Defines the policies for assigning cluster capacity in various environments
@@ -22,11 +22,11 @@ import java.util.function.Supplier;
public class CapacityPolicies {
private final Zone zone;
- private final Supplier<Boolean> sharedHosts;
+ private final Function<ClusterSpec.Type, Boolean> sharedHosts;
public CapacityPolicies(NodeRepository nodeRepository) {
this.zone = nodeRepository.zone();
- this.sharedHosts = PermanentFlags.SHARED_HOST.bindTo(nodeRepository.flagSource()).value()::isEnabled;
+ this.sharedHosts = type -> PermanentFlags.SHARED_HOST.bindTo(nodeRepository.flagSource()).value().isEnabled(type.name());
}
public int decideSize(int requested, Capacity capacity, ClusterSpec cluster, ApplicationId application) {
@@ -66,7 +66,7 @@ public class CapacityPolicies {
// Use small logserver in dev system
return new NodeResources(0.1, 1, 10, 0.3);
}
- return zone.getCloud().dynamicProvisioning() && ! sharedHosts.get() ?
+ return zone.getCloud().dynamicProvisioning() && ! sharedHosts.apply(clusterType) ?
new NodeResources(0.5, 4, 50, 0.3) :
new NodeResources(0.5, 2, 50, 0.3);
}
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 5d45bed19e8..aad32f64fa8 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
@@ -81,7 +81,8 @@ public class GroupPreparer {
hostType,
deficit.resources(),
application, osVersion,
- sharing))
+ sharing,
+ Optional.of(cluster.type())))
.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 bfb526a518f..77bee2b346e 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,11 +3,13 @@ package com.yahoo.vespa.hosted.provision.provisioning;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
/**
@@ -48,7 +50,8 @@ public interface HostProvisioner {
NodeResources resources,
ApplicationId applicationId,
Version osVersion,
- HostSharing sharing);
+ HostSharing sharing,
+ Optional<ClusterSpec.Type> clusterType);
/**
* Continue provisioning of given list of Nodes.
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
index b5579451a0e..bdb7cb4b64d 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
@@ -189,10 +189,13 @@ class NodeAllocation {
private boolean violatesExclusivity(NodeCandidate candidate) {
if (candidate.parentHostname().isEmpty()) return false;
- // In dynamic provisioned zones a node requiring exclusivity must be on a host that has exclusiveTo equal to its owner
- if (nodeRepository.zone().getCloud().dynamicProvisioning())
- return requestedNodes.isExclusive() &&
- ! candidate.parent.flatMap(Node::exclusiveTo).map(application::equals).orElse(false);
+ // In dynamic provisioned zones, exclusivity is violated if...
+ if (nodeRepository.zone().getCloud().dynamicProvisioning()) {
+ // If either the parent is dedicated to a cluster type different from this cluster
+ return ! candidate.parent.flatMap(Node::exclusiveToClusterType).map(cluster.type()::equals).orElse(true) ||
+ // or this cluster is requiring exclusivity, but the host is exclusive to a different owner
+ (requestedNodes.isExclusive() && !candidate.parent.flatMap(Node::exclusiveToApplicationId).map(application::equals).orElse(false));
+ }
// In non-dynamic provisioned zones we require that if either of the nodes on the host requires exclusivity,
// then all the nodes on the host must have the same owner
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
index 5fe10f09f8a..6568046991b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
@@ -136,7 +136,8 @@ public class NodePrioritizer {
if ( ! Nodes.canAllocateTenantNodeTo(host, dynamicProvisioning)) continue;
if (host.reservedTo().isPresent() && !host.reservedTo().get().equals(application.tenant())) continue;
if (host.reservedTo().isPresent() && application.instance().isTester()) continue;
- if (host.exclusiveTo().isPresent()) continue; // Never allocate new nodes to exclusive hosts
+ if (host.exclusiveToApplicationId().isPresent()) continue; // Never allocate new nodes to exclusive hosts
+ if ( ! host.exclusiveToClusterType().map(clusterSpec.type()::equals).orElse(true)) continue;
if (spareHosts.contains(host) && !canAllocateToSpareHosts) continue;
if ( ! capacity.hasCapacity(host, requestedNodes.resources().get())) continue;
if ( ! allNodes.childrenOf(host).owner(application).cluster(clusterSpec.id()).isEmpty()) continue;
@@ -183,7 +184,7 @@ public class NodePrioritizer {
spareHosts.contains(parent.get()),
isSurplus,
false,
- parent.get().exclusiveTo().isEmpty()
+ parent.get().exclusiveToApplicationId().isEmpty()
&& requestedNodes.canResize(node.resources(),
capacity.freeCapacityOf(parent.get(), false),
topologyChange,
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java
index 2b64cc86c9a..0b007bf4a41 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java
@@ -43,7 +43,7 @@ public class NodeResourceLimits {
if (candidateNode.type() != NodeType.tenant) return true; // Resource limits only apply to tenant nodes
// This node is allocated exclusively if that has been explicitly requested, or if the host of the node was
// provisioned exclusively
- boolean exclusive = cluster.isExclusive() || candidateNode.parent.flatMap(Node::exclusiveTo).isPresent();
+ boolean exclusive = cluster.isExclusive() || candidateNode.parent.flatMap(Node::exclusiveToApplicationId).isPresent();
return isWithinRealLimits(nodeRepository.resourcesCalculator().realResourcesOf(candidateNode, nodeRepository, exclusive),
cluster.type());
}
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 caaea1167b5..f0eb4a59af8 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.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
@@ -28,18 +29,21 @@ public class ProvisionedHost {
private final String hostHostname;
private final Flavor hostFlavor;
private final NodeType hostType;
- private final Optional<ApplicationId> exclusiveTo;
+ private final Optional<ApplicationId> exclusiveToApplicationId;
+ private final Optional<ClusterSpec.Type> exclusiveToClusterType;
private final List<Address> nodeAddresses;
private final NodeResources nodeResources;
private final Version osVersion;
- public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType, Optional<ApplicationId> exclusiveTo,
+ public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType,
+ Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType,
List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion) {
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");
this.hostType = Objects.requireNonNull(hostType, "Host type must be set");
- this.exclusiveTo = Objects.requireNonNull(exclusiveTo, "exclusiveTo must be set");
+ this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId must be set");
+ this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType must be set");
this.nodeAddresses = validateNodeAddresses(nodeAddresses);
this.nodeResources = Objects.requireNonNull(nodeResources, "Node resources must be set");
this.osVersion = Objects.requireNonNull(osVersion, "OS version must be set");
@@ -59,7 +63,8 @@ public class ProvisionedHost {
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))));
- exclusiveTo.ifPresent(builder::exclusiveTo);
+ exclusiveToApplicationId.ifPresent(builder::exclusiveToApplicationId);
+ exclusiveToClusterType.ifPresent(builder::exclusiveToClusterType);
return builder.build();
}
@@ -98,6 +103,9 @@ public class ProvisionedHost {
return id.equals(that.id) &&
hostHostname.equals(that.hostHostname) &&
hostFlavor.equals(that.hostFlavor) &&
+ hostType == that.hostType &&
+ exclusiveToApplicationId.equals(that.exclusiveToApplicationId) &&
+ exclusiveToClusterType.equals(that.exclusiveToClusterType) &&
nodeAddresses.equals(that.nodeAddresses) &&
nodeResources.equals(that.nodeResources) &&
osVersion.equals(that.osVersion);
@@ -105,7 +113,7 @@ public class ProvisionedHost {
@Override
public int hashCode() {
- return Objects.hash(id, hostHostname, hostFlavor, nodeAddresses, nodeResources, osVersion);
+ return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion);
}
@Override
@@ -114,7 +122,10 @@ public class ProvisionedHost {
"id='" + id + '\'' +
", hostHostname='" + hostHostname + '\'' +
", hostFlavor=" + hostFlavor +
- ", nodeAddresses='" + nodeAddresses + '\'' +
+ ", hostType=" + hostType +
+ ", exclusiveToApplicationId=" + exclusiveToApplicationId +
+ ", exclusiveToClusterType=" + exclusiveToClusterType +
+ ", nodeAddresses=" + nodeAddresses +
", nodeResources=" + nodeResources +
", osVersion=" + osVersion +
'}';
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 1bea7056790..8d37c13d2bc 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.restapi;
import com.google.common.base.Suppliers;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.NodeFlavors;
import com.yahoo.config.provision.NodeResources;
@@ -179,7 +180,10 @@ public class NodePatcher implements AutoCloseable {
case "reservedTo":
return value.type() == Type.NIX ? node.withoutReservedTo() : node.withReservedTo(TenantName.from(value.asString()));
case "exclusiveTo":
- return node.withExclusiveTo(SlimeUtils.optionalString(value).map(ApplicationId::fromSerializedForm).orElse(null));
+ case "exclusiveToApplicationId":
+ return node.withExclusiveToApplicationId(SlimeUtils.optionalString(value).map(ApplicationId::fromSerializedForm).orElse(null));
+ case "exclusiveToClusterType":
+ 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());
default :
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 1b3b2f81f11..53b6a91d962 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
@@ -129,7 +129,8 @@ class NodesResponse extends SlimeJsonResponse {
object.setString("openStackId", node.id());
object.setString("flavor", node.flavor().name());
node.reservedTo().ifPresent(reservedTo -> object.setString("reservedTo", reservedTo.value()));
- node.exclusiveTo().ifPresent(exclusiveTo -> object.setString("exclusiveTo", exclusiveTo.serializedForm()));
+ node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString("exclusiveTo", applicationId.serializedForm()));
+ node.exclusiveToClusterType().ifPresent(clusterType -> object.setString("exclusiveToClusterType", clusterType.name()));
if (node.flavor().isConfigured())
object.setDouble("cpuCores", node.flavor().resources().vcpu());
NodeResourcesSerializer.toSlime(node.flavor().resources(), object.setObject("resources"));
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java
index 24e297de179..c35d6b89090 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java
@@ -283,7 +283,7 @@ public class NodesV2ApiHandler extends LoggingRequestHandler {
optionalString(inspector.field("parentHostname")).ifPresent(builder::parentHostname);
optionalString(inspector.field("modelName")).ifPresent(builder::modelName);
optionalString(inspector.field("reservedTo")).map(TenantName::from).ifPresent(builder::reservedTo);
- optionalString(inspector.field("exclusiveTo")).map(ApplicationId::fromSerializedForm).ifPresent(builder::exclusiveTo);
+ optionalString(inspector.field("exclusiveTo")).map(ApplicationId::fromSerializedForm).ifPresent(builder::exclusiveToApplicationId);
optionalString(inspector.field("switchHostname")).ifPresent(builder::switchHostname);
return builder.build();
}
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 25e74df677b..50585c19946 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.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
@@ -54,7 +55,8 @@ public class MockHostProvisioner implements HostProvisioner {
@Override
public List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, NodeType hostType, NodeResources resources,
- ApplicationId applicationId, Version osVersion, HostSharing sharing) {
+ ApplicationId applicationId, Version osVersion, HostSharing sharing,
+ Optional<ClusterSpec.Type> clusterType) {
Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream().filter(f -> compatible(f, resources))
.findFirst()
.orElseThrow(() -> new OutOfCapacityException("No host flavor matches " + resources)));
@@ -66,6 +68,7 @@ public class MockHostProvisioner implements HostProvisioner {
hostFlavor,
hostType,
Optional.empty(),
+ Optional.empty(),
createAddressesForHost(hostType, hostFlavor, index),
resources,
osVersion));
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 523ceeb94ce..158a1d6e5ac 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
@@ -7,6 +7,7 @@ import com.yahoo.component.Vtag;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.NetworkPorts;
import com.yahoo.config.provision.NodeFlavors;
@@ -450,12 +451,15 @@ public class NodeSerializerTest {
Node.Builder builder = Node.create("myId", IP.Config.EMPTY, "myHostname",
nodeFlavors.getFlavorOrThrow("default"), NodeType.host);
Node node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(builder.build()));
- assertFalse(node.exclusiveTo().isPresent());
+ assertFalse(node.exclusiveToApplicationId().isPresent());
+ assertFalse(node.exclusiveToClusterType().isPresent());
- ApplicationId exclusiveTo = ApplicationId.from("tenant1", "app1", "instance1");
- node = builder.exclusiveTo(exclusiveTo).build();
+ ApplicationId exclusiveToApp = ApplicationId.from("tenant1", "app1", "instance1");
+ ClusterSpec.Type exclusiveToCluster = ClusterSpec.Type.admin;
+ node = builder.exclusiveToApplicationId(exclusiveToApp).exclusiveToClusterType(exclusiveToCluster).build();
node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(node));
- assertEquals(exclusiveTo, node.exclusiveTo().get());
+ assertEquals(exclusiveToApp, node.exclusiveToApplicationId().get());
+ assertEquals(exclusiveToCluster, node.exclusiveToClusterType().get());
}
private byte[] createNodeJson(String hostname, String... ipAddress) {
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 0b7b9f2fa13..3a7c61f68a1 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
@@ -28,6 +28,7 @@ import org.junit.Test;
import java.time.Instant;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -72,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);
+ Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content));
// Total of 8 nodes should now be in node-repo, 4 active hosts and 4 active nodes
assertEquals(8, tester.nodeRepository().nodes().list().size());
@@ -96,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);
+ Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content));
// Total of 20 nodes should now be in node-repo, 8 active hosts and 12 active nodes
assertEquals(20, tester.nodeRepository().nodes().list().size());
@@ -468,7 +469,7 @@ public class DynamicProvisioningTest {
}).collect(Collectors.toSet());
Node parent = Node.create(hostHostname, new IP.Config(Set.of(hostIp), pool), hostHostname, hostFlavor, NodeType.host)
- .exclusiveTo(exclusiveTo).build();
+ .exclusiveToApplicationId(exclusiveTo).build();
Node child = Node.reserve(Set.of("::" + hostIndex + ":1"), hostHostname + "-1", hostHostname, nodeResources, NodeType.tenant).build();
ProvisionedHost provisionedHost = mock(ProvisionedHost.class);
when(provisionedHost.generateHost()).thenReturn(parent);
@@ -476,7 +477,7 @@ public class DynamicProvisioningTest {
return provisionedHost;
})
.collect(Collectors.toList());
- }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any());
+ }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any());
}
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
index 18fcb56d87f..fdef4135c16 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
@@ -333,6 +333,34 @@ public class VirtualNodeProvisioningTest {
assertNodeParentReservation(tester.getNodes(application1_1).asList(), Optional.empty(), tester); // Reservation is cleared after activation
}
+ @Test
+ public void respects_exclusive_to_cluster_type() {
+ NodeResources resources = new NodeResources(10, 10, 100, 10);
+ ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build();
+
+ tester.makeReadyNodes(10, resources, Optional.empty(), NodeType.host, 1);
+ tester.activateTenantHosts();
+ // All hosts are exclusive to content nodes
+ tester.patchNodes(tester.nodeRepository().nodes().list().asList(), node -> node.withExclusiveToClusterType(ClusterSpec.Type.content));
+
+ Version wantedVespaVersion = Version.fromString("6.39");
+ try {
+ // No capacity for 'container' nodes
+ tester.prepare(applicationId,
+ ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContent")).vespaVersion(wantedVespaVersion).build(),
+ 6, 1, resources);
+ fail("Expected to fail due to out of capacity");
+ } catch (OutOfCapacityException ignored) { }
+
+ // Same cluster, but content type is now 'content'
+ List<HostSpec> nodes = tester.prepare(applicationId,
+ ClusterSpec.request(ClusterSpec.Type.content, ClusterSpec.Id.from("myContent")).vespaVersion(wantedVespaVersion).build(),
+ 6, 1, resources);
+ tester.activate(applicationId, nodes);
+
+ assertEquals(6, tester.nodeRepository().nodes().list(Node.State.active).owner(applicationId).size());
+ }
+
/** Exclusive app first, then non-exclusive: Should give the same result as below */
@Test
public void application_deployment_with_exclusive_app_first() {
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 dd16d4674ad..6c052a6c364 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
@@ -993,11 +993,15 @@ public class NodesV2ApiTest {
String url = "http://localhost:8080/nodes/v2/node/dockerhost1.yahoo.com";
tester.assertPartialResponse(new Request(url), "exclusiveTo", false); // Initially there is no exclusiveTo
- assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": \"t1:a1:i1\"}"), Request.Method.PATCH),
+ assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveToApplicationId\": \"t1:a1:i1\"}"), Request.Method.PATCH),
"{\"message\":\"Updated dockerhost1.yahoo.com\"}");
tester.assertPartialResponse(new Request(url), "exclusiveTo\":\"t1:a1:i1\",", true);
- assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": null}"), Request.Method.PATCH),
+ assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveToClusterType\": \"admin\"}"), Request.Method.PATCH),
+ "{\"message\":\"Updated dockerhost1.yahoo.com\"}");
+ tester.assertPartialResponse(new Request(url), "exclusiveTo\":\"t1:a1:i1\",\"exclusiveToClusterType\":\"admin\",", true);
+
+ assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": null, \"exclusiveToClusterType\": null}"), Request.Method.PATCH),
"{\"message\":\"Updated dockerhost1.yahoo.com\"}");
tester.assertPartialResponse(new Request(url), "exclusiveTo", false);
}