From 4d4a90ccba37e05f7a67e1bca04a271046c2ed81 Mon Sep 17 00:00:00 2001 From: Aaron Echavarria Date: Tue, 10 Jun 2025 09:03:49 -0600 Subject: [PATCH 1/3] support ecs agents pools, to avoid cold start --- .../plugins/amazonecs/ECSAgentPool.java | 199 ++++++++++++++++++ .../amazonecs/ECSAgentPoolMaintainer.java | 106 ++++++++++ .../jenkins/plugins/amazonecs/ECSCloud.java | 13 +- .../plugins/amazonecs/ECSPoolSlave.java | 28 +++ .../jenkins/plugins/amazonecs/ECSSlave.java | 37 ++-- .../plugins/amazonecs/ECSTaskTemplate.java | 40 +--- .../amazonecs/ECSAgentPool/config.jelly | 28 +++ .../ECSAgentPool/help-maintainSchedule.html | 5 + .../ECSAgentPool/help-maxIdleMinutes.html | 3 + .../ECSAgentPool/help-minIdleAgents.html | 5 + .../plugins/amazonecs/ECSCloud/config.jelly | 10 + .../plugins/amazonecs/Messages.properties | 3 +- 12 files changed, 426 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSPoolSlave.java create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maintainSchedule.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maxIdleMinutes.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-minIdleAgents.html diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java new file mode 100644 index 00000000..a79a8687 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java @@ -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 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 { + @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) { + 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) { + 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) { + 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(); + } + } +} + diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java new file mode 100644 index 00000000..c969ed42 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java @@ -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 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 nodeLabels = new HashSet<>(Arrays.asList(node.getLabelString().split("\\s+"))); + if (nodeLabels.containsAll(poolLabels) && computer.isIdle()) { + count++; + } + } + + return count; + } +} \ No newline at end of file diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud.java index d3c01f87..f1796c73 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud.java @@ -77,6 +77,7 @@ public class ECSCloud extends Cloud { private static final Logger LOGGER = Logger.getLogger(ECSCloud.class.getName()); + private List agentPools; private List templates; private final String credentialsId; private final String cluster; @@ -126,13 +127,18 @@ synchronized ECSService getEcsService() { return ecsService; } + @Nonnull + public List getAgentPools() { + return agentPools != null ? agentPools : Collections. emptyList(); + } + @Nonnull public List getTemplates() { return templates != null ? templates : Collections. emptyList(); } @Nonnull - private List getAllTemplates() { + public List getAllTemplates() { List dynamicTemplates = TaskTemplateMap.get().getTemplates(this); List allTemplates = new CopyOnWriteArrayList<>(); @@ -143,6 +149,11 @@ private List getAllTemplates() { return allTemplates; } + @DataBoundSetter + public void setAgentPools(List agentPools) { + this.agentPools = agentPools; + } + @DataBoundSetter public void setTemplates(List templates) { this.templates = templates; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSPoolSlave.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSPoolSlave.java new file mode 100644 index 00000000..4f2c1192 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSPoolSlave.java @@ -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; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlave.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlave.java index dd05663e..ec6b2cbd 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlave.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlave.java @@ -46,13 +46,10 @@ import com.amazonaws.services.ecs.model.ServerException; import com.amazonaws.services.ecs.model.Task; +import hudson.slaves.*; import org.jenkinsci.plugins.durabletask.executors.OnceRetentionStrategy; import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.slaves.AbstractCloudComputer; -import hudson.slaves.AbstractCloudSlave; -import hudson.slaves.CloudRetentionStrategy; -import hudson.slaves.ComputerLauncher; /** * This agent should only handle a single task and then be shutdown. @@ -93,24 +90,32 @@ public class ECSSlave extends AbstractCloudSlave { private boolean survivable = true; - public ECSSlave(@Nonnull ECSCloud cloud, @Nonnull String name, ECSTaskTemplate template, @Nonnull ComputerLauncher launcher) throws Descriptor.FormException, IOException { + + protected ECSSlave(@Nonnull ECSCloud cloud, @Nonnull String name, ECSTaskTemplate template, + @Nonnull ComputerLauncher launcher, RetentionStrategy retention) throws Descriptor.FormException, IOException { super( - name, - "ECS Agent", - template.makeRemoteFSRoot(name), - Math.max(1, cloud.getNumExecutors()), - Mode.EXCLUSIVE, - template.getLabel(), - launcher, - cloud.getRetainAgents() ? - new CloudRetentionStrategy(cloud.getRetentionTimeout()) : - new OnceRetentionStrategy(cloud.getRetentionTimeout()), - Collections.emptyList() + name, + "ECS Agent", + template.makeRemoteFSRoot(name), + Math.max(1, cloud.getNumExecutors()), + Mode.EXCLUSIVE, + template.getLabel(), + launcher, + retention, + Collections.emptyList() ); this.cloud = cloud; this.template = template; } + public ECSSlave(@Nonnull ECSCloud cloud, @Nonnull String name, ECSTaskTemplate template, + @Nonnull ComputerLauncher launcher) throws Descriptor.FormException, IOException { + this(cloud, name, template, launcher, + cloud.getRetainAgents() ? + new CloudRetentionStrategy(cloud.getRetentionTimeout()) : + new OnceRetentionStrategy(cloud.getRetentionTimeout())); + } + public String getClusterArn() { return clusterArn; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java index ff91567a..bb6fafde 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java @@ -26,35 +26,7 @@ package com.cloudbees.jenkins.plugins.amazonecs; import com.amazonaws.services.ecs.AmazonECS; -import com.amazonaws.services.ecs.model.AwsVpcConfiguration; -import com.amazonaws.services.ecs.model.ContainerDefinition; -import com.amazonaws.services.ecs.model.EFSAuthorizationConfig; -import com.amazonaws.services.ecs.model.EFSAuthorizationConfigIAM; -import com.amazonaws.services.ecs.model.EFSTransitEncryption; -import com.amazonaws.services.ecs.model.EFSVolumeConfiguration; -import com.amazonaws.services.ecs.model.HostEntry; -import com.amazonaws.services.ecs.model.HostVolumeProperties; -import com.amazonaws.services.ecs.model.KeyValuePair; -import com.amazonaws.services.ecs.model.LaunchType; -import com.amazonaws.services.ecs.model.OSFamily; -import com.amazonaws.services.ecs.model.CPUArchitecture; -import com.amazonaws.services.ecs.model.CapacityProviderStrategyItem; -import com.amazonaws.services.ecs.model.LinuxParameters; -import com.amazonaws.services.ecs.model.MountPoint; -import com.amazonaws.services.ecs.model.NetworkMode; -import com.amazonaws.services.ecs.model.PlacementStrategy; -import com.amazonaws.services.ecs.model.PlacementStrategyType; -import com.amazonaws.services.ecs.model.PortMapping; -import com.amazonaws.services.ecs.model.RegisterTaskDefinitionRequest; -import com.amazonaws.services.ecs.model.RepositoryCredentials; -import com.amazonaws.services.ecs.model.Volume; -import com.amazonaws.services.ecs.model.DescribeClustersRequest; -import com.amazonaws.services.ecs.model.DescribeClustersResult; -import com.amazonaws.services.ecs.model.Cluster; -import com.amazonaws.services.ecs.model.Ulimit; -import com.amazonaws.services.ecs.model.UlimitName; - -import static com.google.common.base.Strings.isNullOrEmpty; +import com.amazonaws.services.ecs.model.*; import com.amazonaws.services.elasticfilesystem.model.AccessPointDescription; import com.amazonaws.services.elasticfilesystem.model.FileSystemDescription; import com.cloudbees.jenkins.plugins.amazonecs.aws.EFSService; @@ -66,7 +38,7 @@ import hudson.model.labels.LabelAtom; import hudson.util.FormValidation; import hudson.util.ListBoxModel; -import static org.apache.commons.collections.CollectionUtils.isEmpty; +import jakarta.servlet.ServletException; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; @@ -76,9 +48,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import jakarta.servlet.ServletException; import java.io.IOException; -import java.io.PrintStream; import java.io.Serializable; import java.lang.reflect.Modifier; import java.util.*; @@ -86,6 +56,9 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.apache.commons.collections.CollectionUtils.isEmpty; + /** * @author Nicolas De Loof */ @@ -353,6 +326,7 @@ public int getMemoryConstraint() { */ private boolean enableExecuteCommand; + @DataBoundConstructor public ECSTaskTemplate(String templateName, @Nullable String label, @@ -519,6 +493,7 @@ public void setDnsSearchDomains(String dnsSearchDomains) { this.dnsSearchDomains = StringUtils.trimToNull(dnsSearchDomains); } + public boolean isFargate() { if (!this.defaultCapacityProvider && this.capacityProviderStrategies != null && ! this.capacityProviderStrategies.isEmpty()) { for (CapacityProviderStrategyEntry capacityProviderStrategy : this.capacityProviderStrategies) { @@ -927,7 +902,6 @@ public ECSTaskTemplate merge(ECSTaskTemplate parent) { enableExecuteCommand); merged.setLogDriver(logDriver); merged.setEntrypoint(entrypoint); - return merged; } diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/config.jelly new file mode 100644 index 00000000..1be934b7 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/config.jelly @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maintainSchedule.html b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maintainSchedule.html new file mode 100644 index 00000000..95bf9bcc --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maintainSchedule.html @@ -0,0 +1,5 @@ +

+ Cron syntax controlling when the plugin should maintain the minimum + number of idle agents. Leave empty to keep the pool at all times. + Uses the standard Jenkins cron format. +

\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maxIdleMinutes.html b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maxIdleMinutes.html new file mode 100644 index 00000000..5c3232a1 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-maxIdleMinutes.html @@ -0,0 +1,3 @@ +

+ Maximum number of minutes to keep an agent idle before it is terminated. +

\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-minIdleAgents.html b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-minIdleAgents.html new file mode 100644 index 00000000..665b5815 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool/help-minIdleAgents.html @@ -0,0 +1,5 @@ +

+ Number of agent tasks to keep pre-launched and ready for use. + When the number of idle agents falls below this value, the plugin + will start additional agents to replenish the pool. +

\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud/config.jelly index 36031df1..23e75522 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSCloud/config.jelly @@ -95,5 +95,15 @@ + + + + +
+ +
+
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/Messages.properties b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/Messages.properties index fbfeedeb..5072674a 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/Messages.properties @@ -24,4 +24,5 @@ # displayName=Amazon EC2 Container Service Cloud -template=ECS Task template \ No newline at end of file +template=ECS Task templat +agentPool=ECS Agent Pool \ No newline at end of file From 211b2e1061207d3268d9cd8d4a8189b973230bf1 Mon Sep 17 00:00:00 2001 From: Aaron Echavarria Date: Tue, 10 Jun 2025 09:49:08 -0600 Subject: [PATCH 2/3] include changelog and fix spotbugs --- Changelog.md | 2 ++ .../jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index da809d5d..cbfbf8f1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ## vNext +- Feature - Support ECS agent pools to avoid cold start + ## 1.35 / 1.36 - allow certain classes to be used from other plugins (#192) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java index c969ed42..04a23b07 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPoolMaintainer.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -75,9 +76,10 @@ protected void execute(TaskListener listener) throws IOException, InterruptedExc } private int countIdle(@Nonnull ECSAgentPool ecsAgentPool) { - Set poolLabels = new HashSet<>(Arrays.asList(ecsAgentPool.getLabel().split("\\s+"))); int count = 0; + Set poolLabels = new HashSet<>(Arrays.asList(Objects.requireNonNull(ecsAgentPool.getLabel()).split("\\s+"))); + for (Computer computer : Jenkins.get().getComputers()) { if (!(computer instanceof ECSComputer)) { continue; From 751fc181e8f96a38c45b0c67db7a3b6c202b8cdb Mon Sep 17 00:00:00 2001 From: "aaron.echavarria@popular.com" Date: Tue, 10 Jun 2025 11:52:10 -0600 Subject: [PATCH 3/3] include description field --- .../com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java index a79a8687..f58aa8ea 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSAgentPool.java @@ -27,7 +27,7 @@ public class ECSAgentPool extends AbstractDescribableImpl implemen */ private String id; - + @DataBoundConstructor 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;