-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(*): draft version v1.0 for jenkins-multipass-cloud that pass com…
…pilation and unit test
- Loading branch information
Showing
8 changed files
with
585 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
src/main/java/io/hainenber/jenkins/multipass/MultipassAgent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package io.hainenber.jenkins.multipass; | ||
|
||
import hudson.model.Descriptor; | ||
import hudson.model.TaskListener; | ||
import hudson.slaves.AbstractCloudComputer; | ||
import hudson.slaves.AbstractCloudSlave; | ||
import hudson.slaves.ComputerLauncher; | ||
import io.hainenber.jenkins.multipass.sdk.MultipassClient; | ||
import jakarta.annotation.Nonnull; | ||
import jenkins.model.Jenkins; | ||
import org.apache.commons.lang.StringUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.IOException; | ||
import java.io.Serial; | ||
import java.util.Objects; | ||
|
||
public class MultipassAgent extends AbstractCloudSlave { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(MultipassAgent.class); | ||
private final transient MultipassCloud cloud; | ||
|
||
@Serial | ||
private static final long serialVersionUID = 2553788927582449937L; | ||
|
||
|
||
/** | ||
* Constructor | ||
* @param cloud a {@link MultipassCloud} object; | ||
* @param name the name of the agent. | ||
* @param launcher a {@link hudson.slaves.ComputerLauncher} object. | ||
* @throws hudson.model.Descriptor.FormException if any. | ||
* @throws java.io.IOException if any. | ||
*/ | ||
public MultipassAgent(MultipassCloud cloud, @Nonnull String name, @Nonnull ComputerLauncher launcher) throws Descriptor.FormException, IOException { | ||
super(name, "/build", launcher); | ||
this.cloud = cloud; | ||
} | ||
|
||
/** | ||
* Get cloud instance associated with this builder agent. | ||
* @return a {@link MultipassCloud} object. | ||
*/ | ||
public MultipassCloud getCloud() { | ||
return cloud; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public AbstractCloudComputer<MultipassAgent> createComputer() { | ||
return new MultipassComputer(this); | ||
} | ||
|
||
/** {@inheritDoc} */ | ||
protected void _terminate(TaskListener listener) { | ||
listener.getLogger().println("[multipass-cloud]: Terminating agent " + getDisplayName()); | ||
|
||
if (getLauncher() instanceof MultipassLauncher) { | ||
var instanceName = Objects.requireNonNull(getComputer()).getName(); | ||
if (StringUtils.isBlank(instanceName)) { | ||
return; | ||
} | ||
|
||
try { | ||
LOGGER.info("[multipass-cloud]: Terminating instance named '{}'", instanceName); | ||
MultipassClient multipassClient = cloud.getMultipassClient(); | ||
multipassClient.terminateInstance(instanceName); | ||
LOGGER.info("[multipass-cloud]: Terminated instance named '{}'", instanceName); | ||
Jenkins.get().removeNode(this); | ||
LOGGER.info("[multipass-cloud]: Removed Multipass instance named '{}' from Jenkins controller", instanceName); | ||
} catch (IOException e) { | ||
LOGGER.warn("[multipass-cloud]: Failed to terminate Multipass instance named '{}'", instanceName); | ||
} | ||
} | ||
} | ||
} |
225 changes: 225 additions & 0 deletions
225
src/main/java/io/hainenber/jenkins/multipass/MultipassCloud.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
package io.hainenber.jenkins.multipass; | ||
|
||
import hudson.model.Computer; | ||
import hudson.model.Descriptor; | ||
import hudson.model.Label; | ||
import hudson.model.Node; | ||
import hudson.model.labels.LabelAtom; | ||
import hudson.slaves.Cloud; | ||
import hudson.slaves.NodeProvisioner; | ||
import io.hainenber.jenkins.multipass.sdk.MultipassClient; | ||
import jakarta.annotation.Nonnull; | ||
import jenkins.model.Jenkins; | ||
import org.apache.commons.lang.RandomStringUtils; | ||
import org.apache.commons.lang.StringUtils; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
import org.kohsuke.stapler.DataBoundSetter; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.IOException; | ||
import java.util.*; | ||
import java.util.concurrent.Future; | ||
|
||
/** | ||
* Root class that contains all configuration state about | ||
* Multipass cloud agents. | ||
* | ||
* @author dotronghai | ||
*/ | ||
public class MultipassCloud extends Cloud { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(MultipassCloud.class); | ||
private static final int DEFAULT_AGENT_TIMEOUT = 120; | ||
private static final String DEFAULT_AGENT_DISTRIBUTION_ALIAS = "noble"; | ||
|
||
private String[] labels; | ||
private String distroAlias; | ||
private Integer cpus; | ||
private String memory; | ||
private String disk; | ||
private String cloudConfig; | ||
|
||
private transient MultipassClient client; | ||
private transient long lastProvisionTime = 0; | ||
|
||
static { | ||
clearAllNodes(); | ||
} | ||
|
||
/** | ||
* Constructor for MultipassCloud | ||
* @param name the name of the cloud, will auto-generated if not inputted. | ||
*/ | ||
@DataBoundConstructor | ||
public MultipassCloud(String name, @Nonnull String projectName) { | ||
super(StringUtils.isNotBlank(name) ? name: String.format("multipass_cloud_%s", jenkinsController().clouds.size())); | ||
LOGGER.info("[multipass-cloud] Initializing Cloud {}", this); | ||
} | ||
|
||
/** | ||
* Get the active Jenkins instance. | ||
* | ||
* @return a {@link Jenkins} object. | ||
*/ | ||
@Nonnull | ||
protected static Jenkins jenkinsController() { | ||
return Objects.requireNonNull(Jenkins.getInstanceOrNull()); | ||
} | ||
|
||
/** | ||
* Clear all nodes on bootup | ||
*/ | ||
private static void clearAllNodes() { | ||
List<Node> nodes = jenkinsController().getNodes(); | ||
if (nodes.isEmpty()) { | ||
return; | ||
} | ||
|
||
LOGGER.info("[multipass-cloud]: Deleting all previous Multipass nodes..."); | ||
|
||
for (final Node node: nodes) { | ||
if (node instanceof MultipassAgent) { | ||
try { | ||
((MultipassAgent) node).terminate(); | ||
} catch (InterruptedException | IOException e) { | ||
LOGGER.error("[multipass-cloud]: Failed to terminate agent '{}'", node.getDisplayName(), e); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** Getter for the field <code>client</code> | ||
* | ||
* @return a {@link MultipassClient} | ||
*/ | ||
public synchronized MultipassClient getMultipassClient() { | ||
if (this.client == null) { | ||
this.client = new MultipassClient(); | ||
} | ||
return this.client; | ||
} | ||
|
||
public String getLabel() { | ||
return ""; | ||
} | ||
|
||
/** {@inheritDoc} */ | ||
@Override | ||
public synchronized Collection<NodeProvisioner.PlannedNode> provision(CloudState cloudState, int excessWorkload) { | ||
List<NodeProvisioner.PlannedNode> nodeList = new ArrayList<NodeProvisioner.PlannedNode>(); | ||
Label label = cloudState.getLabel(); | ||
|
||
// Guard against non-matching labels | ||
if (label != null && !label.matches(List.of(new LabelAtom(getLabel())))) { | ||
return nodeList; | ||
} | ||
|
||
// Guard against double-provisioning with a 500ms cooldown check | ||
long timeDiff = System.currentTimeMillis() - lastProvisionTime; | ||
if (timeDiff < 500) { | ||
LOGGER.info("[multipass-cloud] Provision of {} skipped, still on cooldown ({}ms of 500ms)", | ||
excessWorkload, | ||
timeDiff | ||
); | ||
return nodeList; | ||
} | ||
|
||
String labelName = Objects.isNull(label) ? getLabel() : label.getDisplayName(); | ||
long currentlyProvisioningInstanceCount = getCurrentlyProvisionedAgentCount(); | ||
long numInstancesToLaunch = Math.max(excessWorkload - currentlyProvisioningInstanceCount, 0); | ||
LOGGER.info("[multipass-cloud] Provisioning {} nodes for label '{}' ({} already provisioning)", | ||
numInstancesToLaunch, | ||
labelName, | ||
currentlyProvisioningInstanceCount | ||
); | ||
|
||
// Initializing builder nodes and add to list of provisioned instances. | ||
for (int i = 0; i < numInstancesToLaunch; i++) { | ||
final String suffix = RandomStringUtils.randomAlphabetic(4); | ||
final String displayName = String.format("%s-multipass", suffix); | ||
final MultipassCloud cloud = this; | ||
final Future<Node> nodeResolver = Computer.threadPoolForRemoting.submit(() -> { | ||
MultipassLauncher launcher = new MultipassLauncher(cloud); | ||
try { | ||
MultipassAgent agent = new MultipassAgent(cloud, displayName, launcher); | ||
jenkinsController().addNode(agent); | ||
return agent; | ||
} catch (Descriptor.FormException | IOException e) { | ||
LOGGER.error("[multipass-cloud] Exception when initializing new Multipass agent: %s", e); | ||
return null; | ||
} | ||
}); | ||
nodeList.add(new NodeProvisioner.PlannedNode(displayName, nodeResolver, 1)); | ||
} | ||
|
||
lastProvisionTime = System.currentTimeMillis(); | ||
|
||
return nodeList; | ||
} | ||
|
||
/** | ||
* Find the number of {@link MultipassAgent} instances still connecting | ||
* to Jenkins controller | ||
*/ | ||
private long getCurrentlyProvisionedAgentCount() { | ||
return jenkinsController().getNodes() | ||
.stream() | ||
.filter(MultipassAgent.class::isInstance) | ||
.map(MultipassAgent.class::cast) | ||
.filter(a -> a.getLauncher().isLaunchSupported()) | ||
.count(); | ||
} | ||
|
||
/** | ||
* | ||
* @return int | ||
*/ | ||
public String getName() { | ||
return this.name; | ||
} | ||
|
||
public String getCloudInitConfig() { | ||
return this.cloudConfig; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setCloudInitConfig(String cloudConfig) { | ||
this.cloudConfig = cloudConfig; | ||
} | ||
|
||
public Integer getCPUs() { | ||
return this.cpus; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setCpus(Integer cpus) { | ||
this.cpus = cpus; | ||
} | ||
|
||
public String getMemory() { | ||
return this.memory; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setMemory(String memory) { | ||
this.memory = memory; | ||
} | ||
|
||
public String getDisk() { | ||
return this.disk; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setDisk(String disk) { | ||
this.disk = disk; | ||
} | ||
|
||
public String getDistroAlias() { | ||
return Objects.isNull(distroAlias) ? DEFAULT_AGENT_DISTRIBUTION_ALIAS : distroAlias; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setDistroAlias(String distroAlias) { | ||
this.distroAlias = distroAlias; | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
src/main/java/io/hainenber/jenkins/multipass/MultipassComputer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package io.hainenber.jenkins.multipass; | ||
|
||
import hudson.model.Computer; | ||
import hudson.model.Executor; | ||
import hudson.model.Queue; | ||
import hudson.slaves.AbstractCloudComputer; | ||
import jakarta.annotation.Nonnull; | ||
import org.apache.commons.lang.time.DurationFormatUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.Objects; | ||
import java.util.StringJoiner; | ||
import java.util.concurrent.Future; | ||
|
||
public class MultipassComputer extends AbstractCloudComputer<MultipassAgent> { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(MultipassComputer.class); | ||
|
||
@Nonnull | ||
private final MultipassCloud cloud; | ||
|
||
/** | ||
* Constructor for MultipassComputer | ||
* @param multipassAgent a {@link MultipassAgent} object. | ||
*/ | ||
public MultipassComputer(MultipassAgent multipassAgent) { | ||
super(multipassAgent); | ||
this.cloud = multipassAgent.getCloud(); | ||
} | ||
|
||
@Override | ||
public void taskAccepted(Executor executor, Queue.Task task) { | ||
super.taskAccepted(executor, task); | ||
LOGGER.info("[multipass-cloud] [{}]: Task in job '{}' accepted", this, task.getFullDisplayName()); | ||
} | ||
|
||
@Override | ||
public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { | ||
super.taskCompleted(executor, task, durationMS); | ||
LOGGER.info("[multipass-cloud] [{}]: Task in job '{}' completed in {}", this, task.getFullDisplayName(), DurationFormatUtils.formatDurationWords(durationMS, true, true)); | ||
gracefulShutdown(); | ||
} | ||
|
||
@Override | ||
public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) { | ||
super.taskCompletedWithProblems(executor, task, durationMS, problems); | ||
LOGGER.info("[multipass-cloud] [{}]: Task in job '{}' completed with problems in {}", this, task.getFullDisplayName(), DurationFormatUtils.formatDurationWords(durationMS, true, true)); | ||
gracefulShutdown(); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return new StringJoiner(", ", MultipassComputer.class.getSimpleName() + "[", "]") | ||
.add("cloud=" + cloud) | ||
.toString(); | ||
} | ||
|
||
private void gracefulShutdown() { | ||
// Mark the computer to no longer accept new tasks; | ||
setAcceptingTasks(false); | ||
|
||
Future<Object> next = Computer.threadPoolForRemoting.submit(() -> { | ||
LOGGER.info("[multipass-cloud] [{}]: Terminating agent after task.", this); | ||
try { | ||
Thread.sleep(500); | ||
MultipassCloud.jenkinsController().removeNode(Objects.requireNonNull(getNode())); | ||
} catch (Exception e) { | ||
LOGGER.info("[multipass-cloud] [{}]: Error encounter when trying to terminate agent: {}", this, e.getClass()); | ||
} | ||
return null; | ||
}); | ||
|
||
next.notify(); | ||
} | ||
} |
Oops, something went wrong.