Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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,29 @@
package io.jenkins.plugins.casc.core;

import hudson.AbortException;
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.slaves.ComputerListener;
import java.io.IOException;

@SuppressWarnings("unused")
@Extension
public class DisconnectedOnStartupListener extends ComputerListener {

@Override
public void preLaunch(Computer c, TaskListener listener) throws IOException {
Node node = c.getNode();
if (node == null) {
return;
}

DisconnectedOnStartupProperty prop = node.getNodeProperties().get(DisconnectedOnStartupProperty.class);

if (prop != null && prop.isEnabled()) {
listener.getLogger().println("Launch canceled: Node marked to remain disconnected on startup by JCasC.");
throw new AbortException("JCasC DisconnectedOnStartupProperty prevented launch.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.jenkins.plugins.casc.core;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.Node;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

public class DisconnectedOnStartupProperty extends NodeProperty<Node> {

private String reason;
private boolean enabled = true;

@DataBoundConstructor
public DisconnectedOnStartupProperty() {}

@DataBoundSetter
public void setReason(String reason) {
this.reason = reason != null ? reason.trim() : null;
}

@DataBoundSetter
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public boolean isEnabled() {
return enabled;
}

public String getReason() {
return (reason != null && !reason.trim().isEmpty()) ? reason : "Disconnected via JCasC configuration";
}

@Extension
@Symbol("disconnectedOnStartup")
public static class DescriptorImpl extends NodePropertyDescriptor {
@Override
@NonNull
public String getDisplayName() {
return "Disconnect Node on Startup";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.jenkins.plugins.casc.core;

import hudson.slaves.OfflineCause;
import java.io.Serializable;

public class JCasCOfflineCause extends OfflineCause implements Serializable {

private static final long serialVersionUID = 1L;

private final String reason;

public JCasCOfflineCause(String reason) {
this.reason = reason != null ? reason : "Disconnected via JCasC configuration";
}

@Override
public String toString() {
return "JCasC Startup State: " + reason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.ProxyConfiguration;
import hudson.model.Computer;
import hudson.model.ComputerSet;
import hudson.model.Descriptor;
import hudson.model.Node;
Expand Down Expand Up @@ -79,6 +80,27 @@ protected Jenkins instance(Mapping mapping, ConfigurationContext context) {
.collect(Collectors.toList());
nodesToKeep.addAll(configuredNodes);
jenkins.setNodes(nodesToKeep);
for (Node node : nodesToKeep) {
DisconnectedOnStartupProperty prop =
node.getNodeProperties().get(DisconnectedOnStartupProperty.class);

Computer c = node.toComputer();
if (c == null) {
continue;
}

if (prop != null && prop.isEnabled()) {
c.setTemporaryOfflineCause(new JCasCOfflineCause(prop.getReason()));
LOGGER.fine(() -> "Marking node '" + node.getNodeName() + "' offline via JCasC");
} else {
if (c.isOffline() && c.getOfflineCause() instanceof JCasCOfflineCause) {
c.setTemporaryOfflineCause(null);
if (!c.isConnecting()) {
c.connect(false);
}
}
}
}
}));

// Add updateCenter, all legwork will be done by a configurator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package io.jenkins.plugins.casc.core;

import static hudson.util.StreamTaskListener.fromStdout;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import hudson.model.Computer;
import hudson.model.Node;
import io.jenkins.plugins.casc.ConfigurationAsCode;
import java.util.Objects;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

public class DisconnectedOnStartupTest {

@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void should_mark_node_offline_on_startup() {
String yaml = Objects.requireNonNull(getClass().getResource("disconnectedOnStartup.yml"))
.toExternalForm();

ConfigurationAsCode.get().configure(yaml);

Node node = Jenkins.get().getNode("test-node-enabled");
assertNotNull(node);

Computer computer = node.toComputer();
assertNotNull(computer);

assertThat(computer.getOfflineCause(), instanceOf(JCasCOfflineCause.class));
assertThat(computer.getOfflineCause().toString(), is("JCasC Startup State: Maintenance Mode"));
}

@Test
public void should_not_mark_node_offline_when_disabled() {
String yaml = Objects.requireNonNull(getClass().getResource("disconnectedOnStartupDisabled.yml"))
.toExternalForm();

ConfigurationAsCode.get().configure(yaml);

Node node = Jenkins.get().getNode("test-node-disabled");
assertNotNull(node);

Computer computer = node.toComputer();
assertNotNull(computer);

assertThat(computer.getOfflineCause() instanceof JCasCOfflineCause, is(false));
}

@Test
public void should_keep_node_online_when_property_not_defined() {
String yaml = Objects.requireNonNull(
getClass().getResource("noDisconnectedProperty.yml"), "YAML resource not found")
.toExternalForm();

ConfigurationAsCode.get().configure(yaml);

Node node = Jenkins.get().getNode("test-node-default");
assertNotNull(node);

Computer computer = node.toComputer();
assertNotNull(computer);

assertThat(computer.getOfflineCause() instanceof JCasCOfflineCause, is(false));
}

@Test
public void should_reconnect_node_when_property_disabled_after_being_enabled() throws Exception {
String yamlEnabled = Objects.requireNonNull(getClass().getResource("disconnectedOnStartup.yml"))
.toExternalForm();

ConfigurationAsCode.get().configure(yamlEnabled);

Node node = Jenkins.get().getNode("test-node-enabled");
assertNotNull(node);

Computer computer = node.toComputer();
assertNotNull(computer);

assertThat(computer.getOfflineCause(), instanceOf(JCasCOfflineCause.class));

j.waitUntilNoActivity();

String yamlToggled = Objects.requireNonNull(getClass().getResource("disconnectedOnStartupToggle.yml"))
.toExternalForm();

ConfigurationAsCode.get().configure(yamlToggled);

Computer updatedComputer = Objects.requireNonNull(Jenkins.get().getNode("test-node-enabled"))
.toComputer();
assertNotNull(updatedComputer);

j.waitUntilNoActivity();

assertThat(updatedComputer.getOfflineCause() instanceof JCasCOfflineCause, is(false));

assertThat(updatedComputer.isConnecting(), is(false));
}

@Test
public void should_prevent_launch_when_disconnected_on_startup_enabled() throws Exception {
String yaml = Objects.requireNonNull(getClass().getResource("disconnectedOnStartup.yml"))
.toExternalForm();

ConfigurationAsCode.get().configure(yaml);

Node node = Jenkins.get().getNode("test-node-enabled");
assertNotNull(node);

Computer computer = node.toComputer();
assertNotNull(computer);

computer.connect(false);

j.waitUntilNoActivity();

assertThat(computer.getOfflineCause(), instanceOf(JCasCOfflineCause.class));
}

@Test
public void should_safely_ignore_computer_with_null_node() throws Exception {
Node tempNode = j.createSlave("temp-node", "label", null);
Computer computer = tempNode.toComputer();
assertNotNull(computer);

Jenkins.get().removeNode(tempNode);
assertNull("Node should be null after removal", computer.getNode());

DisconnectedOnStartupListener listener = new DisconnectedOnStartupListener();

listener.preLaunch(computer, fromStdout());

assertTrue("Listener safely ignored the null node", true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import jenkins.model.Jenkins;
import org.hamcrest.Matchers;
import org.htmlunit.html.HtmlPage;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.JenkinsRule;
Expand Down Expand Up @@ -135,7 +136,12 @@ private void assertCascTileShows(WebClient webClient) throws Exception {

private void assertActionAvailable(HtmlPage page, Action action, boolean shouldContain) {
String responseContent = page.getWebResponse().getContentAsString();
if (shouldContain) {
if (action == APPLY_NEW_CONFIGURATION && shouldContain) {
assertThat(
format("Action %s should be available", action.name()),
responseContent,
Matchers.anyOf(containsString("Setup configuration"), containsString("Apply configuration")));
} else if (shouldContain) {
assertThat(
format("Action %s should be available", action.name()),
responseContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
jenkins:
nodes:
- permanent:
name: "test-node-enabled"
remoteFS: "/tmp"
numExecutors: 1
mode: NORMAL
nodeProperties:
- disconnectedOnStartup:
enabled: true
reason: "Maintenance Mode"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
jenkins:
nodes:
- permanent:
name: "test-node-disabled"
remoteFS: "/tmp"
numExecutors: 1
mode: NORMAL
nodeProperties:
- disconnectedOnStartup:
enabled: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
jenkins:
nodes:
- permanent:
name: "test-node-enabled"
remoteFS: "/tmp"
numExecutors: 1
mode: NORMAL
nodeProperties:
- disconnectedOnStartup:
enabled: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
jenkins:
nodes:
- permanent:
name: "test-node-default"
remoteFS: "/tmp"
numExecutors: 1
mode: NORMAL
Loading