|
11 | 11 | import com.amazonaws.services.ec2.model.Reservation;
|
12 | 12 | import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
|
13 | 13 | import com.amazonaws.services.ec2.model.TerminateInstancesResult;
|
14 |
| -import hudson.model.Executor; |
| 14 | +import com.gargoylesoftware.htmlunit.html.HtmlForm; |
| 15 | +import com.gargoylesoftware.htmlunit.html.HtmlFormUtil; |
| 16 | +import com.gargoylesoftware.htmlunit.html.HtmlPage; |
| 17 | +import com.gargoylesoftware.htmlunit.html.HtmlTextInput; |
15 | 18 | import hudson.model.Node;
|
16 |
| -import hudson.model.Queue; |
17 | 19 | import hudson.model.queue.QueueTaskFuture;
|
18 |
| -import hudson.security.AccessControlled; |
19 |
| -import jenkins.model.Jenkins; |
20 | 20 | import org.junit.Before;
|
21 | 21 | import org.junit.Test;
|
22 | 22 | import org.mockito.ArgumentCaptor;
|
23 | 23 | import org.mockito.Mockito;
|
24 | 24 |
|
25 |
| -import java.io.IOException; |
26 | 25 | import java.util.ArrayList;
|
27 | 26 | import java.util.Arrays;
|
28 | 27 | import java.util.Collections;
|
29 | 28 | import java.util.HashSet;
|
30 | 29 | import java.util.List;
|
| 30 | +import java.util.concurrent.TimeUnit; |
| 31 | +import java.util.stream.Collectors; |
31 | 32 |
|
| 33 | +import static org.junit.Assert.assertNotNull; |
| 34 | +import static org.junit.Assert.assertNotSame; |
32 | 35 | import static org.junit.Assert.assertTrue;
|
33 | 36 | import static org.mockito.ArgumentMatchers.any;
|
34 | 37 | import static org.mockito.ArgumentMatchers.anyString;
|
@@ -72,6 +75,30 @@ public void before() {
|
72 | 75 | when(amazonEC2.terminateInstances(any(TerminateInstancesRequest.class))).thenReturn(new TerminateInstancesResult());
|
73 | 76 | }
|
74 | 77 |
|
| 78 | + @Test |
| 79 | + public void shouldTerminateNodeMarkedForDeletion() throws Exception { |
| 80 | + final EC2FleetCloud cloud = new EC2FleetCloud(null, null, "credId", null, "region", |
| 81 | + null, "fId", "momo", null, new LocalComputerConnector(j), false, false, |
| 82 | + 1, 0, 0, 0, 1, false, true, "-1", false, 0, 0, false, 999, false); |
| 83 | + // Set initial jenkins nodes |
| 84 | + cloud.update(); |
| 85 | + j.jenkins.clouds.add(cloud); |
| 86 | + |
| 87 | + assertAtLeastOneNode(); |
| 88 | + |
| 89 | + EC2FleetNode node = (EC2FleetNode) j.jenkins.getNode("i-1"); |
| 90 | + EC2FleetNodeComputer c = (EC2FleetNodeComputer) node.toComputer(); |
| 91 | + c.doDoDelete(); // mark node for termination |
| 92 | + node.getRetentionStrategy().check(c); |
| 93 | + |
| 94 | + // Make sure the scheduled for termination instances are terminated |
| 95 | + cloud.update(); |
| 96 | + |
| 97 | + final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class); |
| 98 | + verify(amazonEC2, times(1)).terminateInstances(argument.capture()); |
| 99 | + assertTrue(argument.getAllValues().get(0).getInstanceIds().containsAll(Arrays.asList("i-1"))); |
| 100 | + } |
| 101 | + |
75 | 102 | @Test
|
76 | 103 | public void shouldTerminateExcessCapacity() throws Exception {
|
77 | 104 | final EC2FleetCloud cloud = new EC2FleetCloud(null, null, "credId", null, "region",
|
@@ -222,4 +249,101 @@ public void shouldNotTerminateBelowMinSpareSize() throws Exception {
|
222 | 249 |
|
223 | 250 | verify((amazonEC2), times(0)).terminateInstances(any());
|
224 | 251 | }
|
| 252 | + |
| 253 | + @Test |
| 254 | + public void shouldTerminateWhenMaxTotalUsesIsExhausted() throws Exception { |
| 255 | + final String label = "momo"; |
| 256 | + final int numTasks = 4; // schedule a total of 4 tasks, 2 per instance |
| 257 | + final int maxTotalUses = 2; |
| 258 | + final int taskSleepTime = 1; |
| 259 | + |
| 260 | + EC2FleetCloud cloud = spy(new EC2FleetCloud("testCloud", null, "credId", null, "region", |
| 261 | + null, "fId", label, null, new LocalComputerConnector(j), false, false, |
| 262 | + 0, 0, 10, 0, 1, false, true, |
| 263 | + String.valueOf(maxTotalUses), true, 0, 0, false, 10, false)); |
| 264 | + j.jenkins.clouds.add(cloud); |
| 265 | + cloud.update(); |
| 266 | + assertAtLeastOneNode(); |
| 267 | + |
| 268 | + System.out.println("*** scheduling tasks ***"); |
| 269 | + waitJobSuccessfulExecution(enqueTask(numTasks, taskSleepTime)); |
| 270 | + Thread.sleep(3000); // sleep for a bit to make sure post job actions finish and the computers are idle |
| 271 | + |
| 272 | + // make sure the instances scheduled for termination are terminated |
| 273 | + cloud.update(); |
| 274 | + |
| 275 | + final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class); |
| 276 | + verify((amazonEC2)).terminateInstances(argument.capture()); |
| 277 | + assertTrue(argument.getAllValues().get(0).getInstanceIds().containsAll(Arrays.asList("i-1", "i-2"))); |
| 278 | + } |
| 279 | + |
| 280 | + @Test |
| 281 | + public void shouldTerminateNodeForMaxTotalUsesIsExhaustedAfterConfigChange() throws Exception { |
| 282 | + final String label = "momo"; |
| 283 | + final int numTasks = 4; // schedule a total of 4 tasks, 2 per instance |
| 284 | + final int maxTotalUses = 2; |
| 285 | + final long scheduleInterval = 5; |
| 286 | + final int cloudStatusInternalSec = 60; // increase to trigger update manually |
| 287 | + final int taskSleepTime = 1; |
| 288 | + |
| 289 | + EC2FleetCloud cloud = new EC2FleetCloud("testCloud", null, "credId", null, "region", |
| 290 | + null, "fId", label, null, new LocalComputerConnector(j), false, false, |
| 291 | + 0, 0, 10, 0, 1, false, true, |
| 292 | + String.valueOf(maxTotalUses), true, 0, 0, false, |
| 293 | + cloudStatusInternalSec, false); |
| 294 | + j.jenkins.clouds.add(cloud); |
| 295 | + cloud.update(); |
| 296 | + assertAtLeastOneNode(); |
| 297 | + |
| 298 | + // initiate a config change after a node exhausts maxTotalUses and is scheduled for termination |
| 299 | + EC2FleetCloud newCloud; |
| 300 | + String nodeToTerminate = null; |
| 301 | + int taskCount = 0; |
| 302 | + final List<QueueTaskFuture> tasks = new ArrayList<>(); |
| 303 | + for (int i=numTasks; i > 0 ; i--) { |
| 304 | + // get first node that is about to get terminated, before scheduling more tasks |
| 305 | + List<String> nodesWithExhaustedMaxUses = j.jenkins.getNodes().stream() |
| 306 | + .filter(n -> ((EC2FleetNode)n).getUsesRemaining() == 0) |
| 307 | + .map(Node::getNodeName) |
| 308 | + .collect(Collectors.toList()); |
| 309 | + if (nodesWithExhaustedMaxUses != null && !nodesWithExhaustedMaxUses.isEmpty()) { |
| 310 | + nodeToTerminate = nodesWithExhaustedMaxUses.get(0); |
| 311 | + break; // we have what we want, stop scheduling more tasks - exit loop and verify |
| 312 | + } |
| 313 | + |
| 314 | + // schedule a task |
| 315 | + tasks.addAll(enqueTask(1, taskSleepTime)); |
| 316 | + taskCount++; |
| 317 | + System.out.println("scheduled task " + taskCount + ", waiting " + scheduleInterval + " sec"); |
| 318 | + Thread.sleep(TimeUnit.SECONDS.toMillis(scheduleInterval)); |
| 319 | + } |
| 320 | + waitJobSuccessfulExecution(tasks); // wait for scheduleToTerminate to be called |
| 321 | + |
| 322 | + assertNotNull(nodeToTerminate); |
| 323 | + |
| 324 | + // make a config change after a node is scheduled to terminate |
| 325 | + HtmlPage page = j.createWebClient().goTo("configureClouds"); |
| 326 | + HtmlForm form = page.getFormByName("config"); |
| 327 | + System.out.println(form.toString()); |
| 328 | + System.out.println(IntegrationTest.getElementsByNameWithoutJdk(page, "_.name")); |
| 329 | + ((HtmlTextInput) IntegrationTest.getElementsByNameWithoutJdk(page, "_.name").get(0)).setText("new-name"); |
| 330 | + HtmlFormUtil.submit(form); |
| 331 | + |
| 332 | + // verify cloud object was re-created, leading to lost state (i.e. instanceIdsToTerminate) |
| 333 | + newCloud = (EC2FleetCloud) j.jenkins.clouds.get(0); |
| 334 | + assertNotSame(cloud, newCloud); |
| 335 | + assertTrue(cloud.getInstanceIdsToTerminate().containsKey(nodeToTerminate)); |
| 336 | + assertTrue(newCloud.getInstanceIdsToTerminate().isEmpty()); |
| 337 | + |
| 338 | + // initiate check to schedule instance to terminate again |
| 339 | + EC2FleetNode node = (EC2FleetNode) j.jenkins.getNode(nodeToTerminate); |
| 340 | + node.getRetentionStrategy().check(node.toComputer()); |
| 341 | + |
| 342 | + // terminate scheduled instances |
| 343 | + cloud.update(); |
| 344 | + |
| 345 | + final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class); |
| 346 | + verify((amazonEC2)).terminateInstances(argument.capture()); |
| 347 | + assertTrue(argument.getAllValues().get(0).getInstanceIds().contains(nodeToTerminate)); |
| 348 | + } |
225 | 349 | }
|
0 commit comments