diff --git a/README.md b/README.md index e60a4f8cf..ffbfac3b2 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,18 @@ template, it can be configured on cloud level and leave the filed blank in the templates. -Aside from machine/node attributes, every template require name and -labels to be configured. Name will serve both as an identifier of the +Aside from machine/node attributes, every template requires name, labels and +usage mode to be configured. Name will serve both as an identifier of the template as well as a name prefix for Jenkins node and OpenStack machine (that is why some limitations apply here). Labels field expects a set of Jenkins labels that will be assigned to all nodes that the template -provisions. It will also be used to determine which cloud and template to -use to process Jenkins load. When there is a build with no label -requirements, any -template can be used to provision the node. Build with label restriction +provisions. The Usage field specifies whether nodes will be provisioned for +builds that have no label requirement (_Use this node as much as possible_) +or only if a build's label is present (_Only build jobs with label expressions matching this node_). +The combination of the Labels and Usage fields will be used to determine +which cloud and template to use to process Jenkins load. When there is a build with no label +requirements, any template where the Usage mode is '_Use this node as much as possible_' +can be used to provision the node. Builds with label restrictions can trigger provisioning only on templates with matching label set. The attributes at template level will inherit all global values (the value in effect is printed under the field on hte config page). In case required @@ -107,19 +110,24 @@ jenkins: templates: - name: "empty" labels: "linux" + slaveOptions: + mode: EXCLUSIVE - name: "jnlp" labels: "jnlp" slaveOptions: + mode: NORMAL launcherFactory: "jnlp" - name: "volumeSnapshot" labels: "volume" slaveOptions: + mode: NORMAL bootSource: volumeSnapshot: name: "Volume name" - name: "volumeFromImage" labels: "volume from image" slaveOptions: + mode: NORMAL bootSource: volumeFromImage: name: "Volume name" diff --git a/docs/config.png b/docs/config.png index 710903fc5..954852cc8 100644 Binary files a/docs/config.png and b/docs/config.png differ diff --git a/docs/options.png b/docs/options.png index 3fd9bd802..bb235622c 100644 Binary files a/docs/options.png and b/docs/options.png differ diff --git a/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlave.java b/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlave.java index 7feb13f20..ec2f684d8 100644 --- a/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlave.java +++ b/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlave.java @@ -95,7 +95,7 @@ public JCloudsSlave( this.cache = makeCache(); setNumExecutors(slaveOptions.getNumExecutors()); - setMode(Mode.NORMAL); + setMode(slaveOptions.getMode() != null ? slaveOptions.getMode() : Node.Mode.NORMAL); setLabelString(labelString); setRetentionStrategy(new JCloudsRetentionStrategy()); setNodeProperties(mkNodeProperties(Openstack.getAccessIpAddress(metadata), slaveOptions.getNodeProperties())); diff --git a/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlaveTemplate.java b/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlaveTemplate.java index d61f09d51..da4f5fa8b 100644 --- a/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlaveTemplate.java +++ b/plugin/src/main/java/jenkins/plugins/openstack/compute/JCloudsSlaveTemplate.java @@ -3,11 +3,13 @@ import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.RelativePath; import hudson.Util; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Failure; import hudson.model.Label; +import hudson.model.Node; import hudson.model.TaskListener; import hudson.model.labels.LabelAtom; import hudson.util.FormValidation; @@ -91,7 +93,6 @@ public class JCloudsSlaveTemplate implements Describable, public JCloudsSlaveTemplate(final @Nonnull String name, final @Nonnull String labels, final @CheckForNull SlaveOptions slaveOptions) { this.name = Util.fixNull(name).trim(); this.labelString = Util.fixNull(labels).trim(); - this.slaveOptions = slaveOptions == null ? SlaveOptions.empty() : slaveOptions; readResolve(); @@ -195,7 +196,9 @@ public Set getLabelSet() { } public boolean canProvision(final Label label) { - return label == null || label.matches(labelSet); + return label == null + ? slaveOptions.getMode() == null || slaveOptions.getMode() == Node.Mode.NORMAL + : label.matches(labelSet); } /*package*/ boolean hasProvisioned(@Nonnull Server server) { @@ -535,5 +538,14 @@ public FormValidation doCheckName(@QueryParameter String value) { return FormValidation.error(ex.getMessage()); } } + + @Restricted(DoNotUse.class) + @RequirePOST + public FormValidation doCheckLabels(@QueryParameter String value, @RelativePath("slaveOptions") @QueryParameter Node.Mode mode) { + if ((value == null || value.trim().isEmpty()) && mode == Node.Mode.EXCLUSIVE) { + return FormValidation.warning("Nodes without any labels and running in exclusive mode will never be provisioned"); + } + return FormValidation.ok(); + } } } diff --git a/plugin/src/main/java/jenkins/plugins/openstack/compute/SlaveOptions.java b/plugin/src/main/java/jenkins/plugins/openstack/compute/SlaveOptions.java index 0416f8548..afc3b0a1a 100644 --- a/plugin/src/main/java/jenkins/plugins/openstack/compute/SlaveOptions.java +++ b/plugin/src/main/java/jenkins/plugins/openstack/compute/SlaveOptions.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Util; import hudson.model.Describable; +import hudson.model.Node; import hudson.slaves.NodeProperty; import jenkins.model.Jenkins; import jenkins.plugins.openstack.compute.slaveopts.BootSource; @@ -54,7 +55,7 @@ */ public class SlaveOptions implements Describable, Serializable { private static final long serialVersionUID = -1L; - private static final SlaveOptions EMPTY = new SlaveOptions(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + private static final SlaveOptions EMPTY = new SlaveOptions(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); // Provisioning attributes private /*final*/ @CheckForNull BootSource bootSource; @@ -70,6 +71,7 @@ public class SlaveOptions implements Describable, Serializable { private final @CheckForNull String keyPairName; // Slave launch attributes + private final @Nonnull Node.Mode mode; private final Integer numExecutors; private final @CheckForNull String jvmOptions; private final String fsRoot; @@ -138,6 +140,10 @@ public Integer getStartTimeout() { return keyPairName; } + public @Nonnull Node.Mode getMode() { + return mode; + } + public Integer getNumExecutors() { return numExecutors; } @@ -173,6 +179,7 @@ public SlaveOptions(Builder b) { b.availabilityZone, b.startTimeout, b.keyPairName, + b.mode, b.numExecutors, b.jvmOptions, b.fsRoot, @@ -196,6 +203,7 @@ public SlaveOptions( String availabilityZone, Integer startTimeout, String keyPairName, + Node.Mode mode, Integer numExecutors, String jvmOptions, String fsRoot, @@ -215,6 +223,7 @@ public SlaveOptions( this.availabilityZone = Util.fixEmpty(availabilityZone); this.startTimeout = startTimeout; this.keyPairName = Util.fixEmpty(keyPairName); + this.mode = mode; this.numExecutors = numExecutors; this.jvmOptions = Util.fixEmpty(jvmOptions); this.fsRoot = Util.fixEmpty(fsRoot); @@ -252,6 +261,7 @@ private Object readResolve() { .availabilityZone(_override(this.availabilityZone, o.availabilityZone)) .startTimeout(_override(this.startTimeout, o.startTimeout)) .keyPairName(_override(this.keyPairName, o.keyPairName)) + .mode(_override(this.mode, o.mode)) .numExecutors(_override(this.numExecutors, o.numExecutors)) .jvmOptions(_override(this.jvmOptions, o.jvmOptions)) .fsRoot(_override(this.fsRoot, o.fsRoot)) @@ -283,6 +293,7 @@ private Object readResolve() { .availabilityZone(_erase(this.availabilityZone, defaults.availabilityZone)) .startTimeout(_erase(this.startTimeout, defaults.startTimeout)) .keyPairName(_erase(this.keyPairName, defaults.keyPairName)) + .mode(_erase(this.mode, defaults.mode)) .numExecutors(_erase(this.numExecutors, defaults.numExecutors)) .jvmOptions(_erase(this.jvmOptions, defaults.jvmOptions)) .fsRoot(_erase(this.fsRoot, defaults.fsRoot)) @@ -315,6 +326,7 @@ public String toString() { .append("availabilityZone", availabilityZone) .append("startTimeout", startTimeout) .append("keyPairName", keyPairName) + .append("mode", mode) .append("numExecutors", numExecutors) .append("jvmOptions", jvmOptions) .append("fsRoot", fsRoot) @@ -344,6 +356,7 @@ public boolean equals(Object o) { if (!Objects.equals(availabilityZone, that.availabilityZone)) return false; if (!Objects.equals(startTimeout, that.startTimeout)) return false; if (!Objects.equals(keyPairName, that.keyPairName)) return false; + if (!Objects.equals(mode, that.mode)) return false; if (!Objects.equals(numExecutors, that.numExecutors)) return false; if (!Objects.equals(jvmOptions, that.jvmOptions)) return false; if (!Objects.equals(fsRoot, that.fsRoot)) return false; @@ -366,6 +379,7 @@ public int hashCode() { result = 31 * result + (availabilityZone != null ? availabilityZone.hashCode() : 0); result = 31 * result + (startTimeout != null ? startTimeout.hashCode() : 0); result = 31 * result + (keyPairName != null ? keyPairName.hashCode() : 0); + result = 31 * result + (mode != null ? mode.hashCode() : 0); result = 31 * result + (numExecutors != null ? numExecutors.hashCode() : 0); result = 31 * result + (jvmOptions != null ? jvmOptions.hashCode() : 0); result = 31 * result + (fsRoot != null ? fsRoot.hashCode() : 0); @@ -392,6 +406,7 @@ public Builder getBuilder() { .availabilityZone(availabilityZone) .startTimeout(startTimeout) .keyPairName(keyPairName) + .mode(mode) .numExecutors(numExecutors) .jvmOptions(jvmOptions) .fsRoot(fsRoot) @@ -426,6 +441,7 @@ public static final class Builder { private @CheckForNull Integer startTimeout; private @CheckForNull String keyPairName; + private @CheckForNull Node.Mode mode; private @CheckForNull Integer numExecutors; private @CheckForNull String jvmOptions; private @CheckForNull String fsRoot; @@ -496,6 +512,11 @@ public Builder() {} return this; } + public @Nonnull Builder mode(Node.Mode mode) { + this.mode = mode; + return this; + } + public @Nonnull Builder numExecutors(Integer numExecutors) { this.numExecutors = numExecutors; return this; diff --git a/plugin/src/main/resources/jenkins/plugins/openstack/compute/SlaveOptions/config.jelly b/plugin/src/main/resources/jenkins/plugins/openstack/compute/SlaveOptions/config.jelly index 3ea4f467b..a4ea01df3 100644 --- a/plugin/src/main/resources/jenkins/plugins/openstack/compute/SlaveOptions/config.jelly +++ b/plugin/src/main/resources/jenkins/plugins/openstack/compute/SlaveOptions/config.jelly @@ -17,6 +17,7 @@
+ diff --git a/plugin/src/test/java/jenkins/plugins/openstack/PluginTestRule.java b/plugin/src/test/java/jenkins/plugins/openstack/PluginTestRule.java index 5bd8ec806..463547ecc 100644 --- a/plugin/src/test/java/jenkins/plugins/openstack/PluginTestRule.java +++ b/plugin/src/test/java/jenkins/plugins/openstack/PluginTestRule.java @@ -119,7 +119,7 @@ public static SlaveOptions dummySlaveOptions() { dummyUserData("dummyUserDataId"); } return new SlaveOptions( - new BootSource.VolumeSnapshot("id"), "hw", "nw1,mw2", "dummyUserDataId", 1, 2, "pool", "sg", "az", 1, null, 10, + new BootSource.VolumeSnapshot("id"), "hw", "nw1,mw2", "dummyUserDataId", 1, 2, "pool", "sg", "az", 1, null, Node.Mode.NORMAL, 10, "jvmo", "fsRoot", LauncherFactory.JNLP.JNLP, mkListOfNodeProperties(1, 2), 1, null ); } @@ -185,6 +185,7 @@ public SlaveOptions defaultSlaveOptions() { .keyPairName("dummyKeyPairName") .jvmOptions("dummyJvmOptions") .fsRoot("/tmp/jenkins") + .mode(Node.Mode.NORMAL) .launcherFactory(LauncherFactory.JNLP.JNLP) .build() ; diff --git a/plugin/src/test/java/jenkins/plugins/openstack/compute/JCloudsCloudTest.java b/plugin/src/test/java/jenkins/plugins/openstack/compute/JCloudsCloudTest.java index 0bf8a6c40..9e55be54f 100644 --- a/plugin/src/test/java/jenkins/plugins/openstack/compute/JCloudsCloudTest.java +++ b/plugin/src/test/java/jenkins/plugins/openstack/compute/JCloudsCloudTest.java @@ -13,6 +13,7 @@ import hudson.ExtensionList; import hudson.model.Item; import hudson.model.Label; +import hudson.model.Node; import hudson.model.UnprotectedRootAction; import hudson.model.User; import hudson.security.ACL; @@ -133,10 +134,10 @@ public void presentUIDefaults() throws Exception { String openstackAuth = j.dummyCredentials(); JCloudsSlaveTemplate template = new JCloudsSlaveTemplate("template", "label", new SlaveOptions( - new BootSource.Image("iid"), "hw", "nw", "ud", 1, 0, "public", "sg", "az", 2, "kp", 3, "jvmo", "fsRoot", LauncherFactory.JNLP.JNLP, null, 4, false + new BootSource.Image("iid"), "hw", "nw", "ud", 1, 0, "public", "sg", "az", 2, "kp", Node.Mode.NORMAL, 3, "jvmo", "fsRoot", LauncherFactory.JNLP.JNLP, null, 4, false )); JCloudsCloud cloud = new JCloudsCloud("openstack", "endPointUrl", false,"zone", new SlaveOptions( - new BootSource.VolumeSnapshot("vsid"), "HW", "NW", "UD", 6, 4, null, "SG", "AZ", 7, "KP", 8, "JVMO", "FSrOOT", new LauncherFactory.SSH("cid"), null, 9, false + new BootSource.VolumeSnapshot("vsid"), "HW", "NW", "UD", 6, 4, null, "SG", "AZ", 7, "KP", Node.Mode.NORMAL, 8, "JVMO", "FSrOOT", new LauncherFactory.SSH("cid"), null, 9, false ), Collections.singletonList(template),openstackAuth); j.jenkins.clouds.add(cloud); diff --git a/plugin/src/test/java/jenkins/plugins/openstack/compute/SlaveOptionsTest.java b/plugin/src/test/java/jenkins/plugins/openstack/compute/SlaveOptionsTest.java index 877bdc4f4..98030385a 100644 --- a/plugin/src/test/java/jenkins/plugins/openstack/compute/SlaveOptionsTest.java +++ b/plugin/src/test/java/jenkins/plugins/openstack/compute/SlaveOptionsTest.java @@ -34,6 +34,7 @@ public void defaultOverrides() { assertEquals("sg", unmodified.getSecurityGroups()); assertEquals("az", unmodified.getAvailabilityZone()); assertEquals(1, (int) unmodified.getStartTimeout()); + assertEquals(Node.Mode.NORMAL, unmodified.getMode()); assertEquals(10, (int) unmodified.getNumExecutors()); assertEquals("jvmo", unmodified.getJvmOptions()); assertEquals("fsRoot", unmodified.getFsRoot()); @@ -53,6 +54,7 @@ public void defaultOverrides() { .securityGroups("SG") .availabilityZone("AZ") .startTimeout(4) + .mode(Node.Mode.NORMAL) .numExecutors(2) .jvmOptions("JVMO") .fsRoot("FSROOT") @@ -74,6 +76,7 @@ public void defaultOverrides() { assertEquals("SG", overridden.getSecurityGroups()); assertEquals("AZ", overridden.getAvailabilityZone()); assertEquals(4, (int) overridden.getStartTimeout()); + assertEquals(Node.Mode.NORMAL, overridden.getMode()); assertEquals(2, (int) overridden.getNumExecutors()); assertEquals("JVMO", overridden.getJvmOptions()); assertEquals("FSROOT", overridden.getFsRoot()); @@ -99,7 +102,7 @@ public void eraseDefaults() { public void emptyStrings() { SlaveOptions nulls = SlaveOptions.empty(); SlaveOptions emptyStrings = new SlaveOptions( - null, "", "", "", null, null, "", "", "", null, "", null, "", "", null, null, null, null + null, "", "", "", null, null, "", "", "", null, "", null, null, "", "", null, null, null, null ); SlaveOptions emptyBuilt = SlaveOptions.builder() .hardwareId("") diff --git a/plugin/src/test/resources/jenkins/plugins/openstack/JcascTest/jcasc.yaml b/plugin/src/test/resources/jenkins/plugins/openstack/JcascTest/jcasc.yaml index 8ed30d3d6..01e0019b7 100644 --- a/plugin/src/test/resources/jenkins/plugins/openstack/JcascTest/jcasc.yaml +++ b/plugin/src/test/resources/jenkins/plugins/openstack/JcascTest/jcasc.yaml @@ -37,16 +37,19 @@ jenkins: - name: "jnlp" labels: "jnlp" slaveOptions: + mode: NORMAL launcherFactory: "jnlp" - name: "volumeSnapshot" labels: "volume" slaveOptions: + mode: NORMAL bootSource: volumeSnapshot: name: "Volume name" - name: "volumeFromImage" labels: "volume from image" slaveOptions: + mode: NORMAL bootSource: volumeFromImage: name: "Volume name" @@ -54,11 +57,13 @@ jenkins: - name: "customNodeProperties" labels: "templateWithItsOwnNodeProperties" slaveOptions: + mode: NORMAL nodeProperties: - nodePropertyTwo - name: "noNodeProperties" labels: "templateWithNoNodePropertiesInsteadOfDefaults" slaveOptions: + mode: NORMAL nodeProperties: [] unclassified: