Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-25106] Support minimumNoInstances #343

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ THE SOFTWARE.
<artifactId>aws-java-sdk-bom</artifactId>
<version>${aws-java-sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public ListBoxModel doFillRegionItems(
return model;
}

// Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParamter field), or use the default
// Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParameter field), or use the default
// value if not specified.
@VisibleForTesting
URL determineEC2EndpointURL(@Nullable String altEC2Endpoint) throws MalformedURLException {
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/hudson/plugins/ec2/EC2Cloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,47 @@ else if (jenkinsInstance.isTerminating()) {
}
}

public void provision(SlaveTemplate t, int number) {

Jenkins jenkinsInstance = Jenkins.get();
if (jenkinsInstance.isQuietingDown()) {
LOGGER.log(Level.FINE, "Not provisioning nodes, Jenkins instance is quieting down");
return;
} else if (jenkinsInstance.isTerminating()) {
LOGGER.log(Level.FINE, "Not provisioning nodes, Jenkins instance is terminating");
return;
}

try {
LOGGER.log(Level.INFO, "{0}. Attempting to provision {1} slave(s)", new Object[]{t, number});
final List<EC2AbstractSlave> slaves = getNewOrExistingAvailableSlave(t, number, false);

if (slaves == null || slaves.isEmpty()) {
LOGGER.warning("Can't raise nodes for " + t);
return;
}

for (final EC2AbstractSlave slave : slaves) {
if (slave == null) {
LOGGER.warning("Can't raise node for " + t);
continue;
}

Computer c = slave.toComputer();
if (slave.getStopOnTerminate() && c != null) {
c.connect(false);
}
jenkinsInstance.addNode(slave);
}

LOGGER.log(Level.INFO, "{0}. Attempting provision finished", t);
LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more",
new Object[]{Jenkins.get().getComputers().length, number});
} catch (AmazonClientException | IOException e) {
LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e);
}
}

