diff --git a/pom.xml b/pom.xml index d40418b0a..680a1dbe8 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,7 @@ maven-compiler-plugin 1.6 + 1.6 diff --git a/src/main/java/org/jenkinsci/plugins/mesos/JenkinsScheduler.java b/src/main/java/org/jenkinsci/plugins/mesos/JenkinsScheduler.java index ec2e41b1b..a806f08b4 100644 --- a/src/main/java/org/jenkinsci/plugins/mesos/JenkinsScheduler.java +++ b/src/main/java/org/jenkinsci/plugins/mesos/JenkinsScheduler.java @@ -28,19 +28,20 @@ import java.util.Queue; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import net.sf.json.JSONObject; -import com.google.protobuf.ByteString; - import org.apache.commons.lang.StringUtils; import org.apache.mesos.MesosSchedulerDriver; import org.apache.mesos.Protos.Attribute; import org.apache.mesos.Protos.CommandInfo; import org.apache.mesos.Protos.ContainerInfo; import org.apache.mesos.Protos.ContainerInfo.DockerInfo; +import org.apache.mesos.Protos.ContainerInfo.DockerInfo.Network; +import org.apache.mesos.Protos.ContainerInfo.DockerInfo.PortMapping; import org.apache.mesos.Protos.Credential; import org.apache.mesos.Protos.ExecutorID; import org.apache.mesos.Protos.Filters; @@ -57,12 +58,13 @@ import org.apache.mesos.Protos.TaskInfo; import org.apache.mesos.Protos.TaskStatus; import org.apache.mesos.Protos.Value; -import org.apache.mesos.Protos.Value.Type; +import org.apache.mesos.Protos.Value.Range; import org.apache.mesos.Protos.Volume; import org.apache.mesos.Protos.Volume.Mode; import org.apache.mesos.Scheduler; import org.apache.mesos.SchedulerDriver; +import com.google.protobuf.ByteString; public class JenkinsScheduler implements Scheduler { private static final String SLAVE_JAR_URI_SUFFIX = "jnlpJars/slave.jar"; @@ -147,7 +149,7 @@ public synchronized void stop() { if (driver != null) { driver.stop(); } else { - LOGGER.warning("Unable to stop Mesos driver: driver is null."); + LOGGER.warning("Unable to stop Mesos driver: driver is null."); } running = false; } @@ -247,7 +249,12 @@ public synchronized void resourceOffers(SchedulerDriver driver, List offe if (matches(offer, request)) { matched = true; LOGGER.info("Offer matched! Creating mesos task"); - createMesosTask(offer, request); + + try { + createMesosTask(offer, request); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } requests.remove(request); break; } @@ -262,6 +269,7 @@ public synchronized void resourceOffers(SchedulerDriver driver, List offe private boolean matches(Offer offer, Request request) { double cpus = -1; double mem = -1; + List ports = null; for (Resource resource : offer.getResourcesList()) { if (resource.getName().equals("cpus")) { @@ -279,7 +287,11 @@ private boolean matches(Offer offer, Request request) { } else if (resource.getName().equals("disk")) { LOGGER.fine("Ignoring disk resources from offer"); } else if (resource.getName().equals("ports")) { - LOGGER.fine("Ignoring ports resources from offer"); + if (resource.getType().equals(Value.Type.RANGES)) { + ports = resource.getRanges().getRangeList(); + } else { + LOGGER.severe("Ports resource was not a range: " + resource.getType().toString()); + } } else { LOGGER.warning("Ignoring unknown resource type: " + resource.getName()); } @@ -288,22 +300,35 @@ private boolean matches(Offer offer, Request request) { if (cpus < 0) LOGGER.fine("No cpus resource present"); if (mem < 0) LOGGER.fine("No mem resource present"); + boolean hasPortMappings = request.request.slaveInfo.getContainerInfo().hasPortMappings(); + boolean hasPortResources = ports != null && !ports.isEmpty(); + + if (hasPortMappings && !hasPortResources) { + LOGGER.severe("No ports resource present"); + } + // Check for sufficient cpu and memory resources in the offer. double requestedCpus = request.request.cpus; double requestedMem = (1 + JVM_MEM_OVERHEAD_FACTOR) * request.request.mem; // Get matching slave attribute for this label. JSONObject slaveAttributes = getMesosCloud().getSlaveAttributeForLabel(request.request.slaveInfo.getLabelString()); - if (requestedCpus <= cpus && requestedMem <= mem && slaveAttributesMatch(offer, slaveAttributes)) { + if (requestedCpus <= cpus + && requestedMem <= mem + && !(hasPortMappings && !hasPortResources) + && slaveAttributesMatch(offer, slaveAttributes)) { return true; } else { + String requestedPorts = StringUtils.join(request.request.slaveInfo.getContainerInfo().getPortMappings().toArray(), "/"); + LOGGER.info( "Offer not sufficient for slave request:\n" + offer.getResourcesList().toString() + "\n" + offer.getAttributesList().toString() + "\nRequested for Jenkins slave:\n" + - " cpus: " + requestedCpus + "\n" + - " mem: " + requestedMem + "\n" + + " cpus: " + requestedCpus + "\n" + + " mem: " + requestedMem + "\n" + + " ports: " + requestedPorts + "\n" + " attributes: " + (slaveAttributes == null ? "" : slaveAttributes.toString())); return false; } @@ -345,8 +370,36 @@ private boolean slaveAttributesMatch(Offer offer, JSONObject slaveAttributes) { return slaveTypeMatch; } + private List findPortsToUse(Offer offer, Request request, int maxCount) { + List portsToUse = new ArrayList(); + List portRangesList = null; + + for (Resource resource : offer.getResourcesList()) { + if (resource.getName().equals("ports")) { + portRangesList = resource.getRanges().getRangeList(); + break; + } + } + + LOGGER.fine("portRangesList=" + portRangesList); + + int portRangeIndex = 0; + while (portsToUse.size() < maxCount && portRangeIndex < portRangesList.size()) { + Value.Range currentPortRange = portRangesList.get(portRangeIndex); + long nextPort = currentPortRange.getBegin(); + + while (portsToUse.size() < maxCount && nextPort < currentPortRange.getEnd()) { + portsToUse.add((int)nextPort); + nextPort++; + } + } + + return portsToUse; + } + private void createMesosTask(Offer offer, Request request) { - TaskID taskId = TaskID.newBuilder().setValue(request.request.slave.name).build(); + final String slaveName = request.request.slave.name; + TaskID taskId = TaskID.newBuilder().setValue(slaveName).build(); LOGGER.info("Launching task " + taskId.getValue() + " with URI " + joinPaths(jenkinsMaster, SLAVE_JAR_URI_SUFFIX)); @@ -412,18 +465,66 @@ private void createMesosTask(Offer offer, Request request) { MesosSlaveInfo.ContainerInfo containerInfo = request.request.slaveInfo.getContainerInfo(); if (containerInfo != null) { ContainerInfo.Type containerType = ContainerInfo.Type.valueOf(containerInfo.getType()); - ContainerInfo.Builder containerInfoBuilder = ContainerInfo.newBuilder().setType(containerType); + + ContainerInfo.Builder containerInfoBuilder = ContainerInfo.newBuilder() // + .setType(containerType) // + .setHostname(slaveName); + switch(containerType) { case DOCKER: LOGGER.info("Launching in Docker Mode:" + containerInfo.getDockerImage()); - DockerInfo.Builder dockerInfoBuilder = DockerInfo.newBuilder(); + DockerInfo.Builder dockerInfoBuilder = DockerInfo.newBuilder() // + .setImage(containerInfo.getDockerImage()); + if (containerInfo.getParameters() != null) { for (MesosSlaveInfo.Parameter parameter : containerInfo.getParameters()) { LOGGER.info("Adding Docker parameter '" + parameter.getKey() + ":" + parameter.getValue() + "'"); dockerInfoBuilder.addParameters(Parameter.newBuilder().setKey(parameter.getKey()).setValue(parameter.getValue()).build()); } } - containerInfoBuilder.setDocker(dockerInfoBuilder.setImage(containerInfo.getDockerImage())); + + String networking = request.request.slaveInfo.getContainerInfo().getNetworking(); + dockerInfoBuilder.setNetwork(Network.valueOf(networking)); + + if (request.request.slaveInfo.getContainerInfo().hasPortMappings()) { + List portMappings = request.request.slaveInfo.getContainerInfo().getPortMappings(); + int portToUseIndex = 0; + List portsToUse = findPortsToUse(offer, request, portMappings.size()); + + Value.Ranges.Builder portRangesBuilder = Value.Ranges.newBuilder(); + + for (MesosSlaveInfo.PortMapping portMapping : portMappings) { + PortMapping.Builder portMappingBuilder = PortMapping.newBuilder() // + .setContainerPort(portMapping.getContainerPort()) // + .setProtocol(portMapping.getProtocol()); + + int portToUse = portMapping.getHostPort() == null ? portsToUse.get(portToUseIndex++) : portMapping.getHostPort(); + + portMappingBuilder.setHostPort(portToUse); + + portRangesBuilder.addRange( + Value.Range + .newBuilder() + .setBegin(portToUse) + .setEnd(portToUse) + ); + + LOGGER.finest("Adding portMapping: " + portMapping); + dockerInfoBuilder.addPortMappings(portMappingBuilder); + } + + taskBuilder.addResources( + Resource + .newBuilder() + .setName("ports") + .setType(Value.Type.RANGES) + .setRanges(portRangesBuilder) + ); + } else { + LOGGER.fine("No portMappings found"); + } + + containerInfoBuilder.setDocker(dockerInfoBuilder); break; default: LOGGER.warning("Unknown container type:" + containerInfo.getType()); @@ -607,7 +708,7 @@ public static void supervise() { cloud.stopScheduler(); } } else { - LOGGER.info("Schedular already stopped. NOOP."); + LOGGER.info("Scheduler already stopped. NOOP."); } } catch (Exception e) { LOGGER.info("Exception: " + e); diff --git a/src/main/java/org/jenkinsci/plugins/mesos/MesosCloud.java b/src/main/java/org/jenkinsci/plugins/mesos/MesosCloud.java index cfc42dd6d..983fcb6fb 100644 --- a/src/main/java/org/jenkinsci/plugins/mesos/MesosCloud.java +++ b/src/main/java/org/jenkinsci/plugins/mesos/MesosCloud.java @@ -46,6 +46,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.mesos.MesosNativeLibrary; +import org.apache.mesos.Protos.ContainerInfo.DockerInfo.Network; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -101,10 +102,10 @@ public static void init() { for (Cloud c : jenkins.clouds) { if( c instanceof MesosCloud) { - // Register mesos framework on init, if on demand registration is not enabled. - if (!((MesosCloud) c).isOnDemandRegistration()) { + // Register mesos framework on init, if on demand registration is not enabled. + if (!((MesosCloud) c).isOnDemandRegistration()) { ((MesosCloud)c).restartMesos(); - } + } } } } @@ -441,9 +442,9 @@ public boolean configure(StaplerRequest request, JSONObject object) .getBoolean("readOnly"))); } } - + List parameters = new ArrayList(); - + if (containerInfoJson.has("parameters")) { JSONArray parametersJson = containerInfoJson.getJSONArray("parameters"); for (Object obj : parametersJson) { @@ -452,9 +453,24 @@ public boolean configure(StaplerRequest request, JSONObject object) } } + List portMappings = new ArrayList(); + + final String networking = containerInfoJson.getString("networking"); + if (Network.BRIDGE.equals(Network.valueOf(networking)) && containerInfoJson.has("portMappings")) { + JSONArray portMappingsJson = containerInfoJson + .getJSONArray("portMappings"); + for (Object obj : portMappingsJson) { + JSONObject portMappingJson = (JSONObject) obj; + portMappings.add(new MesosSlaveInfo.PortMapping( + portMappingJson.getInt("containerPort"), + portMappingJson.getInt("hostPort"), + portMappingJson.getString("protocol"))); + } + } + containerInfo = new MesosSlaveInfo.ContainerInfo( containerInfoJson.getString("type"), - containerInfoJson.getString("dockerImage"), volumes, parameters); + containerInfoJson.getString("dockerImage"), volumes, parameters, networking, portMappings); } List additionalURIs = new ArrayList(); diff --git a/src/main/java/org/jenkinsci/plugins/mesos/MesosSlaveInfo.java b/src/main/java/org/jenkinsci/plugins/mesos/MesosSlaveInfo.java index 348f175d1..89da2af94 100644 --- a/src/main/java/org/jenkinsci/plugins/mesos/MesosSlaveInfo.java +++ b/src/main/java/org/jenkinsci/plugins/mesos/MesosSlaveInfo.java @@ -2,6 +2,7 @@ import hudson.model.Descriptor.FormException; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -10,6 +11,7 @@ import net.sf.json.JSONSerializer; import org.apache.commons.lang.StringUtils; +import org.apache.mesos.Protos.ContainerInfo.DockerInfo.Network; import org.kohsuke.stapler.DataBoundConstructor; public class MesosSlaveInfo { @@ -123,7 +125,9 @@ public String getJvmArgs() { return jvmArgs; } - public String getJnlpArgs() {return jnlpArgs; } + public String getJnlpArgs() { + return jnlpArgs; + } public ExternalContainerInfo getExternalContainerInfo() { return externalContainerInfo; @@ -176,14 +180,28 @@ public static class ContainerInfo { private final String dockerImage; private final List volumes; private final List parameters; + private final String networking; + private final List portMappings; @DataBoundConstructor - public ContainerInfo(String type, String dockerImage, List volumes, List parameters) + public ContainerInfo(String type, String dockerImage, List volumes, List parameters, String networking, List portMappings) throws FormException { this.type = type; this.dockerImage = dockerImage; this.volumes = volumes; this.parameters = parameters; + + if (networking == null) { + this.networking = Network.BRIDGE.toString(); + } else { + this.networking = networking; + } + + if (Network.HOST.equals(Network.valueOf(networking))) { + this.portMappings = Collections.emptyList(); + } else { + this.portMappings = portMappings; + } } public String getType() { @@ -197,31 +215,76 @@ public String getDockerImage() { public List getVolumes() { return volumes; } - + public List getParameters() { return parameters; } + + public String getNetworking() { + return networking; + } + + public List getPortMappings() { + return portMappings; + } + + public boolean hasPortMappings() { + return portMappings != null && !portMappings.isEmpty(); + } } - + public static class Parameter { private final String key; private final String value; - + @DataBoundConstructor public Parameter(String key, String value) { this.key = key; this.value = value; } - + public String getKey() { return key; } - + public String getValue() { return value; } } + public static class PortMapping { + + // TODO validate 1 to 65535 + private final Integer containerPort; + private final Integer hostPort; + private final String protocol; + + @DataBoundConstructor + public PortMapping(Integer containerPort, Integer hostPort, String protocol) { + this.containerPort = containerPort; + this.hostPort = hostPort; + this.protocol = protocol; + } + + public Integer getContainerPort() { + return containerPort; + } + + public Integer getHostPort() { + return hostPort; + } + + public String getProtocol() { + return protocol; + } + + @Override + public String toString() { + return (hostPort == null ? 0 : hostPort) + ":" + containerPort; + } + + } + public static class Volume { private final String containerPath; private final String hostPath; diff --git a/src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/config.jelly b/src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/config.jelly index 171f16d48..d9dd3133d 100644 --- a/src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/config.jelly @@ -103,6 +103,37 @@ + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+