-
-
Notifications
You must be signed in to change notification settings - Fork 224
Add support for ECS agent pools to avoid cold start #400
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| package com.cloudbees.jenkins.plugins.amazonecs; | ||
|
|
||
| import antlr.ANTLRException; | ||
| import hudson.Extension; | ||
| import hudson.model.AbstractDescribableImpl; | ||
| import hudson.model.Descriptor; | ||
| import hudson.model.Label; | ||
| import hudson.scheduler.CronTabList; | ||
| import hudson.util.FormValidation; | ||
| import org.apache.commons.lang.StringUtils; | ||
| import org.kohsuke.stapler.DataBoundConstructor; | ||
| import org.kohsuke.stapler.DataBoundSetter; | ||
| import org.kohsuke.stapler.QueryParameter; | ||
|
|
||
| import javax.annotation.CheckForNull; | ||
| import java.io.Serializable; | ||
| import java.util.Calendar; | ||
| import java.util.Date; | ||
| import java.util.UUID; | ||
|
|
||
| public class ECSAgentPool extends AbstractDescribableImpl<ECSAgentPool> implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 7831619059473567435L; | ||
|
|
||
| /** | ||
| * Unique identifier for this pool. | ||
| */ | ||
| private String id; | ||
|
|
||
|
|
||
| public ECSAgentPool(@CheckForNull String label, String id, int minIdleAgents, @CheckForNull String maintainSchedule, int maxIdleMinutes, @CheckForNull String description) { | ||
| this.label = label; | ||
| this.id = StringUtils.isBlank(id) ? UUID.randomUUID().toString() : id; | ||
| this.minIdleAgents = minIdleAgents; | ||
| this.maintainSchedule = maintainSchedule; | ||
| this.maxIdleMinutes = maxIdleMinutes; | ||
| this.description = description; | ||
| } | ||
|
|
||
| /** | ||
| * White-space separated list of {@link hudson.model.Node} labels. | ||
| * | ||
| * @see Label | ||
| */ | ||
| @CheckForNull | ||
| private final String label; | ||
|
|
||
| /** | ||
| * Minimum number of idle agents to keep running for this template. | ||
| */ | ||
| private int minIdleAgents; | ||
|
|
||
| /** | ||
| * Cron schedule defining when {@link #minIdleAgents} should be maintained. | ||
| */ | ||
| @CheckForNull | ||
| private String maintainSchedule; | ||
|
|
||
| private int maxIdleMinutes; | ||
|
|
||
| @CheckForNull | ||
| private String description; | ||
|
|
||
| @DataBoundSetter | ||
| public void setMinIdleAgents(int minIdleAgents) { | ||
| this.minIdleAgents = Math.max(0, minIdleAgents); | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setMaintainSchedule(String maintainSchedule) { | ||
| this.maintainSchedule = StringUtils.trimToNull(maintainSchedule); | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setMaxIdleMinutes(int maxIdleMinutes) { | ||
| this.maxIdleMinutes = maxIdleMinutes; | ||
| } | ||
|
|
||
| public String getId() { | ||
| return id; | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setId(String id) { | ||
| this.id = id; | ||
| } | ||
|
|
||
| @CheckForNull | ||
| public String getLabel() { | ||
| return label; | ||
| } | ||
|
|
||
| public int getMinIdleAgents() { | ||
| return minIdleAgents; | ||
| } | ||
|
|
||
| public String getMaintainSchedule() { | ||
| return maintainSchedule; | ||
| } | ||
|
|
||
| public boolean isScheduleActive() { | ||
| if (StringUtils.isEmpty(maintainSchedule)) { | ||
| return true; | ||
| } | ||
| try { | ||
| CronTabList tabs = CronTabList.create(maintainSchedule); | ||
| Calendar calendar = Calendar.getInstance(); | ||
| calendar.setTime(new Date()); | ||
| return tabs.check(calendar); | ||
| } catch (ANTLRException e) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| public int getMaxIdleMinutes() { | ||
| return maxIdleMinutes; | ||
| } | ||
|
|
||
| @CheckForNull | ||
| public String getDescription() { | ||
| return description; | ||
| } | ||
|
|
||
| public void setDescription(@CheckForNull String description) { | ||
| this.description = description; | ||
| } | ||
|
|
||
| private Object readResolve() { | ||
| if (id == null) { | ||
| id = UUID.randomUUID().toString(); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| @Extension | ||
| public static class DescriptorImpl extends Descriptor<ECSAgentPool> { | ||
| @Override | ||
| public String getDisplayName() { | ||
| return Messages.agentPool(); | ||
| } | ||
|
|
||
| public FormValidation doCheckLabel(@QueryParameter String value) { | ||
| if (StringUtils.isBlank(value)) { | ||
| return FormValidation.error("Label is required"); | ||
| } | ||
| return FormValidation.ok(); | ||
| } | ||
|
|
||
| public FormValidation doCheckMinIdleAgents(@QueryParameter String value) { | ||
Check warningCode scanning / Jenkins Security Scan Stapler: Missing permission check
Potential missing permission check in DescriptorImpl#doCheckMinIdleAgents
|
||
| if (StringUtils.isBlank(value)) { | ||
| return FormValidation.ok(); | ||
| } | ||
| try { | ||
| int v = Integer.parseInt(value); | ||
| if (v >= 0) { | ||
| return FormValidation.ok(); | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| // fall through to error | ||
| } | ||
| return FormValidation.error("Must be a non-negative integer"); | ||
| } | ||
|
|
||
| public FormValidation doCheckMaxIdleMinutes(@QueryParameter String value) { | ||
Check warningCode scanning / Jenkins Security Scan Stapler: Missing POST/RequirePOST annotation
Potential CSRF vulnerability: If DescriptorImpl#doCheckMaxIdleMinutes connects to user-specified URLs, modifies state, or is expensive to run, it should be annotated with @POST or @RequirePOST
Check warningCode scanning / Jenkins Security Scan Stapler: Missing permission check
Potential missing permission check in DescriptorImpl#doCheckMaxIdleMinutes
|
||
| if (StringUtils.isBlank(value)) { | ||
| return FormValidation.ok(); | ||
| } | ||
| try { | ||
| int v = Integer.parseInt(value); | ||
| if (v >= 0) { | ||
| return FormValidation.ok(); | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| // fall through to error | ||
| } | ||
| return FormValidation.error("Must be a non-negative integer"); | ||
| } | ||
|
|
||
| public FormValidation doCheckMaintainSchedule(@QueryParameter String value) { | ||
Check warningCode scanning / Jenkins Security Scan Stapler: Missing POST/RequirePOST annotation
Potential CSRF vulnerability: If DescriptorImpl#doCheckMaintainSchedule connects to user-specified URLs, modifies state, or is expensive to run, it should be annotated with @POST or @RequirePOST
Check warningCode scanning / Jenkins Security Scan Stapler: Missing permission check
Potential missing permission check in DescriptorImpl#doCheckMaintainSchedule
|
||
| if (StringUtils.isBlank(value)) { | ||
| return FormValidation.ok(); | ||
| } | ||
| try { | ||
| CronTabList.create(value); | ||
| return FormValidation.ok(); | ||
| } catch (ANTLRException e) { | ||
| return FormValidation.error(e, e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| public FormValidation doCheckDescription(@QueryParameter String value) { | ||
| if (StringUtils.isBlank(value)) { | ||
| return FormValidation.error("Description is required"); | ||
| } | ||
| return FormValidation.ok(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package com.cloudbees.jenkins.plugins.amazonecs; | ||
|
|
||
| import hudson.Extension; | ||
| import hudson.model.AsyncPeriodicWork; | ||
| import hudson.model.Computer; | ||
| import hudson.model.TaskListener; | ||
| import hudson.slaves.Cloud; | ||
| import jenkins.model.Jenkins; | ||
| import org.apache.commons.lang.RandomStringUtils; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Arrays; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
| import javax.annotation.Nonnull; | ||
|
|
||
| @Extension | ||
| public class ECSAgentPoolMaintainer extends AsyncPeriodicWork { | ||
|
|
||
| private static final Logger LOGGER = Logger.getLogger(ECSAgentPoolMaintainer.class.getName()); | ||
|
|
||
| public ECSAgentPoolMaintainer() { | ||
| super("ECS Agent Pool Maintainer"); | ||
| } | ||
|
|
||
| @Override | ||
| public long getRecurrencePeriod() { | ||
| return TimeUnit.MINUTES.toMillis(1); | ||
| } | ||
|
|
||
| @Override | ||
| protected void execute(TaskListener listener) throws IOException, InterruptedException { | ||
| for (Cloud c : Jenkins.get().clouds) { | ||
| if (c instanceof ECSCloud) { | ||
| ECSCloud cloud = (ECSCloud) c; | ||
| for (ECSAgentPool pool : cloud.getAgentPools()) { | ||
|
|
||
| LOGGER.log(Level.INFO, "Pool: {0}, Required: {1}, Labels: [{2}]", new Object[]{pool.getId(), pool.getMinIdleAgents(), pool.getLabel()}); | ||
|
|
||
| if (pool.getMinIdleAgents() <= 0 || !pool.isScheduleActive()) { | ||
| continue; | ||
| } | ||
|
|
||
| final ECSTaskTemplate template = cloud.getTemplate(pool.getLabel()); | ||
|
|
||
| if (template != null) { | ||
|
|
||
| int current = countIdle(pool); | ||
| LOGGER.log(Level.INFO, "Pool: {0}, Required: {1}, Current: {2}, Labels: [{3}] ", new Object[]{pool.getId(), pool.getMinIdleAgents(), current, pool.getLabel()}); | ||
|
|
||
| while (current < pool.getMinIdleAgents()) { | ||
| try { | ||
| String agentName = cloud.getDisplayName() + "-" + pool.getLabel() + "-" + RandomStringUtils.random(5, "bcdfghjklmnpqrstvwxz0123456789"); | ||
| ECSPoolSlave ecsPoolSlave = new ECSPoolSlave(cloud, pool, agentName, template, new ECSLauncher(cloud, cloud.getTunnel(), null)); | ||
| Jenkins.get().addNode(ecsPoolSlave); | ||
| Computer computer = ecsPoolSlave.toComputer(); | ||
| if (computer != null) { | ||
| computer.connect(false); | ||
| } | ||
| LOGGER.log(Level.INFO, "Launch new agent.. Pool: {0}, Name: {1}", new Object[]{pool.getId(), agentName}); | ||
|
|
||
| current++; | ||
| } catch (Exception ex) { | ||
| LOGGER.log(Level.WARNING, "Failed to pre-launch agent for template " + template.getTemplateName(), ex); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private int countIdle(@Nonnull ECSAgentPool ecsAgentPool) { | ||
| Set<String> poolLabels = new HashSet<>(Arrays.asList(ecsAgentPool.getLabel().split("\\s+"))); | ||
| int count = 0; | ||
|
|
||
| for (Computer computer : Jenkins.get().getComputers()) { | ||
| if (!(computer instanceof ECSComputer)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (computer.getNode() == null) { | ||
| continue; | ||
| } | ||
|
|
||
| if (!(computer.getNode() instanceof ECSPoolSlave node)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (!ecsAgentPool.getId().equals(node.getId())) { | ||
| continue; | ||
| } | ||
|
|
||
| Set<String> nodeLabels = new HashSet<>(Arrays.asList(node.getLabelString().split("\\s+"))); | ||
| if (nodeLabels.containsAll(poolLabels) && computer.isIdle()) { | ||
| count++; | ||
| } | ||
| } | ||
|
|
||
| return count; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.cloudbees.jenkins.plugins.amazonecs; | ||
|
|
||
| import hudson.model.Descriptor; | ||
| import hudson.slaves.CloudRetentionStrategy; | ||
| import hudson.slaves.ComputerLauncher; | ||
| import org.jenkinsci.plugins.durabletask.executors.OnceRetentionStrategy; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
| import java.io.IOException; | ||
|
|
||
| public class ECSPoolSlave extends ECSSlave { | ||
|
|
||
| private final String id; | ||
|
|
||
| public ECSPoolSlave(@Nonnull ECSCloud cloud, @Nonnull ECSAgentPool ecsAgentPool, @Nonnull String name, ECSTaskTemplate template, | ||
| @Nonnull ComputerLauncher launcher) throws Descriptor.FormException, IOException { | ||
| super(cloud, name, template, launcher, cloud.getRetainAgents() ? | ||
| new CloudRetentionStrategy(cloud.getRetentionTimeout()) : | ||
| new OnceRetentionStrategy(ecsAgentPool.getMaxIdleMinutes())); | ||
| this.setNumExecutors(cloud.getNumExecutors()); | ||
| this.id = ecsAgentPool.getId(); | ||
|
|
||
| } | ||
|
|
||
| public String getId() { | ||
| return id; | ||
| } | ||
| } |
Check warning
Code scanning / Jenkins Security Scan
Stapler: Missing POST/RequirePOST annotation