summaryrefslogtreecommitdiffstats
path: root/node-repository
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@gmail.com>2022-10-28 12:04:29 +0200
committerGitHub <noreply@github.com>2022-10-28 12:04:29 +0200
commit6bc76deb99ccf0594e576aa3f9a6fd559bba182f (patch)
tree3ab7c3b192c9328bf507bd9356336ea38d5f08e6 /node-repository
parent6710433df30b1872654ad3ffeddbfe0ae4f417f6 (diff)
parent56907fe7dab5bab3ec09d96c5b23e4ba1c4ba0b6 (diff)
Merge pull request #24632 from vespa-engine/mpolden/account-change
Prevent reusing resources across accounts
Diffstat (limited to 'node-repository')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancer.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java24
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java33
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java26
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java36
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java21
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java68
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java40
8 files changed, 166 insertions, 87 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancer.java
index 4befa14942a..ad97ee887c9 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancer.java
@@ -68,7 +68,7 @@ public class LoadBalancer {
public enum State {
- /** This load balancer has been provisioned and reserved for an application */
+ /** The load balancer has been provisioned and reserved for an application */
reserved,
/**
@@ -81,6 +81,9 @@ public class LoadBalancer {
/** The load balancer is in active use by an application */
active,
+ /** The load balancer should be removed immediately by {@link LoadBalancerExpirer} */
+ removable,
+
}
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java
index 5314d36a3c2..f0abe184614 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java
@@ -48,7 +48,7 @@ public class LoadBalancerExpirer extends NodeRepositoryMaintainer {
private final LoadBalancerService service;
private final CuratorDatabaseClient db;
- LoadBalancerExpirer(NodeRepository nodeRepository, Duration interval, LoadBalancerService service, Metric metric) {
+ public LoadBalancerExpirer(NodeRepository nodeRepository, Duration interval, LoadBalancerService service, Metric metric) {
super(nodeRepository, interval, metric);
this.service = Objects.requireNonNull(service, "service must be non-null");
this.db = nodeRepository.database();
@@ -57,26 +57,24 @@ public class LoadBalancerExpirer extends NodeRepositoryMaintainer {
@Override
protected double maintain() {
expireReserved();
- return ( removeInactive() + pruneReals() ) / 2;
+ return (deprovisionRemovable() + pruneReals()) / 2;
}
/** Move reserved load balancer that have expired to inactive */
private void expireReserved() {
Instant now = nodeRepository().clock().instant();
Instant expiry = now.minus(reservedExpiry);
- patchLoadBalancers(lb -> lb.state() == State.reserved && lb.changedAt().isBefore(expiry),
+ patchLoadBalancers(lb -> canDeactivate(lb, expiry),
lb -> db.writeLoadBalancer(lb.with(State.inactive, now), lb.state()));
}
- /** Deprovision inactive load balancers that have expired */
- private double removeInactive() {
+ /** Deprovision removable load balancers */
+ private double deprovisionRemovable() {
MutableInteger attempts = new MutableInteger(0);
var failed = new ArrayList<LoadBalancerId>();
var lastException = new AtomicReference<Exception>();
var expiry = nodeRepository().clock().instant().minus(inactiveExpiry);
- patchLoadBalancers(lb -> lb.state() == State.inactive &&
- lb.changedAt().isBefore(expiry) &&
- allocatedNodes(lb.id()).isEmpty(), lb -> {
+ patchLoadBalancers(lb -> canRemove(lb, expiry), lb -> {
try {
attempts.add(1);
log.log(Level.INFO, () -> "Removing expired inactive " + lb.id());
@@ -145,6 +143,16 @@ public class LoadBalancerExpirer extends NodeRepositoryMaintainer {
}
}
+ private boolean canRemove(LoadBalancer lb, Instant expiry) {
+ return lb.state() == State.removable || (lb.state() == State.inactive &&
+ lb.changedAt().isBefore(expiry) &&
+ allocatedNodes(lb.id()).isEmpty());
+ }
+
+ private boolean canDeactivate(LoadBalancer lb, Instant expiry) {
+ return lb.state() == State.reserved && lb.changedAt().isBefore(expiry);
+ }
+
private List<Node> allocatedNodes(LoadBalancerId loadBalancer) {
return nodeRepository().nodes()
.list(Node.State.active, Node.State.inactive, Node.State.reserved)
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 a5ebc6b3efc..e5f02b38d29 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
@@ -87,7 +87,7 @@ public class CuratorDatabaseClient {
}
public List<String> cluster() {
- return db.cluster().stream().map(HostName::value).collect(Collectors.toUnmodifiableList());
+ return db.cluster().stream().map(HostName::value).toList();
}
private void initZK() {
@@ -162,7 +162,7 @@ public class CuratorDatabaseClient {
}
/**
- * Writes the given nodes to the given state (whether or not they are already in this state or another),
+ * Writes the given nodes to the given state (whether they are already in this state or another),
* and returns a copy of the incoming nodes in their persisted state.
*
* @param toState the state to write the nodes to
@@ -305,19 +305,18 @@ public class CuratorDatabaseClient {
}
private String toDir(Node.State state) {
- switch (state) {
- case active: return "allocated"; // legacy name
- case dirty: return "dirty";
- case failed: return "failed";
- case inactive: return "deallocated"; // legacy name
- case parked : return "parked";
- case provisioned: return "provisioned";
- case ready: return "ready";
- case reserved: return "reserved";
- case deprovisioned: return "deprovisioned";
- case breakfixed: return "breakfixed";
- default: throw new RuntimeException("Node state " + state + " does not map to a directory name");
- }
+ return switch (state) {
+ case active -> "allocated"; // legacy name
+ case dirty -> "dirty";
+ case failed -> "failed";
+ case inactive -> "deallocated"; // legacy name
+ case parked -> "parked";
+ case provisioned -> "provisioned";
+ case ready -> "ready";
+ case reserved -> "reserved";
+ case deprovisioned -> "deprovisioned";
+ case breakfixed -> "breakfixed";
+ };
}
/** Acquires the single cluster-global, reentrant lock for all non-active nodes */
@@ -513,7 +512,7 @@ public class CuratorDatabaseClient {
return db.getChildren(loadBalancersPath).stream()
.map(LoadBalancerId::fromSerializedForm)
.filter(predicate)
- .collect(Collectors.toUnmodifiableList());
+ .toList();
}
/** Returns a given number of unique provision indices */
@@ -524,7 +523,7 @@ public class CuratorDatabaseClient {
int firstIndex = (int) provisionIndexCounter.add(count) - count;
return IntStream.range(0, count)
.mapToObj(i -> firstIndex + i)
- .collect(Collectors.toList());
+ .toList();
}
public CacheStats cacheStats() {
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
index cfc918ce57b..4078a3e80d7 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
@@ -120,22 +120,22 @@ public class LoadBalancerSerializer {
}
private static String asString(LoadBalancer.State state) {
- switch (state) {
- case active: return "active";
- case inactive: return "inactive";
- case reserved: return "reserved";
- default: throw new IllegalArgumentException("No serialization defined for state enum '" + state + "'");
- }
+ return switch (state) {
+ case active -> "active";
+ case inactive -> "inactive";
+ case reserved -> "reserved";
+ case removable -> "removable";
+ };
}
private static LoadBalancer.State stateFromString(String state) {
- switch (state) {
- case "active": return LoadBalancer.State.active;
- case "inactive": return LoadBalancer.State.inactive;
- case "reserved": return LoadBalancer.State.reserved;
- case "removable": return LoadBalancer.State.inactive; // Read future value
- default: throw new IllegalArgumentException("No serialization defined for state string '" + state + "'");
- }
+ return switch (state) {
+ case "active" -> LoadBalancer.State.active;
+ case "inactive" -> LoadBalancer.State.inactive;
+ case "reserved" -> LoadBalancer.State.reserved;
+ case "removable" -> LoadBalancer.State.removable;
+ default -> throw new IllegalArgumentException("No serialization defined for state string '" + state + "'");
+ };
}
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
index 15c9134d10f..0763d19bae3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
@@ -48,6 +48,8 @@ import java.util.stream.Collectors;
// 1) (new) -> reserved -> active
// 2) active | reserved -> inactive
// 3) inactive -> active | (removed)
+// 4) active | reserved | inactive -> removable
+// 5) removable -> (removed)
public class LoadBalancerProvisioner {
private static final Logger log = Logger.getLogger(LoadBalancerProvisioner.class.getName());
@@ -161,7 +163,7 @@ public class LoadBalancerProvisioner {
return db.readLoadBalancerIds().stream()
.filter(id -> id.application().tenant().equals(tenant) &&
id.application().application().equals(application))
- .collect(Collectors.toUnmodifiableList());
+ .toList();
}
/** Require that load balancer IDs do not clash. This prevents name clashing when compacting endpoint DNS names */
@@ -190,9 +192,14 @@ public class LoadBalancerProvisioner {
newLoadBalancer = loadBalancer.get().with(instance);
fromState = newLoadBalancer.state();
}
- // Always store load balancer so that LoadBalancerExpirer can expire partially provisioned load balancers
+ if (!inAccount(cloudAccount, newLoadBalancer)) {
+ // We have a load balancer, but in the wrong account. Load balancer must be removed before we can
+ // provision a new one in the wanted account
+ newLoadBalancer = newLoadBalancer.with(LoadBalancer.State.removable, now);
+ }
+ // Always store the load balancer. LoadBalancerExpirer will remove unwanted ones
db.writeLoadBalancer(newLoadBalancer, fromState);
- requireInstance(id, instance);
+ requireInstance(id, instance, cloudAccount);
}
private void activate(ApplicationTransaction transaction, ClusterSpec.Id cluster, NodeList nodes) {
@@ -206,7 +213,7 @@ public class LoadBalancerProvisioner {
LoadBalancer.State state = instance.isPresent() ? LoadBalancer.State.active : loadBalancer.get().state();
LoadBalancer newLoadBalancer = loadBalancer.get().with(instance).with(state, now);
db.writeLoadBalancers(List.of(newLoadBalancer), loadBalancer.get().state(), transaction.nested());
- requireInstance(id, instance);
+ requireInstance(id, instance, loadBalancer.get().instance().get().cloudAccount());
}
/** Provision or reconfigure a load balancer instance, if necessary */
@@ -276,6 +283,11 @@ public class LoadBalancerProvisioner {
return ids;
}
+ /** Returns whether load balancer is provisioned in given account */
+ private static boolean inAccount(CloudAccount cloudAccount, LoadBalancer loadBalancer) {
+ return loadBalancer.instance().isEmpty() || loadBalancer.instance().get().cloudAccount().equals(cloudAccount);
+ }
+
/** Returns whether load balancer has given reals */
private static boolean hasReals(Optional<LoadBalancer> loadBalancer, Set<Real> reals) {
if (loadBalancer.isEmpty()) return false;
@@ -293,21 +305,19 @@ public class LoadBalancerProvisioner {
Set<String> reachable = new LinkedHashSet<>(node.ipConfig().primary());
// Remove addresses unreachable by the load balancer service
switch (service.protocol()) {
- case ipv4:
- reachable.removeIf(IP::isV6);
- break;
- case ipv6:
- reachable.removeIf(IP::isV4);
- break;
+ case ipv4 -> reachable.removeIf(IP::isV6);
+ case ipv6 -> reachable.removeIf(IP::isV4);
}
return reachable;
}
- private static void requireInstance(LoadBalancerId id, Optional<LoadBalancerInstance> instance) {
+ private static void requireInstance(LoadBalancerId id, Optional<LoadBalancerInstance> instance, CloudAccount cloudAccount) {
if (instance.isEmpty()) {
// Signal that load balancer is not ready yet
- throw new LoadBalancerServiceException("Could not (re)configure " + id + ". The operation will be retried on next deployment",
- null);
+ throw new LoadBalancerServiceException("Could not (re)configure " + id + ". The operation will be retried on next deployment");
+ }
+ if (!instance.get().cloudAccount().equals(cloudAccount)) {
+ throw new LoadBalancerServiceException("Could not (re)configure " + id + " due to change in cloud account. The operation will be retried on next deployment");
}
}
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 67dbefcdfcd..fe634a77997 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
@@ -104,34 +104,34 @@ class NodeAllocation {
/**
* Offer some nodes to this. The nodes may have an allocation to a different application or cluster,
* an allocation to this cluster, or no current allocation (in which case one is assigned).
- *
+ *
* Note that if unallocated nodes are offered before allocated nodes, this will unnecessarily
* reject allocated nodes due to index duplicates.
*
* @param candidates the nodes which are potentially on offer. These may belong to a different application etc.
- * @return the subset of offeredNodes which was accepted, with the correct allocation assigned
*/
- List<Node> offer(List<NodeCandidate> candidates) {
- List<Node> accepted = new ArrayList<>();
+ void offer(List<NodeCandidate> candidates) {
for (NodeCandidate candidate : candidates) {
if (candidate.allocation().isPresent()) {
Allocation allocation = candidate.allocation().get();
ClusterMembership membership = allocation.membership();
if ( ! allocation.owner().equals(application)) continue; // wrong application
if ( ! membership.cluster().satisfies(cluster)) continue; // wrong cluster id/type
- if ((! candidate.isSurplus || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it
+ if ((! candidate.isSurplus || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group, and we can't or have no reason to change it
if ( candidate.state() == Node.State.active && allocation.removable()) continue; // don't accept; causes removal
if ( candidate.state() == Node.State.active && candidate.wantToFail()) continue; // don't accept; causes failing
if ( indexes.contains(membership.index())) continue; // duplicate index (just to be sure)
if ( requiredHostFlavor.isPresent() && ! candidate.parent.map(node -> node.flavor().name()).equals(requiredHostFlavor)) continue;
+ if ( candidate.parent.isPresent() && ! candidate.parent.get().cloudAccount().equals(requestedNodes.cloudAccount())) continue; // wrong account
boolean resizeable = requestedNodes.considerRetiring() && candidate.isResizable;
boolean acceptToRetire = acceptToRetire(candidate);
if ((! saturated() && hasCompatibleFlavor(candidate) && requestedNodes.acceptable(candidate)) || acceptToRetire) {
candidate = candidate.withNode();
- if (candidate.isValid())
- accepted.add(acceptNode(candidate, shouldRetire(candidate, candidates), resizeable));
+ if (candidate.isValid()) {
+ acceptNode(candidate, shouldRetire(candidate, candidates), resizeable);
+ }
}
}
else if (! saturated() && hasCompatibleFlavor(candidate)) {
@@ -154,12 +154,11 @@ class NodeAllocation {
ClusterMembership.from(cluster, nextIndex.get()),
requestedNodes.resources().orElse(candidate.resources()),
nodeRepository.clock().instant());
- if (candidate.isValid())
- accepted.add(acceptNode(candidate, Retirement.none, false));
+ if (candidate.isValid()) {
+ acceptNode(candidate, Retirement.none, false);
+ }
}
}
-
- return accepted;
}
/** Returns the cause of retirement for given candidate */
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 de4df1992ee..d28d0321e6c 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
@@ -44,6 +44,7 @@ import org.junit.Test;
import java.time.Duration;
import java.time.Instant;
+import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@@ -437,16 +438,15 @@ public class DynamicProvisioningMaintainerTest {
final ApplicationId hostApp;
final ApplicationId configSrvApp;
switch (hostType) {
- case confighost:
+ case confighost -> {
hostApp = new ConfigServerHostApplication().getApplicationId();
configSrvApp = new ConfigServerApplication().getApplicationId();
- break;
- case controllerhost:
+ }
+ case controllerhost -> {
hostApp = new ControllerHostApplication().getApplicationId();
configSrvApp = new ControllerApplication().getApplicationId();
- break;
- default:
- throw new IllegalArgumentException("Unexpected config server host like node type: " + hostType);
+ }
+ default -> throw new IllegalArgumentException("Unexpected config server host like node type: " + hostType);
}
Cloud cloud = Cloud.builder().dynamicProvisioning(true).build();
@@ -551,23 +551,32 @@ public class DynamicProvisioningMaintainerTest {
// 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);
+ CloudAccount cloudAccount0 = new CloudAccount("000000000000");
+ Capacity capacity0 = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount0));
+ List<HostSpec> prepared = provisioningTester.prepare(applicationId, spec, capacity0);
// 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().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().equals(cloudAccount)));
+ provisionHostsIn(cloudAccount0, 2, tester);
+ assertEquals(2, provisioningTester.activate(applicationId, prepared).size());
+ NodeList allNodes0 = tester.nodeRepository.nodes().list();
+
+ // Redeployment in different account provisions a new set of hosts
+ CloudAccount cloudAccount1 = new CloudAccount("100000000000");
+ Capacity capacity1 = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount1));
+ prepared = provisioningTester.prepare(applicationId, spec, capacity1);
+ provisionHostsIn(cloudAccount1, 2, tester);
assertEquals(2, provisioningTester.activate(applicationId, prepared).size());
+
+ // No nodes or hosts are reused
+ NodeList allNodes1 = tester.nodeRepository.nodes().list();
+ NodeList activeNodes0 = allNodes0.state(Node.State.active).owner(applicationId);
+ NodeList activeNodes1 = allNodes1.state(Node.State.active).owner(applicationId);
+ assertTrue("New set of nodes is activated",
+ Collections.disjoint(activeNodes0.asList(),
+ activeNodes1.asList()));
+ assertTrue("New set of parents are used",
+ Collections.disjoint(allNodes0.parentsOf(activeNodes0).asList(),
+ allNodes1.parentsOf(activeNodes1).asList()));
}
@Test
@@ -627,6 +636,25 @@ public class DynamicProvisioningMaintainerTest {
assertEquals(0, tester.nodeRepository.nodes().list().rebuilding(true).size());
}
+ private void provisionHostsIn(CloudAccount cloudAccount, int count, DynamicProvisioningTester tester) {
+ tester.maintainer.maintain();
+ List<String> provisionedHostnames = tester.hostProvisioner.provisionedHosts().stream()
+ .filter(host -> host.cloudAccount().equals(cloudAccount))
+ .map(ProvisionedHost::hostHostname)
+ .toList();
+ assertEquals(count, provisionedHostnames.size());
+ for (var hostname : provisionedHostnames) {
+ tester.provisioningTester.nodeRepository().nodes().setReady(hostname, Agent.operator, getClass().getSimpleName());
+ }
+ tester.provisioningTester.prepareAndActivateInfraApplication(DynamicProvisioningTester.tenantHostApp, NodeType.host);
+ NodeList activeHosts = tester.provisioningTester.nodeRepository().nodes()
+ .list(Node.State.active)
+ .nodeType(NodeType.host)
+ .matching(host -> provisionedHostnames.contains(host.hostname()));
+ assertTrue(activeHosts.stream().allMatch(host -> host.cloudAccount().equals(cloudAccount)));
+ assertEquals(count, activeHosts.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/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
index 7706895ebca..d3a8ca2dcc1 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
@@ -20,10 +20,13 @@ import com.yahoo.vespa.hosted.provision.NodeList;
import com.yahoo.vespa.hosted.provision.lb.LoadBalancer;
import com.yahoo.vespa.hosted.provision.lb.LoadBalancerList;
import com.yahoo.vespa.hosted.provision.lb.Real;
+import com.yahoo.vespa.hosted.provision.maintenance.LoadBalancerExpirer;
+import com.yahoo.vespa.hosted.provision.maintenance.TestMetric;
import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.vespa.hosted.provision.node.IP;
import org.junit.Test;
+import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
@@ -313,12 +316,41 @@ public class LoadBalancerProvisionerTest {
@Test
public void load_balancer_with_custom_cloud_account() {
ClusterResources resources = new ClusterResources(3, 1, nodeResources);
- CloudAccount cloudAccount = new CloudAccount("012345678912");
- Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount));
- tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"))));
+ CloudAccount cloudAccount0 = CloudAccount.empty;
+ {
+ Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount0));
+ tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"))));
+ }
LoadBalancerList loadBalancers = tester.nodeRepository().loadBalancers().list();
assertEquals(1, loadBalancers.size());
- assertEquals(cloudAccount, loadBalancers.first().get().instance().get().cloudAccount());
+ assertEquals(cloudAccount0, loadBalancers.first().get().instance().get().cloudAccount());
+
+ // Changing account fails if there is an existing LB in the previous account.
+ CloudAccount cloudAccount1 = new CloudAccount("111111111111");
+ Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount1));
+ try {
+ prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1")));
+ fail("Expected exception");
+ } catch (LoadBalancerServiceException e) {
+ assertTrue(e.getMessage().contains("due to change in cloud account"));
+ }
+
+ // Existing LB is removed
+ loadBalancers = tester.nodeRepository().loadBalancers().list();
+ assertEquals(1, loadBalancers.size());
+ assertSame(LoadBalancer.State.removable, loadBalancers.first().get().state());
+ LoadBalancerExpirer expirer = new LoadBalancerExpirer(tester.nodeRepository(),
+ Duration.ofDays(1),
+ tester.loadBalancerService(),
+ new TestMetric());
+ expirer.run();
+ assertEquals(0, tester.nodeRepository().loadBalancers().list().in(LoadBalancer.State.removable).size());
+
+ // Next deployment provisions a new LB
+ tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"))));
+ loadBalancers = tester.nodeRepository().loadBalancers().list();
+ assertEquals(1, loadBalancers.size());
+ assertEquals(cloudAccount1, loadBalancers.first().get().instance().get().cloudAccount());
}
private void assertReals(ApplicationId application, ClusterSpec.Id cluster, Node.State... states) {