Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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 warning

Code scanning / Jenkins Security Scan

Stapler: Missing POST/RequirePOST annotation

Potential CSRF vulnerability: If DescriptorImpl#doCheckMinIdleAgents connects to user-specified URLs, modifies state, or is expensive to run, it should be annotated with @POST or @RequirePOST

Check warning

Code 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 warning

Code 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 warning

Code 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 warning

Code 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 warning

Code 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
Expand Up @@ -77,6 +77,7 @@ public class ECSCloud extends Cloud {

private static final Logger LOGGER = Logger.getLogger(ECSCloud.class.getName());

private List<ECSAgentPool> agentPools;
private List<ECSTaskTemplate> templates;
private final String credentialsId;
private final String cluster;
Expand Down Expand Up @@ -126,13 +127,18 @@ synchronized ECSService getEcsService() {
return ecsService;
}

@Nonnull
public List<ECSAgentPool> getAgentPools() {
return agentPools != null ? agentPools : Collections.<ECSAgentPool> emptyList();
}

@Nonnull
public List<ECSTaskTemplate> getTemplates() {
return templates != null ? templates : Collections.<ECSTaskTemplate> emptyList();
}

@Nonnull
private List<ECSTaskTemplate> getAllTemplates() {
public List<ECSTaskTemplate> getAllTemplates() {
List<ECSTaskTemplate> dynamicTemplates = TaskTemplateMap.get().getTemplates(this);
List<ECSTaskTemplate> allTemplates = new CopyOnWriteArrayList<>();

Expand All @@ -143,6 +149,11 @@ private List<ECSTaskTemplate> getAllTemplates() {
return allTemplates;
}

@DataBoundSetter
public void setAgentPools(List<ECSAgentPool> agentPools) {
this.agentPools = agentPools;
}

@DataBoundSetter
public void setTemplates(List<ECSTaskTemplate> templates) {
this.templates = templates;
Expand Down
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;
}
}
Loading