diff --git a/pom.xml b/pom.xml index 0c2633de..d2472516 100644 --- a/pom.xml +++ b/pom.xml @@ -43,13 +43,13 @@ - 1.625.3 + 2.19.4 UTF-8 2.5.3 3.0.3 3.3 2.10.3 - 1.119 + 1.102 1.8 8 diff --git a/yet-another-docker-its/pom.xml b/yet-another-docker-its/pom.xml index 46a1efc5..e41a3350 100644 --- a/yet-another-docker-its/pom.xml +++ b/yet-another-docker-its/pom.xml @@ -247,7 +247,7 @@ org.jenkins-ci.plugins cloud-stats - 0.3 + 0.7 org.jenkins-ci.plugins diff --git a/yet-another-docker-its/src/main/java/com/github/kostyasha/it/rule/DockerRule.java b/yet-another-docker-its/src/main/java/com/github/kostyasha/it/rule/DockerRule.java index e6bf66cf..5df5ec17 100644 --- a/yet-another-docker-its/src/main/java/com/github/kostyasha/it/rule/DockerRule.java +++ b/yet-another-docker-its/src/main/java/com/github/kostyasha/it/rule/DockerRule.java @@ -174,7 +174,9 @@ private void prepareDockerCli() { clientConfig = createDefaultConfigBuilder() .build(); - dockerCmdExecFactory = new JerseyDockerCmdExecFactory().withConnectTimeout(10 * 1000); + dockerCmdExecFactory = new JerseyDockerCmdExecFactory() + .withConnectTimeout(10 * 1000) + .withReadTimeout(10 * 1000); dockerClient = DockerClientBuilder.getInstance(clientConfig) .withDockerCmdExecFactory(dockerCmdExecFactory) diff --git a/yet-another-docker-plugin/pom.xml b/yet-another-docker-plugin/pom.xml index ddec7ec5..c62e9015 100644 --- a/yet-another-docker-plugin/pom.xml +++ b/yet-another-docker-plugin/pom.xml @@ -31,7 +31,7 @@ org.jenkins-ci.plugins cloud-stats - 0.5 + 0.7 org.jenkins-ci.plugins diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerCloud.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerCloud.java index 2aef31ee..2fa260f3 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerCloud.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerCloud.java @@ -2,6 +2,7 @@ import com.github.kostyasha.yad.commons.AbstractCloud; import com.github.kostyasha.yad.commons.DockerCreateContainer; +import com.github.kostyasha.yad.launcher.DockerComputerLauncher; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.CreateContainerCmd; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.CreateContainerResponse; @@ -113,6 +114,7 @@ public synchronized Collection provision(@CheckForNull Label label, LOG.warn("Bad template '{}' in cloud '{}': '{}'. Trying next template...", t.getDockerContainerLifecycle().getImage(), getDisplayName(), e.getMessage(), e); tryTemplates.remove(t); + continue; } @@ -200,6 +202,7 @@ private DockerSlave provisionWithWait(DockerSlaveTemplate template, Provisioning throws IOException, Descriptor.FormException { final DockerContainerLifecycle dockerContainerLifecycle = template.getDockerContainerLifecycle(); final String imageId = dockerContainerLifecycle.getImage(); + final DockerComputerLauncher computerLauncher = template.getLauncher(); //pull image dockerContainerLifecycle.getPullImage().exec(getClient(), imageId); @@ -227,13 +230,13 @@ private DockerSlave provisionWithWait(DockerSlaveTemplate template, Provisioning String slaveName = String.format("%s-%s", getDisplayName(), containerId.substring(0, 12)); - if (template.getLauncher().waitUp(getDisplayName(), template, ir)) { + if (computerLauncher.waitUp(getDisplayName(), template, ir)) { LOG.debug("Container {} is ready for ssh slave connection", containerId); } else { LOG.error("Container {} is not ready for ssh slave connection.", containerId); } - final ComputerLauncher launcher = template.getLauncher().getPreparedLauncher(getDisplayName(), template, ir); + final ComputerLauncher launcher = computerLauncher.getPreparedLauncher(getDisplayName(), template, ir); return new DockerSlave(slaveName, nodeDescription, launcher, containerId, template, getDisplayName(), id); } diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerComputerSingle.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerComputerSingle.java new file mode 100644 index 00000000..64646828 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerComputerSingle.java @@ -0,0 +1,80 @@ +package com.github.kostyasha.yad; + +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.remoting.Channel; +import hudson.slaves.AbstractCloudComputer; +import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; +import org.jenkinsci.plugins.cloudstats.TrackedItem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import static java.util.Objects.nonNull; + +/** + * @author Kanstantsin Shautsou + */ +public class DockerComputerSingle extends AbstractCloudComputer implements TrackedItem { + private final ProvisioningActivity.Id activityId; + private transient TaskListener listener; + private transient Run run; + + public DockerComputerSingle(@Nonnull DockerSlaveSingle slave, @Nonnull ProvisioningActivity.Id activityId) { + super(slave); + this.activityId = activityId; + } + + @Override + public TaskListener getListener() { + return nonNull(listener) ? listener : super.getListener(); + } + + public void setListener(TaskListener listener) { + this.listener = listener; + } + + public Run getRun() { + return run; + } + + public void setRun(Run run) { + this.run = run; + } + + @Override + public void setChannel(Channel channel, OutputStream launchLog, Channel.Listener listener) + throws IOException, InterruptedException { + super.setChannel(channel, launchLog, listener); + } + + public boolean isReallyOffline() { + return super.isOffline(); + } + + @Override + public boolean isOffline() { + // create executors to pick tasks + return false; + } + + @Override + public Charset getDefaultCharset() { + // either fails + // java.lang.NullPointerException + // at hudson.model.Run.execute(Run.java:1702) + // at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) + // at hudson.model.ResourceController.execute(ResourceController.java:98) + // at hudson.model.Executor.run(Executor.java:404) + return Charset.forName("UTF-8"); + } + + @Nullable + @Override + public ProvisioningActivity.Id getId() { + return activityId; + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerConnector.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerConnector.java index d31fdb2e..c383ff70 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerConnector.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerConnector.java @@ -3,6 +3,9 @@ import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.github.kostyasha.yad.connector.YADockerConnector; +import com.github.kostyasha.yad.other.ConnectorType; +import com.github.kostyasha.yad.utils.CredentialsListBoxModel; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.exception.DockerException; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.model.Version; @@ -10,18 +13,14 @@ import com.github.kostyasha.yad_docker_java.com.github.dockerjava.core.RemoteApiVersion; import com.github.kostyasha.yad_docker_java.com.google.common.base.Preconditions; import com.github.kostyasha.yad_docker_java.org.apache.commons.lang.StringUtils; -import com.github.kostyasha.yad.other.ConnectorType; -import com.github.kostyasha.yad.utils.CredentialsListBoxModel; +import com.github.kostyasha.yad_docker_java.org.glassfish.jersey.client.ClientProperties; import com.google.common.base.Throwables; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; -import hudson.model.Describable; -import hudson.model.Descriptor; import hudson.model.ItemGroup; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.kohsuke.stapler.AncestorInPath; @@ -30,6 +29,7 @@ import org.kohsuke.stapler.QueryParameter; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; import java.security.GeneralSecurityException; @@ -37,19 +37,19 @@ import java.util.List; import static com.github.kostyasha.yad.client.ClientBuilderForConnector.newClientBuilderForConnector; -import static com.github.kostyasha.yad_docker_java.com.github.dockerjava.core.RemoteApiVersion.parseConfig; import static com.github.kostyasha.yad.other.ConnectorType.NETTY; +import static com.github.kostyasha.yad_docker_java.com.github.dockerjava.core.RemoteApiVersion.parseConfig; import static hudson.util.FormValidation.ok; import static hudson.util.FormValidation.warning; import static org.apache.commons.lang.builder.ToStringBuilder.reflectionToString; import static org.apache.commons.lang.builder.ToStringStyle.MULTI_LINE_STYLE; /** - * Settings for connecting to docker. + * Settings for connecting to docker via docker-java configured connection. * * @author Kanstantsin Shautsou */ -public class DockerConnector implements Describable { +public class DockerConnector extends YADockerConnector { @CheckForNull private String serverUrl; @@ -70,6 +70,9 @@ public class DockerConnector implements Describable { @CheckForNull private Integer connectTimeout; + @CheckForNull + private Integer readTimeout; + @DataBoundConstructor public DockerConnector(String serverUrl) { setServerUrl(serverUrl); @@ -125,6 +128,18 @@ public void setConnectTimeout(Integer connectTimeout) { this.connectTimeout = connectTimeout; } + @CheckForNull + public Integer getReadTimeout() { + return readTimeout; + } + + /** + * @see ClientProperties#READ_TIMEOUT + */ + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + public DockerClient getClient() { if (client == null) { try { @@ -187,14 +202,8 @@ public int hashCode() { .toHashCode(); } - @Override - public Descriptor getDescriptor() { - return (DescriptorImpl) Jenkins.getActiveInstance().getDescriptor(DockerConnector.class); - } - - - @Extension - public static class DescriptorImpl extends Descriptor { + @Extension(ordinal = 100) + public static class DescriptorImpl extends YADockerConnectorDescriptor { public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) { List credentials = @@ -222,7 +231,7 @@ public FormValidation doTestConnection( final DockerClient testClient = newClientBuilderForConnector() .withConfigBuilder(configBuilder) .withConnectorType(connectorType) - .withCredentials(credentialsId) + .withCredentialsId(credentialsId) .withConnectTimeout(connectTimeout) .build(); @@ -253,9 +262,10 @@ public FormValidation doCheckApiVersion(@QueryParameter String apiVersion) { return ok(); } + @Nonnull @Override public String getDisplayName() { - return "Docker Connector"; + return "Direct Docker Connector"; } } } diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerOfflineCause.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerOfflineCause.java index 5fa13188..931913be 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerOfflineCause.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerOfflineCause.java @@ -6,8 +6,14 @@ * @author Kanstantsin Shautsou */ public class DockerOfflineCause extends OfflineCause { + private String message; + + public DockerOfflineCause(String message) { + this.message = message; + } + @Override public String toString() { - return "Shutting down Docker"; + return message; } } diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSimpleBuildWrapper.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSimpleBuildWrapper.java new file mode 100644 index 00000000..6f6232d4 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSimpleBuildWrapper.java @@ -0,0 +1,164 @@ +package com.github.kostyasha.yad; + +import hudson.AbortException; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractProject; +import hudson.model.Computer; +import hudson.model.Descriptor; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.slaves.DelegatingComputerLauncher; +import hudson.tasks.BuildWrapperDescriptor; +import jenkins.model.Jenkins; +import jenkins.tasks.SimpleBuildWrapper; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.cloudstats.CloudStatistics; +import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; +import org.kohsuke.stapler.DataBoundConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.IOException; + +import static java.util.Objects.isNull; +import static org.jenkinsci.plugins.cloudstats.CloudStatistics.ProvisioningListener.get; + +/** + * Wrapper that starts node and allows body execute anything in created {@link #getSlaveName()} slave. + * Body may assign tasks by {@link hudson.model.Label} using Actions or do anything directly on it. + * By defauld {@link com.github.kostyasha.yad.strategy.DockerOnceRetentionStrategy} is used and terminates + * slave after first execution, set other or override it with custom logic. + * + * @author Kanstantsin Shautsou + */ +public class DockerSimpleBuildWrapper extends SimpleBuildWrapper { + private static final Logger LOG = LoggerFactory.getLogger(DockerSimpleBuildWrapper.class); + + private DockerConnector connector; + private DockerSlaveConfig config; + private String slaveName; + + @DataBoundConstructor + public DockerSimpleBuildWrapper(@Nonnull DockerConnector connector, @Nonnull DockerSlaveConfig config) { + this.connector = connector; + this.config = config; + } + + public DockerConnector getConnector() { + return connector; + } + + public DockerSlaveConfig getConfig() { + return config; + } + + @CheckForNull + public String getSlaveName() { + return slaveName; + } + + protected void setSlaveName(String slaveName) { + this.slaveName = slaveName; + } + + @Override + public void setUp(Context context, + Run run, + FilePath workspace, + Launcher launcher, + TaskListener listener, + EnvVars initialEnvironment) throws IOException, InterruptedException { + + final ProvisioningActivity.Id activityId = new ProvisioningActivity.Id( + run.getDisplayName(), + getConfig().getDockerContainerLifecycle().getImage() + ); + + try { + get().onStarted(activityId); + final String futureName = "yadp" + Integer.toString(activityId.getFingerprint()); + + final DockerSlaveSingle slave = new DockerSlaveSingle(futureName, + "Slave for " + run.getFullDisplayName(), + getConfig(), + getConnector(), + activityId + ); + + Jenkins.getInstance().addNode(slave); + final Node futureNode = Jenkins.getInstance().getNode(futureName); + if (isNull(futureNode)) { + throw new IllegalStateException("Can't get Node " + futureName); + } + final DockerSlaveSingle node = (DockerSlaveSingle) futureNode; + try { + final Computer toComputer = node.toComputer(); + if (isNull(toComputer)) { + throw new IllegalStateException("Can't get computer for " + node.getNodeName()); + } + final DockerComputerSingle computer = (DockerComputerSingle) toComputer; + computer.setRun(run); + computer.setListener(listener); + node.setListener(listener); + listener.getLogger().println("Getting launcher..."); + ((DelegatingComputerLauncher) computer.getLauncher()).getLauncher().launch(computer, listener); + } catch (Throwable e) { + LOG.error("fd", e); + CloudStatistics.ProvisioningListener.get().onFailure(node.getId(), e); + } + setSlaveName(futureName); + context.setDisposer(new DisposerImpl(futureName)); + + } catch (Descriptor.FormException | IOException e) { + get().onFailure(activityId, e); + throw new AbortException("failed to run slave"); + } + + } + + /** + * Terminates slave for specified slaveName. Works only with {@link DockerSlaveSingle}. + */ + public static class DisposerImpl extends Disposer { + private static final long serialVersionUID = 1; + private final String slaveName; + + public DisposerImpl(@Nonnull final String slaveName) { + this.slaveName = slaveName; + } + + @Override + public void tearDown(Run run, FilePath workspace, Launcher launcher, TaskListener listener) + throws IOException, InterruptedException { + LOG.info("Shutting down slave"); + listener.getLogger().println("Shutting down slave '" + slaveName + "'."); + final Node slaveNode = Jenkins.getInstance().getNode(slaveName); + if (isNull(slaveNode)) { + throw new IllegalStateException("Can't get node " + slaveName); + } + final DockerSlaveSingle node = (DockerSlaveSingle) slaveNode; + try { + node.terminate(); + } catch (Throwable e) { + LOG.error("Can't terminate node", e); + CloudStatistics.ProvisioningListener.get().onFailure(node.getId(), e); + } + + } + } + + @Symbol("yadockerWrapper") + @Extension + public static final class DescriptorImpl extends BuildWrapperDescriptor { + @Override + public boolean isApplicable(AbstractProject item) { + return true; + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlave.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlave.java index 2d26b8b7..767c2435 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlave.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlave.java @@ -1,6 +1,7 @@ package com.github.kostyasha.yad; import com.github.kostyasha.yad.action.DockerTerminateCmdAction; +import com.github.kostyasha.yad.queue.FlyweightCauseOfBlockage; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.exception.NotModifiedException; import com.github.kostyasha.yad_docker_java.com.google.common.base.MoreObjects; @@ -8,6 +9,7 @@ import hudson.Extension; import hudson.model.Computer; import hudson.model.Descriptor; +import hudson.model.Node; import hudson.model.Queue; import hudson.model.TaskListener; import hudson.model.queue.CauseOfBlockage; @@ -15,9 +17,12 @@ import hudson.slaves.Cloud; import hudson.slaves.ComputerLauncher; import jenkins.model.Jenkins; +import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.cloudstats.CloudStatistics; import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; import org.jenkinsci.plugins.cloudstats.TrackedItem; +import org.kohsuke.stapler.StaplerRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,9 +31,10 @@ import javax.annotation.Nullable; import java.io.IOException; +import static java.util.Objects.nonNull; /** - * Jenkins Slave with yad specific configuration + * Jenkins Slave with yad specific configuration. */ @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Broken serialization https://issues.jenkins-ci.org/browse/JENKINS-31916") @@ -95,7 +101,7 @@ public void setDockerSlaveTemplate(DockerSlaveTemplate dockerSlaveTemplate) { @Nonnull public DockerCloud getCloud() { - final Cloud cloud = Jenkins.getActiveInstance().getCloud(getCloudId()); + final Cloud cloud = Jenkins.getInstance().getCloud(getCloudId()); if (cloud == null) { throw new RuntimeException("Docker template " + dockerSlaveTemplate + " has no assigned Cloud."); @@ -137,6 +143,11 @@ public boolean containerExistsInCloud() { } } + @Override + public Node reconfigure(StaplerRequest req, JSONObject form) throws Descriptor.FormException { + return null; + } + @Override protected void _terminate(TaskListener listener) throws IOException, InterruptedException { final DockerContainerLifecycle dockerContainerLifecycle = dockerSlaveTemplate.getDockerContainerLifecycle(); @@ -144,10 +155,11 @@ protected void _terminate(TaskListener listener) throws IOException, Interrupted LOG.info("Requesting disconnect for computer: '{}'", name); final Computer toComputer = toComputer(); if (toComputer != null) { - toComputer.disconnect(new DockerOfflineCause()); + toComputer.disconnect(new DockerOfflineCause("Terminating from _terminate.")); } } catch (Exception e) { LOG.error("Can't disconnect computer: '{}'", name, e); + listener.error("Can't disconnect computer: " + name); } if (StringUtils.isNotBlank(containerId)) { @@ -181,9 +193,11 @@ protected void _terminate(TaskListener listener) throws IOException, Interrupted } else { LOG.error("ContainerId is absent, no way to remove/stop container"); } - // after it node will be finally removed from jenkins -// CloudStatistics.ProvisioningListener.get().onComplete() no completion method?! - // https://issues.jenkins-ci.org/browse/JENKINS-33780 + + ProvisioningActivity activity = CloudStatistics.get().getActivityFor(this); + if (nonNull(activity)) { + activity.enterIfNotAlready(ProvisioningActivity.Phase.COMPLETED); + } } @Nullable diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveConfig.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveConfig.java new file mode 100644 index 00000000..68b753ca --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveConfig.java @@ -0,0 +1,216 @@ +package com.github.kostyasha.yad; + +import com.github.kostyasha.yad.launcher.DockerComputerJNLPLauncher; +import com.github.kostyasha.yad.strategy.DockerOnceRetentionStrategy; +import com.github.kostyasha.yad_docker_java.com.google.common.base.Strings; +import hudson.Extension; +import hudson.Util; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.model.Node; +import hudson.slaves.ComputerLauncher; +import hudson.slaves.NodeProperty; +import hudson.slaves.NodePropertyDescriptor; +import hudson.slaves.RetentionStrategy; +import hudson.util.DescribableList; +import hudson.util.FormValidation; +import jenkins.model.Jenkins; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +import static java.util.UUID.randomUUID; + +/** + * Some generic config with everything required for container-slave operaition. + * Without docker connector because one connector may have multiple configs i.e. {@link DockerCloud} + * + * @author Kanstantsin Shautsou + */ +public class DockerSlaveConfig extends AbstractDescribableImpl { + /** + * Unique id of this template configuration. Required for: + * - hashcode, + * - cloud counting + */ + @Nonnull + protected final String id; + + private String labelString = "docker"; + + protected ComputerLauncher launcher = new DockerComputerJNLPLauncher(); + + private String remoteFs = "/home/jenkins"; + + protected Node.Mode mode = Node.Mode.EXCLUSIVE; + + protected RetentionStrategy retentionStrategy = new DockerOnceRetentionStrategy(10); + + protected int numExecutors = 1; + + /** + * Bundle class that contains all docker related actions/configs + */ + protected DockerContainerLifecycle dockerContainerLifecycle = new DockerContainerLifecycle(); + + private List> nodeProperties = emptyList(); + + public DockerSlaveConfig() { + this.id = randomUUID().toString(); + } + + /** + * @param id some unique id to identify this configuration. Use case - count running computers based on this config. + */ + public DockerSlaveConfig(@Nonnull String id) { + this.id = id; + } + + public DockerContainerLifecycle getDockerContainerLifecycle() { + return dockerContainerLifecycle; + } + + @DataBoundSetter + public void setDockerContainerLifecycle(DockerContainerLifecycle dockerContainerLifecycle) { + this.dockerContainerLifecycle = dockerContainerLifecycle; + } + + public String getLabelString() { + return labelString; + } + + @DataBoundSetter + public void setLabelString(String labelString) { + this.labelString = Util.fixNull(labelString); + } + + @DataBoundSetter + public void setMode(Node.Mode mode) { + this.mode = mode; + } + + public Node.Mode getMode() { + return mode; + } + + /** + * Experimental option allows set number of executors + */ + @DataBoundSetter + public void setNumExecutors(int numExecutors) { + this.numExecutors = numExecutors; + } + + public int getNumExecutors() { + return numExecutors; + } + + @DataBoundSetter + public void setRetentionStrategy(RetentionStrategy retentionStrategy) { + this.retentionStrategy = retentionStrategy; + } + + public RetentionStrategy getRetentionStrategy() { + return retentionStrategy; + } + + @DataBoundSetter + public void setLauncher(ComputerLauncher launcher) { + this.launcher = launcher; + } + + public ComputerLauncher getLauncher() { + return launcher; + } + + @Nonnull + public String getRemoteFs() { + return Strings.isNullOrEmpty(remoteFs) ? "/home/jenkins" : remoteFs; + } + + @DataBoundSetter + public void setRemoteFs(String remoteFs) { + this.remoteFs = remoteFs; + } + + @Nonnull + @Restricted(value = NoExternalUse.class) // ancient UI jelly form + public DescribableList, NodePropertyDescriptor> getNodePropertiesUI() throws IOException { + return new DescribableList<>(Jenkins.getActiveInstance().getNodesObject(), getNodeProperties()); + } + + @Restricted(value = NoExternalUse.class) // ancient UI jelly form + public void setNodePropertiesUI(DescribableList, NodePropertyDescriptor> nodePropertiesUI) { + setNodeProperties(nodePropertiesUI); + } + + @Nonnull + public List> getNodeProperties() { + return nonNull(nodeProperties) ? unmodifiableList(nodeProperties) : emptyList(); + } + + public void setNodeProperties(List> nodeProperties) { + this.nodeProperties = nodeProperties; + } + + /** + * Id used for counting running slaves + */ + @Nonnull + public String getId() { + return id; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof DockerSlaveConfig)) return false; + + DockerSlaveConfig that = (DockerSlaveConfig) o; + + return new EqualsBuilder() + .append(numExecutors, that.numExecutors) + .append(id, that.id) + .append(labelString, that.labelString) + .append(launcher, that.launcher) + .append(remoteFs, that.remoteFs) + .append(mode, that.mode) + .append(retentionStrategy, that.retentionStrategy) + .append(dockerContainerLifecycle, that.dockerContainerLifecycle) +// .append(nodeProperties, that.nodeProperties) + .isEquals(); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + public FormValidation doCheckLabelString(@QueryParameter String labelString) { + if (isNull(labelString)) { + return FormValidation.warning("Please specify some label"); + } + + return FormValidation.ok(); + } + + @Nonnull + @Override + public String getDisplayName() { + return "Docker Slave Configuration"; + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveSingle.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveSingle.java new file mode 100644 index 00000000..ea9dd6bc --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveSingle.java @@ -0,0 +1,179 @@ +package com.github.kostyasha.yad; + +import com.github.kostyasha.yad.action.DockerTerminateCmdAction; +import com.github.kostyasha.yad.connector.YADockerConnector; +import com.github.kostyasha.yad.launcher.DockerComputerSingleJNLPLauncher; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.exception.NotModifiedException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.model.Computer; +import hudson.model.Descriptor; +import hudson.model.TaskListener; +import hudson.slaves.AbstractCloudComputer; +import hudson.slaves.AbstractCloudSlave; +import hudson.slaves.DelegatingComputerLauncher; +import hudson.util.StreamTaskListener; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.cloudstats.CloudStatistics; +import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; +import org.jenkinsci.plugins.cloudstats.TrackedItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.charset.Charset; + +import static java.util.Objects.nonNull; + +/** + * Alternative to {@link DockerSlave}. + * + * @author Kanstantsin Shautsou + */ +@SuppressFBWarnings(value = "SE_BAD_FIELD", + justification = "Broken serialization https://issues.jenkins-ci.org/browse/JENKINS-31916") +public class DockerSlaveSingle extends AbstractCloudSlave implements TrackedItem { + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(DockerSlaveSingle.class); + + private final YADockerConnector connector; + private final ProvisioningActivity.Id activityId; + private final DockerSlaveConfig config; + + private transient TaskListener listener = null; + + public DockerSlaveSingle(@Nonnull String name, + @Nonnull String nodeDescription, + @Nonnull DockerSlaveConfig config, + @Nonnull YADockerConnector connector, + @Nonnull ProvisioningActivity.Id activityId) + throws IOException, Descriptor.FormException { + super(name, nodeDescription, + config.getRemoteFs(), config.getNumExecutors(), config.getMode(), + "", + config.getLauncher(), config.getRetentionStrategy(), config.getNodeProperties()); + this.connector = connector; + this.activityId = activityId; + this.config = config; + + } + + public YADockerConnector getConnector() { + return connector; + } + + public DockerSlaveConfig getConfig() { + return config; + } + + @Override + public DelegatingComputerLauncher getLauncher() { + return (DelegatingComputerLauncher) super.getLauncher(); + } + + private String getContainerId() { + return ((DockerComputerSingleJNLPLauncher) getLauncher().getLauncher()).getContainerId(); + } + + @Nonnull + public TaskListener getListener() { + return nonNull(listener) ? listener : new StreamTaskListener(System.out, Charset.forName("UTF-8")); + } + + /** + * Set listener that will be used for printing out messages instead default listener. + */ + public void setListener(TaskListener listener) { + this.listener = listener; + } + + @Override + public AbstractCloudComputer createComputer() { + return new DockerComputerSingle(this, activityId); + } + + + @Override + public void terminate() throws InterruptedException, IOException { + try { + _terminate(getListener()); + } finally { + try { + Jenkins.getInstance().removeNode(this); + } catch (IOException e) { + LOG.warn("Failed to remove {}", name, e); + getListener().error("Failed to remove " + name); + } + } + } + + @Override + protected void _terminate(TaskListener listener) throws IOException, InterruptedException { + final DockerContainerLifecycle dockerContainerLifecycle = config.getDockerContainerLifecycle(); + try { + LOG.info("Requesting disconnect for computer: '{}'", name); + final Computer toComputer = toComputer(); + if (toComputer != null) { + toComputer.disconnect(new DockerOfflineCause("Terminating from _terminate.")); + } + } catch (Exception e) { + LOG.error("Can't disconnect computer: '{}'", name, e); + listener.error("Can't disconnect computer: " + name); + } + + if (StringUtils.isNotBlank(getContainerId())) { + try { + dockerContainerLifecycle.getStopContainer().exec(connector.getClient(), getContainerId()); + LOG.info("Stopped container {}", getContainerId()); + listener.getLogger().println("Stopped container " + getContainerId()); + } catch (NotModifiedException ex) { + LOG.info("Container '{}' is already stopped.", getContainerId()); + } catch (Exception ex) { + LOG.error("Failed to stop instance '{}' for slave '{}' due to exception: {}", + getContainerId(), name, ex.getMessage()); + } + + final Computer computer = toComputer(); + if (computer instanceof DockerComputerSingle) { + final DockerComputerSingle dockerComputer = (DockerComputerSingle) computer; + for (DockerTerminateCmdAction a : dockerComputer.getActions(DockerTerminateCmdAction.class)) { + try { + a.exec(connector.getClient(), getContainerId()); + } catch (Exception e) { + LOG.error("Failed execute action {}", a, e); + listener.error("Failed execute " + a.getDisplayName()); + } + } + } else { + LOG.error("Computer '{}' is not DockerComputerSingle", computer); + listener.error("Computer ' " + computer + "' is not DockerComputerSingle", computer); + } + + try { + dockerContainerLifecycle.getRemoveContainer().exec(connector.getClient(), getContainerId()); + LOG.info("Removed container {}", getContainerId()); + listener.getLogger().println("Removed container " + getContainerId()); + } catch (Exception ex) { + LOG.error("Failed to remove instance '{}' for slave '{}' due to exception: {}", + getContainerId(), name, ex.getMessage()); + listener.error("failed to remove " + getContainerId()); + } + } else { + LOG.error("ContainerId is absent, no way to remove/stop container"); + listener.error("ContainerId is absent, no way to remove/stop container"); + } + + ProvisioningActivity activity = CloudStatistics.get().getActivityFor(this); + if (nonNull(activity)) { + activity.enterIfNotAlready(ProvisioningActivity.Phase.COMPLETED); + } + } + + @Nonnull + @Override + public ProvisioningActivity.Id getId() { + return activityId; + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveTemplate.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveTemplate.java index 53106517..b83c3f22 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveTemplate.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/DockerSlaveTemplate.java @@ -1,30 +1,20 @@ package com.github.kostyasha.yad; import com.github.kostyasha.yad.commons.DockerCreateContainer; -import com.github.kostyasha.yad_docker_java.com.google.common.base.MoreObjects; -import com.github.kostyasha.yad_docker_java.com.google.common.base.Strings; -import com.github.kostyasha.yad.launcher.DockerComputerJNLPLauncher; import com.github.kostyasha.yad.launcher.DockerComputerLauncher; import com.github.kostyasha.yad.strategy.DockerOnceRetentionStrategy; +import com.github.kostyasha.yad_docker_java.com.google.common.base.MoreObjects; import hudson.Extension; -import hudson.Util; -import hudson.model.Describable; -import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; import hudson.model.Label; import hudson.model.Node; import hudson.model.labels.LabelAtom; import hudson.slaves.NodeProperty; -import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.RetentionStrategy; -import hudson.util.DescribableList; import hudson.util.FormValidation; -import jenkins.model.Jenkins; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; @@ -32,71 +22,37 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.UUID; -import static java.util.Collections.emptyList; -import static java.util.Collections.unmodifiableList; import static java.util.Objects.isNull; -import static java.util.Objects.nonNull; /** * All configuration (jenkins and docker specific) required for launching slave instances. */ -public class DockerSlaveTemplate implements Describable { +public class DockerSlaveTemplate extends DockerSlaveConfig { private static final Logger LOG = LoggerFactory.getLogger(DockerSlaveTemplate.class); - /** - * Unique id of this template configuration. Required for: - * - hashcode, - * - cloud counting - */ - @Nonnull - private final String id; - - private String labelString = "docker"; - - private transient String remoteFsMapping; - - private DockerComputerLauncher launcher = new DockerComputerJNLPLauncher(); - - private String remoteFs = "/home/jenkins"; - private int maxCapacity = 10; - private Node.Mode mode = Node.Mode.EXCLUSIVE; - - private RetentionStrategy retentionStrategy = new DockerOnceRetentionStrategy(10); - - private int numExecutors = 1; - - /** - * Bundle class that contains all docker related actions/configs - */ - private DockerContainerLifecycle dockerContainerLifecycle = new DockerContainerLifecycle(); - - private List> nodeProperties = null; - private transient /*almost final*/ Set labelSet; /** * Generates new unique ID for new instances. */ public DockerSlaveTemplate() { - this.id = UUID.randomUUID().toString(); + super(); } /** * Custom specified ID. When editing existed UI entry, UI sends it back. */ public DockerSlaveTemplate(@Nonnull String id) throws FormException { - if (id == null) { + super(id); + if (isNull(id)) { throw new FormException("Hidden id must not be null", "id"); } - this.id = id; } /** @@ -109,42 +65,13 @@ public DockerSlaveTemplate(@Nonnull String id, List> n setNodeProperties(nodePropertiesUI); } - public DockerContainerLifecycle getDockerContainerLifecycle() { - return dockerContainerLifecycle; - } - - @DataBoundSetter - public void setDockerContainerLifecycle(DockerContainerLifecycle dockerContainerLifecycle) { - this.dockerContainerLifecycle = dockerContainerLifecycle; - } - - public String getLabelString() { - return labelString; - } @DataBoundSetter public void setLabelString(String labelString) { - this.labelString = Util.fixNull(labelString); + super.setLabelString(labelString); this.labelSet = Label.parse(labelString); } - @DataBoundSetter - public void setMode(Node.Mode mode) { - this.mode = mode; - } - - public Node.Mode getMode() { - return mode; - } - - /** - * Experimental option allows set number of executors - */ - @DataBoundSetter - public void setNumExecutors(int numExecutors) { - this.numExecutors = numExecutors; - } - public int getNumExecutors() { if (getRetentionStrategy() instanceof DockerOnceRetentionStrategy) { return 1; // works only with one executor! @@ -153,15 +80,6 @@ public int getNumExecutors() { return numExecutors; } - @DataBoundSetter - public void setRetentionStrategy(RetentionStrategy retentionStrategy) { - this.retentionStrategy = retentionStrategy; - } - - public RetentionStrategy getRetentionStrategy() { - return retentionStrategy; - } - /** * tmp fix for terminating boolean caching */ @@ -173,25 +91,6 @@ public RetentionStrategy getRetentionStrategyCopy() { return retentionStrategy; } - @DataBoundSetter - public void setLauncher(DockerComputerLauncher launcher) { - this.launcher = launcher; - } - - public DockerComputerLauncher getLauncher() { - return launcher; - } - - @Nonnull - public String getRemoteFs() { - return Strings.isNullOrEmpty(remoteFs) ? "/home/jenkins" : remoteFs; - } - - @DataBoundSetter - public void setRemoteFs(String remoteFs) { - this.remoteFs = remoteFs; - } - public int getMaxCapacity() { return maxCapacity; } @@ -203,27 +102,12 @@ public void setMaxCapacity(int maxCapacity) { @Nonnull public Set getLabelSet() { - return labelSet != null ? labelSet : Collections.emptySet(); - } - - @Nonnull - @Restricted(value = NoExternalUse.class) // ancient UI jelly form - public DescribableList, NodePropertyDescriptor> getNodePropertiesUI() throws IOException { - return new DescribableList<>(Jenkins.getActiveInstance().getNodesObject(), getNodeProperties()); - } - - @Restricted(value = NoExternalUse.class) // ancient UI jelly form - public void setNodePropertiesUI(DescribableList, NodePropertyDescriptor> nodePropertiesUI) { - setNodeProperties(nodePropertiesUI); - } - - @Nonnull - public List> getNodeProperties() { - return nonNull(nodeProperties) ? unmodifiableList(nodeProperties) : emptyList(); + return labelSet != null ? labelSet : Collections.emptySet(); } - public void setNodeProperties(List> nodeProperties) { - this.nodeProperties = nodeProperties; + @Override + public DockerComputerLauncher getLauncher() { + return (DockerComputerLauncher) super.getLauncher(); } /** @@ -240,7 +124,7 @@ public Object readResolve() { } try { - labelSet = Label.parse(labelString); // fails sometimes under debugger + labelSet = Label.parse(getLabelString()); // fails sometimes under debugger } catch (Throwable t) { LOG.error("Can't parse labels: {}", t); } @@ -248,18 +132,6 @@ public Object readResolve() { return this; } - /** - * Id used for counting running slaves - */ - public String getId() { - return id; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -269,19 +141,16 @@ public boolean equals(Object o) { DockerSlaveTemplate that = (DockerSlaveTemplate) o; return new EqualsBuilder() + .appendSuper(true) .append(maxCapacity, that.maxCapacity) - .append(numExecutors, that.numExecutors) - .append(id, that.id) - .append(labelString, that.labelString) - .append(launcher, that.launcher) - .append(remoteFs, that.remoteFs) - .append(mode, that.mode) - .append(retentionStrategy, that.retentionStrategy) - .append(dockerContainerLifecycle, that.dockerContainerLifecycle) -// .append(nodeProperties, that.nodeProperties) .isEquals(); } + @Override + public int hashCode() { + return super.hashCode(); + } + @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); @@ -293,19 +162,13 @@ public String getShortDescription() { .toString(); } - public Descriptor getDescriptor() { - return (DescriptorImpl) Jenkins.getActiveInstance().getDescriptor(getClass()); + @Override + public DescriptorImpl getDescriptor() { + return (DescriptorImpl) super.getDescriptor(); } @Extension - public static final class DescriptorImpl extends Descriptor { - public FormValidation doCheckLabelString(@QueryParameter String labelString) { - if (isNull(labelString)) { - return FormValidation.warning("Please specify some label"); - } - - return FormValidation.ok(); - } + public static final class DescriptorImpl extends DockerSlaveConfig.DescriptorImpl { public FormValidation doCheckNumExecutors(@QueryParameter int numExecutors) { if (numExecutors > 1) { @@ -316,6 +179,7 @@ public FormValidation doCheckNumExecutors(@QueryParameter int numExecutors) { return FormValidation.ok(); } + @Nonnull @Override public String getDisplayName() { return "Docker Template"; diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/client/ClientBuilderForConnector.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/client/ClientBuilderForConnector.java index e9a402ef..c40fd912 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/client/ClientBuilderForConnector.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/client/ClientBuilderForConnector.java @@ -55,6 +55,7 @@ public class ClientBuilderForConnector { private ConnectorType connectorType = null; private Integer connectTimeout = null; + private Integer readTimeout = null; private ClientBuilderForConnector() { } @@ -85,7 +86,7 @@ public ClientBuilderForConnector withSslConfig(SSLConfig sslConfig) public ClientBuilderForConnector forConnector(DockerConnector connector) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { LOG.debug("Building connection to docker host '{}'", connector.getServerUrl()); - withCredentials(connector.getCredentialsId()); + withCredentialsId(connector.getCredentialsId()); withConnectorType(connector.getConnectorType()); withConnectTimeout(connector.getConnectTimeout()); @@ -102,6 +103,11 @@ public ClientBuilderForConnector withConnectTimeout(Integer connectTimeout) { return this; } + public ClientBuilderForConnector withReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + return this; + } + /** * Method to setup url and docker-api version. Convenient for test-connection purposes and quick requests * @@ -121,17 +127,25 @@ public ClientBuilderForConnector forServer(String uri, @Nullable String version) * @param credentialsId credentials to find in jenkins * @return docker-java client */ - public ClientBuilderForConnector withCredentials(String credentialsId) + public ClientBuilderForConnector withCredentialsId(String credentialsId) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { if (isNotBlank(credentialsId)) { - Credentials credentials = lookupSystemCredentials(credentialsId); - - if (credentials instanceof CertificateCredentials) { - CertificateCredentials certificateCredentials = (CertificateCredentials) credentials; - withSslConfig(new KeystoreSSLConfig( - certificateCredentials.getKeyStore(), - certificateCredentials.getPassword().getPlainText() - )); + withCredentials(lookupSystemCredentials(credentialsId)); + } else { + withSslConfig(null); + } + + return this; + } + + public ClientBuilderForConnector withCredentials(Credentials credentials) throws UnrecoverableKeyException, + NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + if (credentials instanceof CertificateCredentials) { + CertificateCredentials certificateCredentials = (CertificateCredentials) credentials; + withSslConfig(new KeystoreSSLConfig( + certificateCredentials.getKeyStore(), + certificateCredentials.getPassword().getPlainText() + )); // } else if (credentials instanceof StandardUsernamePasswordCredentials) { // StandardUsernamePasswordCredentials usernamePasswordCredentials = // ((StandardUsernamePasswordCredentials) credentials); @@ -139,17 +153,14 @@ public ClientBuilderForConnector withCredentials(String credentialsId) // dockerClientConfigBuilder.withRegistryUsername(usernamePasswordCredentials.getUsername()); // dockerClientConfigBuilder.withRegistryPassword(usernamePasswordCredentials.getPassword().getPlainText()); // - } else if (credentials instanceof DockerServerCredentials) { - final DockerServerCredentials dockerCreds = (DockerServerCredentials) credentials; - - withSslConfig(new VariableSSLConfig( - dockerCreds.getClientKey(), - dockerCreds.getClientCertificate(), - dockerCreds.getServerCaCertificate() - )); - } - } else { - withSslConfig(null); + } else if (credentials instanceof DockerServerCredentials) { + final DockerServerCredentials dockerCreds = (DockerServerCredentials) credentials; + + withSslConfig(new VariableSSLConfig( + dockerCreds.getClientKey(), + dockerCreds.getClientCertificate(), + dockerCreds.getServerCaCertificate() + )); } return this; @@ -184,10 +195,13 @@ public DockerClient build() throws UnrecoverableKeyException, NoSuchAlgorithmExc } if (dockerCmdExecFactory instanceof JerseyDockerCmdExecFactory) { + final JerseyDockerCmdExecFactory jersey = (JerseyDockerCmdExecFactory) dockerCmdExecFactory; if (nonNull(connectTimeout)) { - final JerseyDockerCmdExecFactory jersey = (JerseyDockerCmdExecFactory) dockerCmdExecFactory; dockerCmdExecFactory = jersey.withConnectTimeout(connectTimeout); } + if (nonNull(readTimeout)) { + jersey.withReadTimeout(readTimeout); + } } if (isNull(clientConfig)) { diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/commons/AbstractCloud.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/commons/AbstractCloud.java index 90bfdcf3..a9d117a1 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/commons/AbstractCloud.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/commons/AbstractCloud.java @@ -12,6 +12,9 @@ import java.util.HashMap; import java.util.List; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + /** * (Very) Pure abstraction to clean up docker specific implementation. * Normally it should be in {@link hudson.slaves.AbstractCloudImpl}, but it doesn't provide templates @@ -27,7 +30,7 @@ public abstract class AbstractCloud extends Cloud { protected final HashMap provisionedImages = new HashMap<>(); @Nonnull - protected List templates = Collections.emptyList(); + protected List templates = new ArrayList<>(0); /** * Total max allowed number of containers @@ -95,11 +98,11 @@ public List getTemplates(Label label) { List dockerSlaveTemplates = new ArrayList<>(); for (DockerSlaveTemplate t : templates) { - if (label == null && t.getMode() == Node.Mode.NORMAL) { + if (isNull(label) && t.getMode() == Node.Mode.NORMAL) { dockerSlaveTemplates.add(t); } - if (label != null && label.matches(t.getLabelSet())) { + if (nonNull(label) && label.matches(t.getLabelSet())) { dockerSlaveTemplates.add(t); } } diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/CredentialsYADockerConnector.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/CredentialsYADockerConnector.java new file mode 100644 index 00000000..549d5c49 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/CredentialsYADockerConnector.java @@ -0,0 +1,145 @@ +package com.github.kostyasha.yad.connector; + +import com.cloudbees.plugins.credentials.Credentials; +import com.github.kostyasha.yad.other.ConnectorType; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.core.DefaultDockerClientConfig; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +import static com.github.kostyasha.yad.client.ClientBuilderForConnector.newClientBuilderForConnector; +import static com.github.kostyasha.yad.other.ConnectorType.NETTY; +import static java.util.Objects.isNull; + +/** + * Connector from Credentials. + * + * @author Kanstantsin Shautsou + */ +public class CredentialsYADockerConnector extends YADockerConnector { + + @CheckForNull + private String serverUrl; + + @CheckForNull + private String apiVersion; + + private transient Boolean tlsVerify; + + @CheckForNull + private Credentials credentials = null; + + @CheckForNull + private transient DockerClient client = null; + + private ConnectorType connectorType = NETTY; + + @CheckForNull + private Integer connectTimeout; + + public CredentialsYADockerConnector() { + } + + @CheckForNull + public ConnectorType getConnectorType() { + return connectorType; + } + + public CredentialsYADockerConnector withConnectorType(ConnectorType connectorType) { + this.connectorType = connectorType; + return this; + } + + @CheckForNull + public String getServerUrl() { + return serverUrl; + } + + public CredentialsYADockerConnector withServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + @CheckForNull + public String getApiVersion() { + return apiVersion; + } + + public CredentialsYADockerConnector withApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + return this; + } + + @CheckForNull + public Boolean getTlsVerify() { + return tlsVerify; + } + + public CredentialsYADockerConnector withTlsVerify(Boolean tlsVerify) { + this.tlsVerify = tlsVerify; + return this; + } + + @CheckForNull + public Credentials getCredentials() { + return credentials; + } + + public CredentialsYADockerConnector withCredentials(Credentials credentials) { + this.credentials = credentials; + return this; + } + + public CredentialsYADockerConnector withClient(DockerClient client) { + this.client = client; + return this; + } + + @CheckForNull + public Integer getConnectTimeout() { + return connectTimeout; + } + + public CredentialsYADockerConnector withConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + @Nonnull + @Override + public DockerClient getClient() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, + KeyManagementException { + + if (isNull(client)) { + DefaultDockerClientConfig.Builder configBuilder = new DefaultDockerClientConfig.Builder() + .withApiVersion(apiVersion) + .withDockerHost(serverUrl); + + final DockerClient newClient = newClientBuilderForConnector() + .withConfigBuilder(configBuilder) + .withConnectorType(connectorType) + .withCredentials(credentials) + .withConnectTimeout(connectTimeout) + .build(); + + newClient.versionCmd().exec(); + client = newClient; + } + + return client; + } + + + public static class DescriptorImpl extends YADockerConnectorDescriptor { + @Nonnull + @Override + public String getDisplayName() { + return "Connector from Credentials"; + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/DockerCloudConnectorId.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/DockerCloudConnectorId.java new file mode 100644 index 00000000..338bcf27 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/DockerCloudConnectorId.java @@ -0,0 +1,91 @@ +package com.github.kostyasha.yad.connector; + +import com.github.kostyasha.yad.DockerCloud; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.model.Version; +import hudson.Extension; +import hudson.slaves.Cloud; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import static hudson.util.FormValidation.error; +import static hudson.util.FormValidation.ok; +import static org.apache.commons.lang.builder.ToStringBuilder.reflectionToString; +import static org.apache.commons.lang.builder.ToStringStyle.MULTI_LINE_STYLE; + +/** + * Should be under {@link com.github.kostyasha.yad.connector} package, + * but it was first class and can't move. + * Get {@link DockerClient} from existing {@link DockerCloud} + * + * @author Kanstantsin Shautsou + */ +public class DockerCloudConnectorId extends YADockerConnector { + + private String cloudId; + + @DataBoundConstructor + public DockerCloudConnectorId() { + } + + public String getCloudId() { + return cloudId; + } + + @DataBoundSetter + public void setCloudId(String cloudId) { + this.cloudId = cloudId; + } + + @CheckForNull + @Override + public DockerClient getClient() { + final Cloud cloud = Jenkins.getInstance().getCloud(cloudId); + if (cloud instanceof DockerCloud) { + final DockerCloud dockerCloud = (DockerCloud) cloud; + return dockerCloud.getClient(); + } + return null; + } + + @Extension + public static class DescriptorImpl extends YADockerConnectorDescriptor { + public FormValidation doCheckCloudId(@QueryParameter String cloudId) { + try { + final Cloud cloud = Jenkins.getInstance().getCloud(cloudId); + if (cloud instanceof DockerCloud) { + final DockerCloud dockerCloud = (DockerCloud) cloud; + Version verResult = dockerCloud.getConnector().getClient().versionCmd().exec(); + + return ok(reflectionToString(verResult, MULTI_LINE_STYLE)); + } else { + return FormValidation.error("cloudId '" + cloudId + "' isn't DockerCloud"); + } + } catch (Throwable t) { + return error(t, "error"); + } + } + + public ListBoxModel doFillCloudIdItems() { + ListBoxModel items = new ListBoxModel(); + Jenkins.getInstance().clouds.getAll(DockerCloud.class) + .forEach(dockerCloud -> + items.add(dockerCloud.getDisplayName()) + ); + return items; + } + + @Nonnull + @Override + public String getDisplayName() { + return "Docker Cloud by Name"; + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/YADockerConnector.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/YADockerConnector.java new file mode 100644 index 00000000..819e3bed --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/connector/YADockerConnector.java @@ -0,0 +1,30 @@ +package com.github.kostyasha.yad.connector; + +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; +import hudson.DescriptorExtensionList; +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; + +/** + * Different connectors to docker. + * DockerConnector appeared first so can't rename and YAD become parent. + * + * @author Kanstantsin Shautsou + */ +public abstract class YADockerConnector extends AbstractDescribableImpl implements ExtensionPoint { + + public abstract DockerClient getClient() throws Exception; + + @Override + public YADockerConnectorDescriptor getDescriptor() { + return (YADockerConnectorDescriptor) super.getDescriptor(); + } + + public abstract static class YADockerConnectorDescriptor extends Descriptor { + public static DescriptorExtensionList allDockerConnectorDescriptors() { + return Jenkins.getInstance().getDescriptorList(YADockerConnector.class); + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerJNLPLauncher.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerJNLPLauncher.java index eef528bb..d789bf31 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerJNLPLauncher.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerJNLPLauncher.java @@ -146,7 +146,8 @@ public void launch(@Nonnull SlaveComputer computer, TaskListener listener) throw if (computer instanceof DockerComputer) { dockerComputer = (DockerComputer) computer; } else { - throw new IllegalArgumentException("Docker JNLP Launcher accepts only DockerComputer"); + listener.error("Docker JNLP Launcher accepts only DockerComputer.class"); + throw new IllegalArgumentException("Docker JNLP Launcher accepts only DockerComputer.class"); } Objects.requireNonNull(dockerComputer); @@ -154,6 +155,7 @@ public void launch(@Nonnull SlaveComputer computer, TaskListener listener) throw final DockerCloud dockerCloud = dockerComputer.getCloud(); // Objects.requireNonNull(dockerCloud, "Cloud not found for computer " + computer.getName()); if (isNull(dockerCloud)) { + listener.error("Cloud not found for computer " + computer.getName()); throw new NullPointerException("Cloud not found for computer " + computer.getName()); } final DockerClient connect = dockerCloud.getClient(); diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerSingleJNLPLauncher.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerSingleJNLPLauncher.java new file mode 100644 index 00000000..838f9247 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/DockerComputerSingleJNLPLauncher.java @@ -0,0 +1,321 @@ +package com.github.kostyasha.yad.launcher; + +import com.github.kostyasha.yad.DockerComputerSingle; +import com.github.kostyasha.yad.DockerContainerLifecycle; +import com.github.kostyasha.yad.DockerSlaveSingle; +import com.github.kostyasha.yad.commons.DockerCreateContainer; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.StartContainerCmd; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.exception.NotFoundException; +import com.github.kostyasha.yad_docker_java.com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.kostyasha.yad_docker_java.javax.ws.rs.ProcessingException; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.slaves.JNLPLauncher; +import hudson.slaves.SlaveComputer; +import hudson.util.TimeUnit2; +import jenkins.model.Jenkins; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.kohsuke.stapler.DataBoundSetter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +import static com.github.kostyasha.yad_docker_java.org.apache.commons.lang.StringUtils.isNotEmpty; +import static com.github.kostyasha.yad_docker_java.org.apache.commons.lang.StringUtils.trimToEmpty; +import static java.util.Objects.isNull; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * Alternative {@link DockerComputerJNLPLauncher}. + *

+ * Used for {@link DockerSlaveSingle} {@link DockerComputerSingle} + * + * @author Kanstantsin Shautsou + */ +public class DockerComputerSingleJNLPLauncher extends JNLPLauncher { + private static final Logger LOG = LoggerFactory.getLogger(DockerComputerSingleJNLPLauncher.class); + + private static final String NL = "\"\n"; + public static final long DEFAULT_TIMEOUT = 120L; + public static final String DEFAULT_USER = "jenkins"; + + protected long launchTimeout = DEFAULT_TIMEOUT; //seconds + + protected String user = DEFAULT_USER; + + protected String jvmOpts = ""; + + protected String slaveOpts = ""; + + protected String jenkinsUrl = ""; + + protected boolean noCertificateCheck = false; + + private String containerId; + + @DataBoundSetter + public void setSlaveOpts(String slaveOpts) { + this.slaveOpts = trimToEmpty(slaveOpts); + } + + @Nonnull + public String getSlaveOpts() { + return trimToEmpty(slaveOpts); + } + + @DataBoundSetter + public void setJvmOpts(String jvmOpts) { + this.jvmOpts = trimToEmpty(jvmOpts); + } + + @Nonnull + public String getJvmOpts() { + return trimToEmpty(jvmOpts); + } + + @DataBoundSetter + public void setNoCertificateCheck(boolean noCertificateCheck) { + this.noCertificateCheck = noCertificateCheck; + } + + public boolean isNoCertificateCheck() { + return noCertificateCheck; + } + + @DataBoundSetter + public void setUser(String user) { + this.user = trimToEmpty(user); + } + + public String getUser() { + return trimToEmpty(user); + } + + public long getLaunchTimeout() { + return launchTimeout; + } + + @DataBoundSetter + public void setLaunchTimeout(long launchTimeout) { + this.launchTimeout = launchTimeout; + } + + @Nonnull + public String getJenkinsUrl(String rootUrl) { + return isNotEmpty(jenkinsUrl) ? jenkinsUrl : trimToEmpty(rootUrl); + } + + public void setJenkinsUrl(String jenkinsUrl) { + this.jenkinsUrl = trimToEmpty(jenkinsUrl); + } + + private void setContainerId(String containerId) { + this.containerId = containerId; + } + + /** + * @return not null when container launched. + */ + @CheckForNull + public String getContainerId() { + return containerId; + } + + @Override + public void launch(SlaveComputer computer, TaskListener listener) { + listener.getLogger().println("Launching " + computer.getDisplayName()); + try { + if (!(computer instanceof DockerComputerSingle)) { + throw new IllegalStateException(computer.getName() + " not instance of DockerComputerSingle"); + } + provisionWithWait((DockerComputerSingle) computer, listener); + } catch (Throwable e) { + LOG.error("Can't launch ", e); + listener.error(e.toString()); + } + } + + /** + * Provision slave container and wait for it's availability. + */ + private void provisionWithWait(DockerComputerSingle computer, TaskListener listener) + throws Exception { + final PrintStream logger = listener.getLogger(); + + final Run run = computer.getRun(); + final DockerSlaveSingle slave = computer.getNode(); + if (isNull(slave)) { + throw new IllegalStateException("Can't get slave for " + computer.getNode()); + } + final DockerClient client = slave.getConnector().getClient(); + final DockerContainerLifecycle containerLifecycle = slave.getConfig().getDockerContainerLifecycle(); + + final String imageId = containerLifecycle.getImage(); + + //pull image + logger.println("Pulling image " + imageId + "..."); + containerLifecycle.getPullImage().exec(client, imageId); + + logger.println("Trying to run container for " + imageId); + LOG.info("Trying to run container for {}", imageId); + final DockerCreateContainer createContainer = containerLifecycle.getCreateContainer(); + CreateContainerCmd containerConfig = client.createContainerCmd(imageId); + // template specific options + createContainer.fillContainerConfig(containerConfig); + + // cloud specific options + appendContainerConfig(containerConfig); + + // create + CreateContainerResponse createResp = containerConfig.exec(); + String cId = createResp.getId(); + setContainerId(cId); + logger.println("Created container " + cId + ", for " + run.getDisplayName()); + LOG.debug("Created container {}, for {}", cId, run.getDisplayName()); + // start + StartContainerCmd startCommand = client.startContainerCmd(cId); + startCommand.exec(); + logger.println("Started container " + cId); + LOG.debug("Start container {}, for {}", cId, run.getDisplayName()); + + boolean running = false; + long launchTime = System.currentTimeMillis(); + while (!running && + TimeUnit2.SECONDS.toMillis(launchTimeout) > System.currentTimeMillis() - launchTime) { + try { + InspectContainerResponse inspectResp = client.inspectContainerCmd(cId).exec(); + if (isTrue(inspectResp.getState().getRunning())) { + logger.println("Container is running!"); + LOG.debug("Container {} is running", cId); + running = true; + } else { + logger.println("Container is not running..."); + } + } catch (ProcessingException ignore) { + } + Thread.sleep(1000); + } + + if (!running) { + listener.error("Failed to run container for %s, clean-up container", imageId); + LOG.error("Failed to run container for {}, clean-up container", imageId); + containerLifecycle.getRemoveContainer().exec(client, cId); + } + + // now real launch + final String rootUrl = getJenkinsUrl(Jenkins.getInstance().getRootUrl()); +// Objects.requireNonNull(rootUrl, "Jenkins root url is not specified!"); + if (isNull(rootUrl)) { + listener.fatalError("Jenkins root url is not specified!"); + containerLifecycle.getRemoveContainer().exec(client, cId); + throw new IllegalStateException("Jenkins root url is not specified!"); + } + final DockerSlaveSingle node = computer.getNode(); + if (isNull(node)) { + throw new NullPointerException("Node can't be null"); + } + + // exec jnlp connection in running container + // TODO implement PID 1 replacement + String startCmd = + "cat << EOF > /tmp/config.sh.tmp && cd /tmp && mv config.sh.tmp config.sh\n" + + "JENKINS_URL=\"" + rootUrl + NL + + "JENKINS_USER=\"" + getUser() + NL + + "JENKINS_HOME=\"" + node.getRemoteFS() + NL + + "COMPUTER_URL=\"" + computer.getUrl() + NL + + "COMPUTER_SECRET=\"" + computer.getJnlpMac() + NL + + "JAVA_OPTS=\"" + getJvmOpts() + NL + + "SLAVE_OPTS=\"" + getSlaveOpts() + NL + + "NO_CERTIFICATE_CHECK=\"" + isNoCertificateCheck() + NL + + "EOF" + "\n"; + + try { + final ExecCreateCmdResponse createCmdResponse = client.execCreateCmd(cId) + .withTty(true) + .withAttachStdin(false) + .withAttachStderr(true) + .withAttachStdout(true) + .withCmd("/bin/sh", "-cxe", startCmd.replace("$", "\\$")) + .exec(); + + logger.println("Starting connection command for " + cId); + LOG.info("Starting connection command for {}", cId); + + try (ExecStartResultCallback exec = client.execStartCmd(createCmdResponse.getId()) + .withDetach(true) + .withTty(true) + .exec(new ExecStartResultCallback()) + ) { + exec.awaitCompletion(10, SECONDS); + } catch (NotFoundException ex) { + listener.error("Can't execute command: " + ex.getMessage().trim()); + LOG.error("Can't execute jnlp connection command: '{}'", ex.getMessage().trim()); + containerLifecycle.getRemoveContainer().exec(client, cId); + node.terminate(); + throw ex; + } + } catch (Throwable ex) { + listener.error("Can't execute command: " + ex.getMessage().trim()); + LOG.error("Can't execute jnlp connection command: '{}'", ex.getMessage().trim()); + containerLifecycle.getRemoveContainer().exec(client, cId); + node.terminate(); + throw ex; + } + + LOG.info("Successfully executed jnlp connection for '{}'", cId); + logger.println("Successfully executed jnlp connection for " + cId); + + // TODO better strategy + launchTime = System.currentTimeMillis(); + while (computer.isReallyOffline() && + TimeUnit2.SECONDS.toMillis(launchTimeout) > System.currentTimeMillis() - launchTime) { + logger.println("Waiting slave connection..."); + Thread.sleep(1000); + } + + if (computer.isReallyOffline()) { + LOG.info("Launch timeout, termintaing slave based on '{}'", cId); + logger.println("Launch timeout, termintaing slave."); + containerLifecycle.getRemoveContainer().exec(client, cId); + node.terminate(); + throw new IOException("Can't connect slave to jenkins"); + } + + LOG.info("Launched slave '{}' '{}' based on '{}'", + computer.getSlaveVersion(), computer.getName(), cId); + logger.println("Launched slave for " + cId); + } + + public void appendContainerConfig(CreateContainerCmd createContainerCmd) + throws IOException { + try (InputStream instream = DockerComputerJNLPLauncher.class.getResourceAsStream("DockerComputerJNLPLauncher/init.sh")) { + final String initCmd = IOUtils.toString(instream, Charsets.UTF_8); + if (initCmd == null) { + throw new IllegalStateException("Resource file 'init.sh' not found"); + } + + // wait for params + createContainerCmd.withCmd("/bin/sh", + "-cxe", + "cat << EOF >> /tmp/init.sh && chmod +x /tmp/init.sh && exec /tmp/init.sh\n" + + initCmd.replace("$", "\\$") + "\n" + + "EOF" + "\n" + ); + } + + createContainerCmd.withTty(true); + createContainerCmd.withStdinOpen(true); + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/NoOpDelegatingComputerLauncher.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/NoOpDelegatingComputerLauncher.java new file mode 100644 index 00000000..322b0f0f --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/launcher/NoOpDelegatingComputerLauncher.java @@ -0,0 +1,35 @@ +package com.github.kostyasha.yad.launcher; + +import hudson.Extension; +import hudson.model.TaskListener; +import hudson.slaves.ComputerLauncher; +import hudson.slaves.DelegatingComputerLauncher; +import hudson.slaves.SlaveComputer; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Delegating ComputerLauncher without launch. + * + * @author Kanstantsin Shautsou + */ +public class NoOpDelegatingComputerLauncher extends DelegatingComputerLauncher { + public NoOpDelegatingComputerLauncher(ComputerLauncher core) { + super(core); + } + + @Override + public void launch(SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException { + // noop + } + + @Extension + public static final class DescriptorImpl extends DelegatingComputerLauncher.DescriptorImpl { + @Nonnull + @Override + public String getDisplayName() { + return "No Launching Delegating Computer Launcher"; + } + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/listener/DockerRunListener.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/listener/DockerRunListener.java deleted file mode 100644 index cbc1422a..00000000 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/listener/DockerRunListener.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.kostyasha.yad.listener; - -import com.github.kostyasha.yad.DockerComputer; -import hudson.Extension; -import hudson.model.Computer; -import hudson.model.Executor; -import hudson.model.Run; -import hudson.model.TaskListener; -import hudson.model.listeners.RunListener; -import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.text.ParseException; - -import static com.github.kostyasha.yad.utils.ContainerRecordUtils.createRecordFor; - -/** - * @author Kanstantsin Shautsou - */ -@Extension -public class DockerRunListener extends RunListener> { - private static final Logger LOG = LoggerFactory.getLogger(DockerRunListener.class); - - @Override - public void onStarted(Run run, TaskListener listener) { - final Executor executor = run.getExecutor(); - if (executor == null) { - return; - } - - final Computer owner = executor.getOwner(); - DockerComputer dockerComputer; - if (owner instanceof DockerComputer) { - dockerComputer = (DockerComputer) owner; - } else { - return; - } - - try { - DockerFingerprints.addRunFacet( - createRecordFor(dockerComputer), - run - ); - } catch (IOException | ParseException e) { - LOG.error("Can't add fingerprint to run {}", run, e); - } - } -} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/FlyweightCauseOfBlockage.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/FlyweightCauseOfBlockage.java similarity index 87% rename from yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/FlyweightCauseOfBlockage.java rename to yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/FlyweightCauseOfBlockage.java index 28e5c8af..7a7069c3 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/FlyweightCauseOfBlockage.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/FlyweightCauseOfBlockage.java @@ -1,4 +1,4 @@ -package com.github.kostyasha.yad; +package com.github.kostyasha.yad.queue; import hudson.model.queue.CauseOfBlockage; diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/SingleNodeCauseOfBlockage.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/SingleNodeCauseOfBlockage.java new file mode 100644 index 00000000..ee8f0065 --- /dev/null +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/queue/SingleNodeCauseOfBlockage.java @@ -0,0 +1,23 @@ +package com.github.kostyasha.yad.queue; + +import hudson.model.queue.CauseOfBlockage; + +/** + * @author Kanstantsin Shautsou + */ +public class SingleNodeCauseOfBlockage extends CauseOfBlockage { + private String displayName; + + public SingleNodeCauseOfBlockage(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String getShortDescription() { + return "Slave tied to " + getDisplayName(); + } +} diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/strategy/DockerOnceRetentionStrategy.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/strategy/DockerOnceRetentionStrategy.java index 781c8934..dd77c784 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/strategy/DockerOnceRetentionStrategy.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/strategy/DockerOnceRetentionStrategy.java @@ -93,7 +93,7 @@ public void taskCompletedWithProblems(Executor executor, Queue.Task task, long d done(executor); } - private void done(Executor executor) { + protected void done(Executor executor) { final AbstractCloudComputer c = (AbstractCloudComputer) executor.getOwner(); Queue.Executable exec = executor.getCurrentExecutable(); if (executor instanceof OneOffExecutor) { @@ -110,7 +110,7 @@ private void done(Executor executor) { done(c); } - private void done(final AbstractCloudComputer c) { + protected void done(final AbstractCloudComputer c) { c.setAcceptingTasks(false); // just in case synchronized (this) { if (terminating) { diff --git a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/utils/ContainerRecordUtils.java b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/utils/ContainerRecordUtils.java index b3f41271..03926505 100644 --- a/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/utils/ContainerRecordUtils.java +++ b/yet-another-docker-plugin/src/main/java/com/github/kostyasha/yad/utils/ContainerRecordUtils.java @@ -5,7 +5,14 @@ import com.github.kostyasha.yad_docker_java.com.fasterxml.jackson.databind.util.StdDateFormat; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.DockerClient; import com.github.kostyasha.yad_docker_java.com.github.dockerjava.api.command.InspectContainerResponse; +import hudson.model.Computer; +import hudson.model.Executor; +import hudson.model.Run; +import hudson.model.TaskListener; import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.text.ParseException; @@ -19,6 +26,8 @@ * @author Kanstantsin Shautsou */ public class ContainerRecordUtils { + private static final Logger LOG = LoggerFactory.getLogger(ContainerRecordUtils.class); + private ContainerRecordUtils() { } @@ -46,4 +55,28 @@ public static ContainerRecord createRecordFor(DockerComputer computer) throws Pa return new ContainerRecord(host, containerId, imageId, containerName, created, tags); } + + public static void attachFacet(Run run, TaskListener listener) { + final Executor executor = run.getExecutor(); + if (executor == null) { + return; + } + + final Computer owner = executor.getOwner(); + DockerComputer dockerComputer; + if (owner instanceof DockerComputer) { + dockerComputer = (DockerComputer) owner; + try { + DockerFingerprints.addRunFacet( + createRecordFor(dockerComputer), + run + ); + } catch (IOException | ParseException e) { + listener.error("Can't add Docker fingerprint to run."); + LOG.error("Can't add fingerprint to run {}", run, e); + } + } + + + } } diff --git a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerConnector/config.groovy b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerConnector/config.groovy index 95246a14..2990da32 100644 --- a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerConnector/config.groovy +++ b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerConnector/config.groovy @@ -33,6 +33,10 @@ f.entry(title: _("Connect timeout"), field: "connectTimeout", default: "0") { f.number() } +f.entry(title: _("Read timeout"), field: "readTimeout", default: "0") { + f.number() +} + f.validateButton(title: _("Test Connection"), progress: _("Testing..."), method: "testConnection", with: "serverUrl,credentialsId,version,connectorType" diff --git a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerSlaveTemplate/config.groovy b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerSlaveTemplate/config.groovy index 5b83c36b..57879c48 100644 --- a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerSlaveTemplate/config.groovy +++ b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/DockerSlaveTemplate/config.groovy @@ -15,8 +15,8 @@ if (instance == null) { instance = new DockerSlaveTemplate(); } -f.invisibleEntry() { - f.textbox(field: "id") +f.entry(title: "Internal template ID") { + f.textbox(field: "id", readonly: "true") } f.entry(title: _("Max Instances"), field: "maxCapacity") { diff --git a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorId/config.groovy b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorId/config.groovy new file mode 100644 index 00000000..e7df896d --- /dev/null +++ b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorId/config.groovy @@ -0,0 +1,9 @@ +package com.github.kostyasha.yad.connector.DockerCloudConnectorId + +import lib.FormTagLib + +def f = namespace(FormTagLib) + +f.entry(field: "cloudId", title: "YADocker Connector") { + f.select(name: "cloudId") +} diff --git a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateId/config.groovy b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateId/config.groovy new file mode 100644 index 00000000..9907408d --- /dev/null +++ b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateId/config.groovy @@ -0,0 +1,7 @@ +package com.github.kostyasha.yad.connector.DockerCloudConnectorTemplateId + +import lib.FormTagLib + +def f = namespace(FormTagLib) + +f.property() \ No newline at end of file diff --git a/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateRaw/config.groovy b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateRaw/config.groovy new file mode 100644 index 00000000..5d8d39fb --- /dev/null +++ b/yet-another-docker-plugin/src/main/resources/com/github/kostyasha/yad/connector/DockerCloudConnectorTemplateRaw/config.groovy @@ -0,0 +1,7 @@ +package com.github.kostyasha.yad.connector.DockerCloudConnectorTemplateRaw + +import lib.FormTagLib + +def f = namespace(FormTagLib) + +f.property(field: "slaveConfig") diff --git a/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/TaskStepTest.java b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/TaskStepTest.java new file mode 100644 index 00000000..a739e25d --- /dev/null +++ b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/TaskStepTest.java @@ -0,0 +1,39 @@ +package com.github.kostyasha.yad; + +import jenkins.model.Jenkins; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static com.github.kostyasha.yad.other.ConnectorType.NETTY; + +/** + * @author Kanstantsin Shautsou + */ +public class TaskStepTest { + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Ignore + @Test + public void actionTask() throws Exception { + final Jenkins jenkins = jRule.getInstance(); + + final DockerSlaveTemplate slaveTemplate = new DockerSlaveTemplate(); + + slaveTemplate.setLabelString("docker-slave"); + +// final DockerCloud dockerCloud = new DockerCloud("localTestCloud"); + +// final DockerLabelAssignmentAction assignmentAction = new DockerLabelAssignmentAction("docker-slave"); + final DockerConnector dockerConnector = new DockerConnector("tcp://192.168.1.3:2376/"); + dockerConnector.setConnectorType(NETTY); + +// assignmentAction.setConnector(dockerConnector); + +// final Queue.WaitingItem waitingItem = jenkins.getQueue().schedule(new DockerTask(), 0, assignmentAction); + + jRule.waitUntilNoActivity(); + } +} diff --git a/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/DockerTask.java b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/DockerTask.java new file mode 100644 index 00000000..ced3e062 --- /dev/null +++ b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/DockerTask.java @@ -0,0 +1,100 @@ +package com.github.kostyasha.yad.step; + +import hudson.model.Label; +import hudson.model.Node; +import hudson.model.Queue; +import hudson.model.ResourceList; +import hudson.model.queue.AbstractQueueTask; +import hudson.security.ACL; +import hudson.security.AccessControlled; +import hudson.security.Permission; +import jenkins.model.Jenkins; +import org.acegisecurity.AccessDeniedException; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * @author Kanstantsin Shautsou + */ +public class DockerTask extends AbstractQueueTask implements Queue.TransientTask, AccessControlled { + @Override + public boolean isBuildBlocked() { + return false; + } + + @Override + public String getWhyBlocked() { + return null; + } + + @Override + public String getName() { + return "Some name"; + } + + @Override + public String getFullDisplayName() { + return "Full display name"; + } + + @Override + public void checkAbortPermission() { + } + + @Override + public boolean hasAbortPermission() { + return true; + } + + @Override + public String getUrl() { + return null; + } + + @Override + public ResourceList getResourceList() { + return ResourceList.EMPTY; + } + + @Override + public String getDisplayName() { + return "Display name for task"; + } + + @Override + public Label getAssignedLabel() { + return null; + } + + @Override + public Node getLastBuiltOn() { + return null; + } + + @Override + public long getEstimatedDuration() { + return -1; + } + + @Override + public Queue.Executable createExecutable() throws IOException { + return new MyExecutable(this); + } + + @Nonnull + @Override + public ACL getACL() { + return Jenkins.getInstance().getAuthorizationStrategy().getRootACL(); + } + + @Override + public void checkPermission(@Nonnull Permission permission) throws AccessDeniedException { + getACL().checkPermission(permission); + } + + @Override + public boolean hasPermission(@Nonnull Permission permission) { + return getACL().hasPermission(permission); + } +} diff --git a/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/MyExecutable.java b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/MyExecutable.java new file mode 100644 index 00000000..47a6146c --- /dev/null +++ b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/MyExecutable.java @@ -0,0 +1,49 @@ +package com.github.kostyasha.yad.step; + +import hudson.model.Queue; +import hudson.model.queue.SubTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; + +import static java.util.concurrent.TimeUnit.MINUTES; + +/** + * @author Kanstantsin Shautsou + */ +public class MyExecutable implements Queue.Executable { + private static final Logger LOG = LoggerFactory.getLogger(MyExecutable.class); + + private DockerTask dockerTask; + + public MyExecutable(DockerTask dockerTask) { + this.dockerTask = dockerTask; + } + + @Nonnull + @Override + public SubTask getParent() { + return dockerTask; + } + + @Override + public void run() { + LOG.info(" In run!"); + try { + Thread.sleep(MINUTES.toMillis(5)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public long getEstimatedDuration() { + return -1; + } + + @Override + public String toString() { + return "This is my super executable"; + } +} diff --git a/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/TaskBuildStep.java b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/TaskBuildStep.java new file mode 100644 index 00000000..4df7638c --- /dev/null +++ b/yet-another-docker-plugin/src/test/java/com/github/kostyasha/yad/step/TaskBuildStep.java @@ -0,0 +1,55 @@ +package com.github.kostyasha.yad.step; + +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractProject; +import hudson.model.Queue; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.queue.SubTask; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import jenkins.tasks.SimpleBuildStep; +import org.kohsuke.stapler.DataBoundConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; + +import static java.util.concurrent.TimeUnit.MINUTES; + +/** + * @author Kanstantsin Shautsou + */ +public class TaskBuildStep extends Builder implements SimpleBuildStep { + + @DataBoundConstructor + public TaskBuildStep() { + } + + @Override + public void perform(@Nonnull Run run, @Nonnull FilePath filePath, @Nonnull Launcher launcher, + @Nonnull TaskListener taskListener) throws InterruptedException, IOException { + taskListener.getLogger().println("Entering task producer"); + Queue.getInstance().schedule(new DockerTask(), 0); + taskListener.getLogger().println("Task scheduled? sleep"); + Thread.sleep(MINUTES.toMillis(10)); + taskListener.getLogger().println("end of sleep"); + } + + @Extension + public static final class DescriptorImpl extends BuildStepDescriptor { + @Nonnull + @Override + public String getDisplayName() { + return "Test Task Producer"; + } + + @Override + public boolean isApplicable(Class aClass) { + return true; + } + } +} diff --git a/yet-another-docker-plugin/src/test/java/org/jvnet/hudson/test/DockerSimpleBuildWrapperTest.java b/yet-another-docker-plugin/src/test/java/org/jvnet/hudson/test/DockerSimpleBuildWrapperTest.java new file mode 100644 index 00000000..37f050f9 --- /dev/null +++ b/yet-another-docker-plugin/src/test/java/org/jvnet/hudson/test/DockerSimpleBuildWrapperTest.java @@ -0,0 +1,109 @@ +package org.jvnet.hudson.test; + +import com.github.kostyasha.yad.DockerConnector; +import com.github.kostyasha.yad.DockerSimpleBuildWrapper; +import com.github.kostyasha.yad.DockerSlaveConfig; +import com.github.kostyasha.yad.launcher.DockerComputerSingleJNLPLauncher; +import com.github.kostyasha.yad.launcher.NoOpDelegatingComputerLauncher; +import com.github.kostyasha.yad.strategy.DockerOnceRetentionStrategy; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.queue.QueueTaskFuture; +import hudson.tasks.Shell; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebXmlConfiguration; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static com.github.kostyasha.yad.other.ConnectorType.JERSEY; + +/** + * @author Kanstantsin Shautsou + */ +public class DockerSimpleBuildWrapperTest { + private static final Logger LOG = LoggerFactory.getLogger(DockerSimpleBuildWrapperTest.class); + + // switch to Inet4Address? + private static final String ADDRESS = "192.168.1.3"; + + @Rule + public JenkinsRule jRule = new JenkinsRule() { + @Override + public URL getURL() throws IOException { + return new URL("http://" + ADDRESS + ":" + localPort + contextPath + "/"); + } + + @Override + protected ServletContext createWebServer() throws Exception { + server = new Server(new ThreadPoolImpl(new ThreadPoolExecutor(10, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> { + Thread t = new Thread(r); + t.setName("Jetty Thread Pool"); + return t; + }))); + + WebAppContext context = new WebAppContext(WarExploder.getExplodedDir().getPath(), contextPath); + context.setClassLoader(getClass().getClassLoader()); + context.setConfigurations(new Configuration[]{new WebXmlConfiguration()}); + context.addBean(new NoListenerConfiguration(context)); + server.setHandler(context); + context.setMimeTypes(MIME_TYPES); + context.getSecurityHandler().setLoginService(configureUserRealm()); + context.setResourceBase(WarExploder.getExplodedDir().getPath()); + + ServerConnector connector = new ServerConnector(server); + HttpConfiguration config = connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration(); + // use a bigger buffer as Stapler traces can get pretty large on deeply nested URL + config.setRequestHeaderSize(12 * 1024); + connector.setHost(ADDRESS); + if (System.getProperty("port") != null) + connector.setPort(Integer.parseInt(System.getProperty("port"))); + + server.addConnector(connector); + server.start(); + + localPort = connector.getLocalPort(); + LOG.info("Running on {}", getURL()); + + return context.getServletContext(); + } + }; + + @Ignore("For local experiments") + @Test + public void testWrapper() throws Exception { + final FreeStyleProject project = jRule.createProject(FreeStyleProject.class, "freestyle"); + + final DockerConnector connector = new DockerConnector("tcp://" + ADDRESS + ":2376/"); + connector.setConnectorType(JERSEY); + + final DockerSlaveConfig config = new DockerSlaveConfig(); + config.getDockerContainerLifecycle().setImage("java:8-jdk-alpine"); + config.setLauncher(new NoOpDelegatingComputerLauncher(new DockerComputerSingleJNLPLauncher())); + config.setRetentionStrategy(new DockerOnceRetentionStrategy(10)); + + final DockerSimpleBuildWrapper dockerSimpleBuildWrapper = new DockerSimpleBuildWrapper(connector, config); + project.getBuildWrappersList().add(dockerSimpleBuildWrapper); + project.getBuildersList().add(new Shell("sleep 30")); + + final QueueTaskFuture taskFuture = project.scheduleBuild2(0); + + jRule.waitUntilNoActivity(); + jRule.pause(); + } + +} \ No newline at end of file diff --git a/yet-another-docker-plugin/src/test/resources/com/github/kostyasha/yad/step/MyExecutable/executorCell.groovy b/yet-another-docker-plugin/src/test/resources/com/github/kostyasha/yad/step/MyExecutable/executorCell.groovy new file mode 100644 index 00000000..bc1eb55d --- /dev/null +++ b/yet-another-docker-plugin/src/test/resources/com/github/kostyasha/yad/step/MyExecutable/executorCell.groovy @@ -0,0 +1,11 @@ +package com.github.kostyasha.yad.step.MyExecutable + +import lib.FormTagLib + +def f = namespace(FormTagLib); + +td(class: "pane") { + div(style: "white-space: normal") { + text("Building") + } +}