private PlannedNode createPlannedNode(final SlaveTemplate t, final EC2AbstractSlave slave) {
return new PlannedNode(t.getDisplayName(),
Computer.threadPoolForRemoting.submit(new Callable<Node>() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* @author Kohsuke Kawaguchi
*/
public final class EC2OndemandSlave extends EC2AbstractSlave {
public class EC2OndemandSlave extends EC2AbstractSlave {
private static final Logger LOGGER = Logger.getLogger(EC2OndemandSlave.class.getName());

@Deprecated
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@
import hudson.model.Executor;
import hudson.model.ExecutorListener;
import hudson.model.Queue;
import hudson.plugins.ec2.util.MinimumInstanceChecker;
import hudson.slaves.RetentionStrategy;
import jenkins.model.Jenkins;

import java.io.IOException;
import java.lang.reflect.Field;
import java.time.Clock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
Expand Down Expand Up @@ -129,6 +128,16 @@ private long internalCheck(EC2Computer computer) {
return 1;
}

/*
* If we have equal or less number of slaves than the template's minimum instance count, don't perform check.
*/
SlaveTemplate slaveTemplate = computer.getSlaveTemplate();
if (slaveTemplate != null) {
long numberOfCurrentInstancesForTemplate = MinimumInstanceChecker.countCurrentNumberOfSlaves(slaveTemplate);
if (numberOfCurrentInstancesForTemplate > 0 && numberOfCurrentInstancesForTemplate <= slaveTemplate.getMinimumNumberOfInstances()) {
return 1;
}
}

if (computer.isIdle() && !DISABLED) {
final long uptime;
Expand All @@ -148,7 +157,7 @@ private long internalCheck(EC2Computer computer) {
// * Already Terminated
// * We use stop-on-terminate and the instance is currently stopped or stopping
if (InstanceState.TERMINATED.equals(state)
|| computer.getSlaveTemplate().stopOnTerminate && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) {
|| (slaveTemplate != null && slaveTemplate.stopOnTerminate) && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) {
if (computer.isOnline()) {
LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" + state.toString());
computer.disconnect(null);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import hudson.plugins.ec2.util.MinimumInstanceChecker;
import jenkins.model.Jenkins;

import com.amazonaws.AmazonClientException;
Expand All @@ -36,6 +37,11 @@ public long getRecurrencePeriod() {

@Override
protected void execute(TaskListener listener) throws IOException, InterruptedException {
removeDeadNodes();
MinimumInstanceChecker.checkForMinimumInstances();
}

private void removeDeadNodes() {
for (Node node : Jenkins.get().getNodes()) {
if (node instanceof EC2AbstractSlave) {
final EC2AbstractSlave ec2Slave = (EC2AbstractSlave) node;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hudson/plugins/ec2/EC2SpotSlave.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import javax.annotation.CheckForNull;

public final class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness {
public class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness {
private static final Logger LOGGER = Logger.getLogger(EC2SpotSlave.class.getName());

private final String spotInstanceRequestId;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/hudson/plugins/ec2/PluginImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package hudson.plugins.ec2;

import hudson.plugins.ec2.util.MinimumInstanceChecker;
import jenkins.model.Jenkins;
import hudson.Extension;
import hudson.Plugin;
Expand Down Expand Up @@ -60,4 +61,9 @@ public String getDisplayName() {
return "EC2 PluginImpl";
}
}

@Override
public void postInitialize() {
MinimumInstanceChecker.checkForMinimumInstances();
}
}
121 changes: 111 additions & 10 deletions src/main/java/hudson/plugins/ec2/SlaveTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@

import javax.servlet.ServletException;

import hudson.plugins.ec2.util.AmazonEC2Factory;
import hudson.plugins.ec2.util.*;

import hudson.XmlFile;
import hudson.model.listeners.SaveableListener;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
Expand All @@ -64,7 +67,6 @@
import hudson.model.*;
import hudson.model.Descriptor.FormException;
import hudson.model.labels.LabelAtom;
import hudson.plugins.ec2.util.DeviceMappingParser;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;

Expand Down Expand Up @@ -126,6 +128,8 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {

public int instanceCap;

public int minimumNumberOfInstances;

public final boolean stopOnTerminate;

private final List<EC2Tag> tags;
Expand Down Expand Up @@ -179,7 +183,7 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {
public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
boolean stopOnTerminate, String subnetId, List<EC2Tag> tags, String idleTerminationMinutes,
boolean stopOnTerminate, String subnetId, List<EC2Tag> tags, String idleTerminationMinutes, int minimumNumberOfInstances,
String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
Expand Down Expand Up @@ -225,6 +229,8 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
this.usePrivateDnsName = this.connectionStrategy.equals(ConnectionStrategy.PRIVATE_DNS);
this.connectUsingPublicIp = this.connectionStrategy.equals(ConnectionStrategy.PUBLIC_IP);

this.minimumNumberOfInstances = minimumNumberOfInstances;

if (null == instanceCapStr || instanceCapStr.isEmpty()) {
this.instanceCap = Integer.MAX_VALUE;
} else {
Expand All @@ -246,6 +252,22 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
readResolve(); // initialize
}

@Deprecated
public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
boolean stopOnTerminate, String subnetId, List<EC2Tag> tags, String idleTerminationMinutes,
String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses) {
this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
idleTerminationMinutes, 0, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices,
useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess,
monitoring, t2Unlimited, connectionStrategy, maxTotalUses);
}

erikhakansson marked this conversation as resolved.
Show resolved Hide resolved
@Deprecated
public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
Expand Down Expand Up @@ -482,6 +504,10 @@ public void setAmiType(AMITypeData amiType) {
this.amiType = amiType;
}

public int getMinimumNumberOfInstances() {
return minimumNumberOfInstances;
}

public int getInstanceCap() {
return instanceCap;
}
Expand Down Expand Up @@ -1107,16 +1133,57 @@ private HashSet<Tag> buildTags(String slaveType) {
}

protected EC2OndemandSlave newOndemandSlave(Instance inst) throws FormException, IOException {
return new EC2OndemandSlave(getSlaveName(inst.getInstanceId()), inst.getInstanceId(), description, remoteFS, getNumExecutors(), labels, mode, initScript,
tmpDir, Collections.emptyList(), remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, inst.getPublicDnsName(),
inst.getPrivateDnsName(), EC2Tag.fromAmazonTags(inst.getTags()), parent.name,
useDedicatedTenancy, getLaunchTimeout(), amiType, connectionStrategy, maxTotalUses);
EC2SlaveConfig.OnDemand config = new EC2SlaveConfig.OnDemandBuilder()
.withName(getSlaveName(inst.getInstanceId()))
.withInstanceId(inst.getInstanceId())
.withDescription(description)
.withRemoteFS(remoteFS)
.withNumExecutors(getNumExecutors())
.withLabelString(labels)
.withMode(mode)
.withInitScript(initScript)
.withTmpDir(tmpDir)
.withNodeProperties(Collections.emptyList())
.withRemoteAdmin(remoteAdmin)
.withJvmopts(jvmopts)
.withStopOnTerminate(stopOnTerminate)
.withIdleTerminationMinutes(idleTerminationMinutes)
.withPublicDNS(inst.getPublicDnsName())
.withPrivateDNS(inst.getPrivateDnsName())
.withTags(EC2Tag.fromAmazonTags(inst.getTags()))
.withCloudName(parent.name)
.withUseDedicatedTenancy(useDedicatedTenancy)
.withLaunchTimeout(getLaunchTimeout())
.withAmiType(amiType)
.withConnectionStrategy(connectionStrategy)
.withMaxTotalUses(maxTotalUses)
.build();
return EC2SlaveFactory.getInstance().createOnDemandSlave(config);
}

protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormException, IOException {
return new EC2SpotSlave(getSlaveName(sir.getSpotInstanceRequestId()), sir.getSpotInstanceRequestId(), description, remoteFS, getNumExecutors(), mode, initScript,
tmpDir, labels, Collections.emptyList(), remoteAdmin, jvmopts, idleTerminationMinutes, EC2Tag.fromAmazonTags(sir.getTags()), parent.name,
getLaunchTimeout(), amiType, connectionStrategy, maxTotalUses);
EC2SlaveConfig.Spot config = new EC2SlaveConfig.SpotBuilder()
.withName(getSlaveName(sir.getSpotInstanceRequestId()))
.withSpotInstanceRequestId(sir.getSpotInstanceRequestId())
.withDescription(description)
.withRemoteFS(remoteFS)
.withNumExecutors(getNumExecutors())
.withMode(mode)
.withInitScript(initScript)
.withTmpDir(tmpDir)
.withLabelString(labels)
.withNodeProperties(Collections.emptyList())
.withRemoteAdmin(remoteAdmin)
.withJvmopts(jvmopts)
.withIdleTerminationMinutes(idleTerminationMinutes)
.withTags(EC2Tag.fromAmazonTags(sir.getTags()))
.withCloudName(parent.name)
.withLaunchTimeout(getLaunchTimeout())
.withAmiType(amiType)
.withConnectionStrategy(connectionStrategy)
.withMaxTotalUses(maxTotalUses)
.build();
return EC2SlaveFactory.getInstance().createSpotSlave(config);
}

/**
Expand Down Expand Up @@ -1289,6 +1356,16 @@ public boolean isUseHTTPS() {
return amiType.isWindows() && ((WindowsData) amiType).isUseHTTPS();
}

@Extension
public static final class OnSaveListener extends SaveableListener {
@Override
public void onChange(Saveable o, XmlFile file) {
if (o instanceof Hudson) {
MinimumInstanceChecker.checkForMinimumInstances();
}
}
}

@Extension
public static final class DescriptorImpl extends Descriptor<SlaveTemplate> {

Expand Down Expand Up @@ -1425,6 +1502,30 @@ public FormValidation doCheckMaxTotalUses(@QueryParameter String value) {
return FormValidation.error("Maximum Total Uses must be greater or equal to -1");
}

public FormValidation doCheckMinimumNumberOfInstances(@QueryParameter String value, @QueryParameter String instanceCapStr) {
if (value == null || value.trim().isEmpty())
return FormValidation.ok();
try {
int val = Integer.parseInt(value);
if (val > 0) {
int instanceCap;
try {
instanceCap = Integer.parseInt(instanceCapStr);
} catch (NumberFormatException ignore) {
instanceCap = Integer.MAX_VALUE;
}
if (val > instanceCap) {
return FormValidation
.error("Minimum number of instances must not be larger than AMI Instance Cap %d",
instanceCap);
}
return FormValidation.ok();
}
} catch (NumberFormatException ignore) {
}
return FormValidation.error("Minimum number of instances must be a non-negative integer (or null)");
}

public FormValidation doCheckInstanceCapStr(@QueryParameter String value) {
if (value == null || value.trim().isEmpty())
return FormValidation.ok();
Expand Down
Loading