├── settings.gradle ├── .envrc ├── Dockerfile.agent ├── dcos-testing ├── conf │ ├── plugins.conf │ ├── jenkins │ │ └── configuration.yaml │ └── nginx │ │ └── nginx.conf.template ├── README.md └── jenkins-app.json ├── src ├── main │ ├── resources │ │ ├── org │ │ │ └── jenkinsci │ │ │ │ └── plugins │ │ │ │ └── mesos │ │ │ │ ├── MesosSlaveInfo │ │ │ │ └── URI │ │ │ │ │ ├── help-value.html │ │ │ │ │ ├── help-executable.html │ │ │ │ │ ├── help-extract.html │ │ │ │ │ └── config.jelly │ │ │ │ ├── MesosCloud │ │ │ │ ├── help-agentUser.html │ │ │ │ ├── help-frameworkName.html │ │ │ │ ├── help-mesosMasterUrl.html │ │ │ │ ├── help-jenkinsURL.html │ │ │ │ ├── help-role.html │ │ │ │ └── config.jelly │ │ │ │ ├── MesosAgentSpecTemplate │ │ │ │ ├── Volume │ │ │ │ │ ├── help-readOnly.html │ │ │ │ │ ├── help-containerPath.html │ │ │ │ │ ├── help-hostPath.html │ │ │ │ │ └── config.jelly │ │ │ │ ├── help-mem.html │ │ │ │ ├── help-label.html │ │ │ │ ├── ContainerInfo │ │ │ │ │ ├── help-volumes.html │ │ │ │ │ ├── help-dockerForcePullImage.html │ │ │ │ │ ├── help-isDind.html │ │ │ │ │ ├── help-networking.html │ │ │ │ │ ├── help-dockerPrivilegedMode.html │ │ │ │ │ ├── help-type.html │ │ │ │ │ ├── help-dockerImage.html │ │ │ │ │ └── config.jelly │ │ │ │ ├── help-cpus.html │ │ │ │ ├── help-minExecutors.html │ │ │ │ ├── help-disk.html │ │ │ │ ├── help-jnlpArgs.html │ │ │ │ ├── help-maxExecutors.html │ │ │ │ ├── help-idleTerminationMinutes.html │ │ │ │ ├── help-domainFilterModel.html │ │ │ │ ├── help-agentAttributes.html │ │ │ │ ├── help-agentCommandStyle.html │ │ │ │ ├── NetworkInfo │ │ │ │ │ └── config.jelly │ │ │ │ └── config.jelly │ │ │ │ ├── MesosJenkinsAgent │ │ │ │ └── configure-entries.jelly │ │ │ │ └── config │ │ │ │ └── models │ │ │ │ └── faultdomain │ │ │ │ ├── DomainFilterModel │ │ │ │ └── config.jelly │ │ │ │ └── StringDomainFilter │ │ │ │ └── config-detail.jelly │ │ ├── index.jelly │ │ └── application.conf │ └── java │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── mesos │ │ ├── config │ │ └── models │ │ │ └── faultdomain │ │ │ ├── DomainFilterModelDescriptor.java │ │ │ ├── DomainFilterModel.java │ │ │ ├── Any.java │ │ │ ├── Home.java │ │ │ └── StringDomainFilter.java │ │ ├── MesosRetentionStrategy.java │ │ ├── MesosPodRecordRepository.java │ │ ├── Metrics.java │ │ ├── MesosComputer.java │ │ ├── NoDelayProvisionerStrategy.java │ │ ├── MesosSlaveInfo.java │ │ ├── api │ │ ├── Settings.java │ │ ├── Session.java │ │ ├── LaunchCommandBuilder.java │ │ └── RunTemplateFactory.java │ │ ├── MesosJenkinsAgent.java │ │ ├── MesosAgentSpecTemplate.java │ │ └── MesosApi.java └── test │ ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── mesos │ │ ├── MetricsTest.java │ │ ├── integration │ │ ├── IntegrationTest.java │ │ ├── DockerAgentTest.java │ │ ├── MesosApiTest.java │ │ ├── MesosCloudProvisionTest.java │ │ └── MesosJenkinsAgentLifecycleTest.java │ │ ├── MesosAgentSpecTemplateDescriptorTest.java │ │ ├── MesosSlaveInfoTest.java │ │ ├── fixture │ │ └── AgentSpecMother.java │ │ ├── api │ │ ├── LaunchCommandBuilderTest.java │ │ └── SessionTest.java │ │ ├── TestUtils.java │ │ ├── MesosCloudTest.java │ │ ├── MesosJenkinsAgentTest.java │ │ ├── MesosCloudDescriptorTest.java │ │ └── JenkinsConfigClient.java │ └── resources │ └── configuration.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── ci ├── set_port_range.sh ├── install_docker.sh ├── run.sh ├── install_mesos.sh └── provision.sh ├── Jenkinsfile ├── nodes └── Dockerfile.windows ├── .github ├── workflows │ └── gradle.yml └── stale.yml ├── gradlew.bat ├── Dockerfile ├── gradlew ├── README.md └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mesos' 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export JAVA_HOME=`/usr/libexec/java_home -v 1.8` 2 | -------------------------------------------------------------------------------- /Dockerfile.agent: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:8u252 2 | 3 | ENTRYPOINT ["/bin/bash", "-c"] 4 | CMD [] 5 | -------------------------------------------------------------------------------- /dcos-testing/conf/plugins.conf: -------------------------------------------------------------------------------- 1 | configuration-as-code:1.36 2 | metrics:4.0.2.6 3 | job-dsl:1.76 4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosSlaveInfo/URI/help-value.html: -------------------------------------------------------------------------------- 1 |
2 | The URI itself. 3 |
-------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mesos-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/help-agentUser.html: -------------------------------------------------------------------------------- 1 |
2 | The user name on the host used to start the Jenkins agent. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosSlaveInfo/URI/help-executable.html: -------------------------------------------------------------------------------- 1 |
2 | Defines whether the downloaded file should be executable or not. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/Volume/help-readOnly.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, the volume will be read-only from the container. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-mem.html: -------------------------------------------------------------------------------- 1 |
2 | The memory the Jenkins agent task will require on the Mesos cluster. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosSlaveInfo/URI/help-extract.html: -------------------------------------------------------------------------------- 1 |
2 | Defines whether the downloaded file is compressed and should be extracted. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/Volume/help-containerPath.html: -------------------------------------------------------------------------------- 1 |
2 | The volume will be mapped to the given path inside the container. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/Volume/help-hostPath.html: -------------------------------------------------------------------------------- 1 |
2 | The following path on the host will be mapped inside the container. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-label.html: -------------------------------------------------------------------------------- 1 |
2 | Jobs assigned to this label will provision Jenkins agents using this template. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-volumes.html: -------------------------------------------------------------------------------- 1 |
2 | This section allows to specify volumes bindings into the container. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-cpus.html: -------------------------------------------------------------------------------- 1 |
2 | The number of CPU shares the Jenkins agent task will require on the Mesos cluster. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-minExecutors.html: -------------------------------------------------------------------------------- 1 |
2 | This is the minimum number of executors the agent will have once connected to Jenkins. 3 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | .idea 4 | 5 | # Ignore Gradle build output directory 6 | build 7 | 8 | # Ignore Jenkins work dir 9 | work 10 | sandboxes 11 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-dockerForcePullImage.html: -------------------------------------------------------------------------------- 1 |
2 | The Docker image will be pulled even if it is already available locally. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-isDind.html: -------------------------------------------------------------------------------- 1 |
2 | Check if the Docker image runs a Docker daemon. This is required if you want to build Docker images. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/help-frameworkName.html: -------------------------------------------------------------------------------- 1 |
2 | This will be the name of the framework visible in the Mesos UI. It doesn't have to be unique, you can customize it if you want. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-disk.html: -------------------------------------------------------------------------------- 1 |
2 | Specify amount of disk needed from the Mesos cluster. If you don't want to add disk constraint on 3 | the offers, set it to 0.0. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-networking.html: -------------------------------------------------------------------------------- 1 |
2 | Specify the networking mode to use for this container. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-jnlpArgs.html: -------------------------------------------------------------------------------- 1 |
2 | Specifies the additional JNLP arguments (e.g., -jnlpCredentials) to be used when invoking 3 | the Jenkins agent on the Mesos agent. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-maxExecutors.html: -------------------------------------------------------------------------------- 1 |
2 | This is the maximum number of executors the agent will have once connected to Jenkins. 3 | If less jobs are pending, the agent will use the smaller number. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-idleTerminationMinutes.html: -------------------------------------------------------------------------------- 1 |
2 | Number of minutes of idleness before a agent should be terminated. A value of zero indicates that 3 | the agent should never be automatically terminated. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosJenkinsAgent/configure-entries.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 23 17:36:56 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-domainFilterModel.html: -------------------------------------------------------------------------------- 1 |
2 | A fault domain filter is used to specify on which Mesos Fault Domain 3 | the Jenkins node should run. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | This plugin can be used to connect Jenkins to a Mesos cluster 7 | and dynamically launch Jenkins agents based on the work load. 8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-dockerPrivilegedMode.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, the Docker container will be launched in privileged mode. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-type.html: -------------------------------------------------------------------------------- 1 |
2 | The type of containerizer to use in Mesos. See the 3 | Mesos documentation section 4 | for details. 5 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/config/models/faultdomain/DomainFilterModel/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-agentAttributes.html: -------------------------------------------------------------------------------- 1 |
2 | Selects on which Mesos agent the Jenkins node should run based on the agent string attributes. 3 | It should be a comma separated list of key:value pairs. 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/help-agentCommandStyle.html: -------------------------------------------------------------------------------- 1 |
2 | Defines the style of the command used to launch a Jenkins agent on Mesos. It defaults to Linux but 3 | also supports Windows. The launch commands differ because Windows use %ENV_VAR% while 4 | Unix based systems use ${ENV_VAR}. 5 |
-------------------------------------------------------------------------------- /ci/set_port_range.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # See http://mesos.apache.org/documentation/latest/port-mapping-isolator/ 4 | # and mesosphere.util.PortAllocator docs. 5 | echo "Ephemeral port range before: $(cat /proc/sys/net/ipv4/ip_local_port_range)" 6 | sysctl -w net.ipv4.ip_local_port_range="60001 61000" 7 | echo "Ephemeral port range after: $(cat /proc/sys/net/ipv4/ip_local_port_range)" 8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/help-dockerImage.html: -------------------------------------------------------------------------------- 1 |
2 | The name of the Docker image to execute. You can use any format accepted by the docker pull command. 3 |
4 | Note: The image must include a Java runtime so that the Jenkins agent can start. 5 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/NetworkInfo/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/help-mesosMasterUrl.html: -------------------------------------------------------------------------------- 1 |
2 | Set the Mesos master address in following format: 3 | 7 |
-------------------------------------------------------------------------------- /ci/install_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x -e -o pipefail 3 | 4 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - 5 | add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 6 | 7 | apt-get -y update 8 | apt-get -y install docker-ce docker-ce-cli containerd.io 9 | 10 | docker --version 11 | 12 | # Add user to docker group 13 | gpasswd -a runner docker 14 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/config/models/faultdomain/StringDomainFilter/config-detail.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/help-jenkinsURL.html: -------------------------------------------------------------------------------- 1 |
2 | http://host:post where Jenkins is actually running. For ex: http://myjenkinshost:8080. 3 | Jenkins URL provided in Jenkins location configuration could be a reverse proxy URL. 4 | In that case you can override it by specifying actual Jenkins URL here. 5 | This URL would be used by Jenkins agents spawned by Mesos plugin to talk to Jenkins Controller. 6 |
-------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/config/models/faultdomain/DomainFilterModelDescriptor.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.config.models.faultdomain; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | /** 6 | * Descriptor for instances of a domain filter. This is used in the agent spec template to collect 7 | * all descriptors by their base class. 8 | */ 9 | public abstract class DomainFilterModelDescriptor extends Descriptor {} 10 | -------------------------------------------------------------------------------- /ci/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit -o nounset -o pipefail 3 | 4 | systemctl stop mesos-master 5 | systemctl stop mesos-slave 6 | 7 | # Java 8 | yum install -y wget 9 | wget -q https://d3pxv6yz143wms.cloudfront.net/8.232.09.1/java-1.8.0-amazon-corretto-devel-1.8.0_232.b09-1.x86_64.rpm 10 | yum localinstall -y java-1.8.0-amazon-corretto-devel-1.8.0_232.b09-1.x86_64.rpm 11 | 12 | JAVA_HOME=/usr/lib/jvm/java-1.8.0-amazon-corretto ./gradlew clean check javadoc --info 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MetricsTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class MetricsTest { 9 | 10 | @Test 11 | void metricsPrefixSanitization() { 12 | final String prefix = "I'm an invalid pref$x!.1"; 13 | assertThat(Metrics.sanitize(prefix), is("I-m-an-invalid-pref-x-.1")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/help-role.html: -------------------------------------------------------------------------------- 1 |
2 | Specifies the resources role that Jenkins scheduler will receive offers for. If role is set to 3 | * (default), it will receive offers for unreserved resources. If role is set to something 4 | else (e.g., jenkins), it will receive offers for only reserved resources of the given role. 5 | See the Mesos Documentation for 6 | details. 7 |
-------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | node('maven') { 3 | stage('Build') { 4 | try { 5 | checkout scm 6 | sh './gradlew clean javadoc check -x integrationTest --info' 7 | } finally { 8 | junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml' 9 | 10 | publishHTML (target: [ alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'build/reports/spotbugs/', reportFiles: '*.html', reportName: 'SpotBugs' ]) 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /nodes/Dockerfile.windows: -------------------------------------------------------------------------------- 1 | # escape=` 2 | 3 | # Installer image 4 | FROM mcr.microsoft.com/windows/servercore:1809 AS installer 5 | 6 | SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"] 7 | 8 | # Install Chocolatey 9 | RUN $env:chocolateyUseWindowsCompression = 'true'; ` 10 | Set-ExecutionPolicy Bypass -Scope Process -Force; ` 11 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 12 | 13 | # Install Java runtime and .NET SDK. 14 | RUN choco install -y corretto8jre dotnetcore-sdk 15 | RUN choco install -y git -params '"/GitAndUnixToolsOnPath"' 16 | CMD ["powershell"] 17 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/Volume/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosSlaveInfo/URI/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ci/install_mesos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x -e -o pipefail 3 | 4 | MESOS_VERSION=$1 5 | 6 | DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]') 7 | CODENAME=$(lsb_release -cs) 8 | 9 | # Add Mesosphere repo to the list 10 | apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E56151BF 11 | echo "deb http://repos.mesosphere.io/${DISTRO} ${CODENAME} main" | \ 12 | sudo tee /etc/apt/sources.list.d/mesosphere.list 13 | 14 | # Some keys are missing from time to time - add them manually 15 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 16 | apt-get -y update 17 | 18 | # Install Mesos 19 | apt-get -y install mesos="$MESOS_VERSION-2.0.1.ubuntu1604" 20 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/config/models/faultdomain/DomainFilterModel.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.config.models.faultdomain; 2 | 3 | import com.mesosphere.usi.core.models.faultdomain.DomainFilter; 4 | import hudson.model.AbstractDescribableImpl; 5 | 6 | /** 7 | * A simple config model that enables a hetero descriptor list. See {@link StringDomainFilter}, 8 | * {@link Any}, {@link Home} and the usage in MesosAgentSpecTemplate/config.jelly for usage. 9 | * 10 | * @see Introducing 12 | * Variability into Jenkins Plugins 13 | */ 14 | public abstract class DomainFilterModel extends AbstractDescribableImpl { 15 | public abstract DomainFilter getFilter(); 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/integration/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.integration; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import java.util.concurrent.TimeUnit; 8 | import org.junit.jupiter.api.Tag; 9 | import org.junit.jupiter.api.Timeout; 10 | 11 | /** 12 | * Tag integration tests and apply timeout to each test method. 13 | * 14 | *

The timeout can be overridden individually. 15 | * 16 | * @see org.junit.jupiter.api.Timeout 17 | */ 18 | @Target({ElementType.TYPE, ElementType.METHOD}) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | @Tag("integration") 21 | @Timeout(value = 3, unit = TimeUnit.MINUTES) 22 | public @interface IntegrationTest {} 23 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosRetentionStrategy.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import hudson.model.Descriptor; 4 | import hudson.slaves.CloudRetentionStrategy; 5 | import hudson.slaves.RetentionStrategy; 6 | 7 | /** A strategy to terminate idle {@link MesosComputer} */ 8 | public class MesosRetentionStrategy extends CloudRetentionStrategy { 9 | /** 10 | * Constructs a new {@link hudson.slaves.CloudRetentionStrategy}. 11 | * 12 | * @param idleMinutes The number of minutes to wait before calling getNode().terminate() on an 13 | * idle {@link MesosComputer} 14 | */ 15 | public MesosRetentionStrategy(int idleMinutes) { 16 | super(idleMinutes); 17 | } 18 | 19 | public static class DescriptorImpl extends Descriptor> { 20 | @Override 21 | public String getDisplayName() { 22 | return "Mesos Retention Strategy"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/config/models/faultdomain/Any.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.config.models.faultdomain; 2 | 3 | import com.mesosphere.usi.core.models.faultdomain.AnyDomain$; 4 | import com.mesosphere.usi.core.models.faultdomain.DomainFilter; 5 | import hudson.Extension; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | 8 | /** 9 | * This is just a wrapper around {@link com.mesosphere.usi.core.models.faultdomain.AnyDomain} since 10 | * the USI model does not implement {@link hudson.model.Describable}. 11 | */ 12 | public class Any extends DomainFilterModel { 13 | @DataBoundConstructor 14 | public Any() {} 15 | 16 | @Override 17 | public DomainFilter getFilter() { 18 | return AnyDomain$.MODULE$; 19 | } 20 | 21 | @Extension 22 | public static class DescriptorImpl extends DomainFilterModelDescriptor { 23 | public String getDisplayName() { 24 | return "Any Domain"; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/config/models/faultdomain/Home.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.config.models.faultdomain; 2 | 3 | import com.mesosphere.usi.core.models.faultdomain.DomainFilter; 4 | import com.mesosphere.usi.core.models.faultdomain.HomeRegionFilter$; 5 | import hudson.Extension; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | 8 | /** 9 | * This is just a wrapper around {@link com.mesosphere.usi.core.models.faultdomain.HomeRegionFilter} 10 | * since the USI model does not implement {@link hudson.model.Describable}. 11 | */ 12 | public class Home extends DomainFilterModel { 13 | @DataBoundConstructor 14 | public Home() {} 15 | 16 | @Override 17 | public DomainFilter getFilter() { 18 | return HomeRegionFilter$.MODULE$; 19 | } 20 | 21 | @Extension 22 | public static class DescriptorImpl extends DomainFilterModelDescriptor { 23 | public String getDisplayName() { 24 | return "Home Region"; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplateDescriptorTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import hudson.util.FormValidation.Kind; 7 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate.DescriptorImpl; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | 11 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 12 | public class MesosAgentSpecTemplateDescriptorTest { 13 | 14 | @Test 15 | public void validateCpus(TestUtils.JenkinsRule j) { 16 | MesosAgentSpecTemplate.DescriptorImpl descriptor = new DescriptorImpl(); 17 | assertThat(descriptor.doCheckCpus("0.0").kind, is(Kind.ERROR)); 18 | assertThat(descriptor.doCheckCpus("0").kind, is(Kind.ERROR)); 19 | assertThat(descriptor.doCheckCpus("-0.1").kind, is(Kind.ERROR)); 20 | assertThat(descriptor.doCheckCpus("0.1").kind, is(Kind.OK)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ci/provision.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x -e -o pipefail 3 | 4 | MESOS_VERSION=$1 5 | 6 | 7 | DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]') 8 | CODENAME=$(lsb_release -cs) 9 | 10 | # Add Mesosphere repo to the list 11 | apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E56151BF 12 | echo "deb http://repos.mesosphere.io/${DISTRO} ${CODENAME} main" | \ 13 | sudo tee /etc/apt/sources.list.d/mesosphere.list 14 | 15 | # Some keys are missing from time to time - add them manually 16 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 17 | apt-get -y update 18 | 19 | # Install Mesos 20 | apt-get -y install mesos="$MESOS_VERSION-2.0.3" java-common 21 | mesos master --version 22 | mesos agent --version 23 | 24 | # Install Corretto JDK 11 25 | wget -q https://d3pxv6yz143wms.cloudfront.net/11.0.2.9.3/java-11-amazon-corretto-jdk_11.0.2.9-3_amd64.deb 26 | dpkg --install java-11-amazon-corretto-jdk_11.0.2.9-3_amd64.deb 27 | update-alternatives --config java 28 | update-alternatives --config javac 29 | java -version 30 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MesosSlaveInfoTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.equalTo; 5 | import static org.hamcrest.Matchers.is; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class MesosSlaveInfoTest { 10 | 11 | @Test 12 | void migrateSingleAgentAttribute() { 13 | MesosSlaveInfo info = new MesosSlaveInfo(); 14 | final String oldAttributes = "{\"os\":\"linux\"}"; 15 | 16 | final String result = info.migrateAttributeFilter(info.parseSlaveAttributes(oldAttributes)); 17 | 18 | assertThat(result, is(equalTo("os:linux"))); 19 | } 20 | 21 | @Test 22 | void migrateMultipleAgentAttributes() { 23 | MesosSlaveInfo info = new MesosSlaveInfo(); 24 | final String oldAttributes = "{\"os\":\"linux\", \"foo\":\"bar\"}"; 25 | 26 | final String result = info.migrateAttributeFilter(info.parseSlaveAttributes(oldAttributes)); 27 | 28 | assertThat(result, is(equalTo("os:linux,foo:bar"))); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/configuration.yaml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | clouds: 3 | - mesos: 4 | agentUser: "${JENKINS_AGENT_USER:-root}" 5 | frameworkName: "${JENKINS_FRAMEWORK_NAME:-Jenkins Scheduler}" 6 | jenkinsURL: "http://${JENKINS_FRAMEWORK_NAME:-jenkins}.${MARATHON_NAME:-marathon}.mesos:${PORT0:-8080}" 7 | mesosAgentSpecTemplates: 8 | - label: "linux" 9 | agentAttributes: "" 10 | agentCommandStyle: Linux 11 | containerInfo: 12 | dockerForcePullImage: false 13 | dockerImage: "amazoncorretto:8" 14 | dockerPrivilegedMode: false 15 | isDind: false 16 | networking: HOST 17 | type: "DOCKER" 18 | cpus: "0.1" 19 | disk: "0.0" 20 | domainFilterModel: "home" 21 | idleTerminationMinutes: 3 22 | jnlpArgs: "-noReconnect" 23 | maxExecutors: 1 24 | mem: "512" 25 | minExecutors: 1 26 | mode: EXCLUSIVE 27 | mesosMasterUrl: "${JENKINS_MESOS_MASTER:-http://leader.mesos:5050}" 28 | role: "${JENKINS_AGENT_ROLE:-*}" 29 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | mesos-client { 2 | 3 | master-url: "http://127.0.0.1:5050" 4 | 5 | redirect-retries: 3 6 | 7 | idle-timeout: "75 seconds" 8 | 9 | back-pressure { 10 | source-buffer-size: 10 11 | } 12 | } 13 | akka { 14 | loggers = ["akka.event.slf4j.Slf4jLogger"] 15 | 16 | loglevel = "INFO" 17 | 18 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 19 | 20 | http.client { 21 | 22 | # The time period within which the TCP connecting process must be completed. 23 | connecting-timeout = 10s 24 | 25 | # The time after which an idle connection will be automatically closed. 26 | # Set to `infinite` to completely disable idle timeouts. 27 | idle-timeout = infinite 28 | } 29 | } 30 | 31 | usi { 32 | # Operational Jenkins configuration. 33 | jenkins { 34 | agent-timeout: "5 minutes" 35 | command-queue-buffer-size: 256 36 | failover-timeout: "7 days" 37 | 38 | # Number of times Jenkins will try to reconnect to Mesos via USI 39 | connection-retries: 5 40 | 41 | # Backoffs times Jenkins will use to reconnect to Mesos via USI 42 | connection-min-backoff: 1s 43 | connection-max-backoff: 30s 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosPodRecordRepository.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import akka.Done; 4 | import com.mesosphere.usi.repository.PodRecordRepository; 5 | import java.util.concurrent.CompletableFuture; 6 | import scala.collection.immutable.HashMap; 7 | import scala.collection.immutable.Map; 8 | import scala.compat.java8.FutureConverters; 9 | import scala.concurrent.Future; 10 | 11 | public class MesosPodRecordRepository implements PodRecordRepository { 12 | @Override 13 | public Future delete(Object record) { 14 | CompletableFuture javaFuture = CompletableFuture.supplyAsync(() -> Done.done()); 15 | return FutureConverters.toScala((javaFuture)); 16 | } 17 | 18 | @Override 19 | public Future store(Object record) { 20 | CompletableFuture javaFuture = CompletableFuture.supplyAsync(() -> Done.done()); 21 | return FutureConverters.toScala((javaFuture)); 22 | } 23 | 24 | @Override 25 | public Future> readAll() { 26 | Map map = new HashMap<>(); 27 | CompletableFuture> javaFuture = CompletableFuture.supplyAsync(() -> map); 28 | return FutureConverters.toScala((javaFuture)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosCloud/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /dcos-testing/README.md: -------------------------------------------------------------------------------- 1 | # Running on DC/OS 2 | 3 | This folder includes a Docker file and a [Marathon](https://mesosphere.github.io/marathon/) app 4 | definition which can be used to run Jenkins with this plugin on [DC/OS](https://dcos.io/). It should 5 | only be used for testing. Please use the DC/OS universe Jenkins package for production. 6 | 7 | ### Testing On DC/OS Enterprise 8 | 9 | The `Dockerfile` defines a Docker image that supports DC/OS strict mode. It should be publishes as 10 | `mesosphere/jenkins:unstalbe`. It requires a service account to run. To setup one up with the DC/OS CLI 11 | 12 | 1. Create service account secrets with 13 | ``` 14 | dcos security org service-accounts keypair jenkins.private.pem jenkins.pub.pem 15 | ``` 16 | 2. Create the actual service account called `jenkins` 17 | ``` 18 | dcos security org service-accounts create -p jenkins.pub.pem -d "Jenkins Service Account" jenkins 19 | ``` 20 | 3. Store private key as secret so that the Jenkins controller can access it 21 | ``` 22 | dcos security secrets create -f ./jenkins.private.pem jenkins/private_key 23 | ``` 24 | 4. Grant `jenkins` service account rights to start Mesos tasks: 25 | ``` 26 | dcos security org users grant jenkins dcos:mesos:master:task:user:nobody create 27 | dcos security org users grant jenkins "dcos:mesos:master:framework:role:*" read 28 | dcos security org users grant jenkins "dcos:mesos:master:framework:role:*" create 29 | ``` 30 | 5. Deploy the Jenkins app defined in `./jenkins-app.json` 31 | ``` 32 | dcos marathon app add jenkins-app.json 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-16.04 13 | timeout-minutes: 30 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | - name: Cache Gradle packages 24 | uses: actions/cache@v1 25 | with: 26 | path: ~/.gradle/caches 27 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 28 | restore-keys: ${{ runner.os }}-gradle 29 | - name: Install Docker 30 | run: sudo ./ci/install_docker.sh 31 | - name: Install Mesos 32 | run: sudo ./ci/install_mesos.sh 1.9.0 33 | - name: Set Port Range 34 | run: sudo ./ci/set_port_range.sh 35 | - name: Build and Unit Test 36 | run: ./gradlew clean check javadoc -x integrationTest 37 | - name: Test Integration 38 | run: sudo ./gradlew integrationTest 39 | - name: Archive Test Results 40 | if: failure() 41 | uses: actions/upload-artifact@v1 42 | with: 43 | name: JUnit 44 | path: build/test-results 45 | - name: Create Sandboxes Archive 46 | if: always() 47 | run: ./gradlew zipSandboxes --info && sudo rm -rf sandboxes && ls build/distributions 48 | - name: Archive Sandboxes 49 | if: always() 50 | uses: actions/upload-artifact@v1 51 | with: 52 | name: Sandboxes 53 | path: build/distributions/sandboxes-undefined.zip 54 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/fixture/AgentSpecMother.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.fixture; 2 | 3 | import hudson.model.Node.Mode; 4 | import java.util.Collections; 5 | import org.apache.mesos.v1.Protos.ContainerInfo.DockerInfo.Network; 6 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate; 7 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate.ContainerInfo; 8 | 9 | /** 10 | * A Mother object for {@link org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate}. 11 | * 12 | * @see ObjectMother 13 | */ 14 | public class AgentSpecMother { 15 | 16 | public static final MesosAgentSpecTemplate simple = 17 | new MesosAgentSpecTemplate( 18 | "label", 19 | Mode.EXCLUSIVE, 20 | "0.1", 21 | "32", 22 | 1, 23 | 1, 24 | 1, 25 | "0", 26 | "", 27 | "", 28 | Collections.emptyList(), 29 | null, 30 | null, 31 | null); 32 | 33 | public static final MesosAgentSpecTemplate docker = 34 | new MesosAgentSpecTemplate( 35 | "label", 36 | Mode.EXCLUSIVE, 37 | "0.5", 38 | "512", 39 | 3, 40 | 1, 41 | 1, 42 | "1", 43 | "", 44 | "", 45 | Collections.emptyList(), 46 | new ContainerInfo( 47 | "DOCKER", 48 | "jeschkies/jenkins-simple-agent:testing", 49 | false, 50 | false, 51 | true, 52 | Collections.emptyList(), 53 | Network.HOST), 54 | null, 55 | null); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/config/models/faultdomain/StringDomainFilter.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.config.models.faultdomain; 2 | 3 | import com.mesosphere.usi.core.models.faultdomain.DomainFilter; 4 | import hudson.Extension; 5 | import org.apache.mesos.v1.Protos.DomainInfo; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | 8 | /** This is a simple {@link DomainFilter} that matches the agent region and zone with a string. */ 9 | public class StringDomainFilter extends DomainFilterModel implements DomainFilter { 10 | 11 | private final String region; 12 | private final String zone; 13 | 14 | @DataBoundConstructor 15 | public StringDomainFilter(String region, String zone) { 16 | this.region = region; 17 | this.zone = zone; 18 | } 19 | 20 | @Override 21 | public DomainFilter getFilter() { 22 | return this; 23 | } 24 | 25 | @Override 26 | public String description() { 27 | return String.format("accept %s region and %s zone", region, zone); 28 | } 29 | 30 | /** 31 | * Application of the domain filter. 32 | * 33 | * @param masterDomain The domain info of the master. 34 | * @param nodeDomain The domain info of and offer. 35 | * @return true if the region and zone is equal to the provided region and zone, false otherwise. 36 | */ 37 | @Override 38 | public boolean apply(DomainInfo masterDomain, DomainInfo nodeDomain) { 39 | return this.region.equals(nodeDomain.getFaultDomain().getRegion().getName()) 40 | && this.zone.equals(nodeDomain.getFaultDomain().getZone().getName()); 41 | } 42 | 43 | @Extension 44 | public static final class DescriptorImpl extends DomainFilterModelDescriptor { 45 | 46 | public String getDisplayName() { 47 | return "String Matching"; 48 | } 49 | } 50 | 51 | public String getRegion() { 52 | return this.region; 53 | } 54 | 55 | public String getZone() { 56 | return this.zone; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/Metrics.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import com.mesosphere.usi.metrics.dropwizard.conf.HistorgramSettings; 4 | import com.mesosphere.usi.metrics.dropwizard.conf.MetricsSettings; 5 | import java.util.HashMap; 6 | import javax.annotation.Nonnull; 7 | import scala.Option; 8 | 9 | public class Metrics { 10 | 11 | @Nonnull 12 | private static HashMap metrics = new HashMap<>(); 13 | 14 | /** 15 | * The USI metrics is a singleton per framework name. 16 | * 17 | *

A singleton is required because we use in in validation code in {@link MesosCloud} and in 18 | * {@link MesosApi} instances. 19 | * 20 | * @param frameworkName The name of the framework that is used as a prefix. 21 | * @return The Metrics implementation for the framework. 22 | */ 23 | public static synchronized com.mesosphere.usi.metrics.Metrics getInstance(String frameworkName) { 24 | final String prefix = sanitize(frameworkName); 25 | if (!metrics.containsKey(prefix)) { 26 | // Construct metrics 27 | MetricsSettings metricsSettings = 28 | new MetricsSettings( 29 | sanitize(frameworkName), 30 | HistorgramSettings.apply( 31 | HistorgramSettings.apply$default$1(), 32 | HistorgramSettings.apply$default$2(), 33 | HistorgramSettings.apply$default$3(), 34 | HistorgramSettings.apply$default$4(), 35 | HistorgramSettings.apply$default$5()), 36 | Option.empty(), 37 | Option.empty()); 38 | metrics.put( 39 | prefix, 40 | new com.mesosphere.usi.metrics.dropwizard.DropwizardMetrics( 41 | metricsSettings, jenkins.metrics.api.Metrics.metricRegistry())); 42 | } 43 | 44 | return metrics.get(prefix); 45 | } 46 | 47 | /** @return a santized prefix for Dropwizard metrics. */ 48 | public static String sanitize(String prefix) { 49 | return prefix.replaceAll("[^a-zA-Z0-9\\-\\.]", "-"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/api/LaunchCommandBuilderTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.containsString; 5 | import static org.hamcrest.Matchers.not; 6 | 7 | import akka.actor.ActorSystem; 8 | import akka.stream.ActorMaterializer; 9 | import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; 10 | import hudson.security.HudsonPrivateSecurityRealm; 11 | import jenkins.model.Jenkins; 12 | import org.jenkinsci.plugins.mesos.TestUtils; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | 16 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 17 | public class LaunchCommandBuilderTest { 18 | 19 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 20 | static ActorMaterializer materializer = ActorMaterializer.create(system); 21 | 22 | @Test 23 | public void testJnlpAgentCommandContainsSecret(TestUtils.JenkinsRule j) throws Exception { 24 | final String name = "jenkins-jnlp-security"; 25 | LaunchCommandBuilder builder = new LaunchCommandBuilder().withName(name); 26 | 27 | // before enabling security shell command contains no secret param 28 | assertThat(builder.buildJnlpSecret(), not(containsString("-secret"))); 29 | 30 | // This test requires a running Jenkins instances *and* at least one node. 31 | Jenkins instance = j.getInstance(); 32 | if (instance == null) { 33 | throw new IllegalStateException("Jenkins is null"); 34 | } 35 | HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); 36 | instance.setSecurityRealm(realm); 37 | FullControlOnceLoggedInAuthorizationStrategy strategy = 38 | new hudson.security.FullControlOnceLoggedInAuthorizationStrategy(); 39 | 40 | strategy.setAllowAnonymousRead(false); 41 | instance.setAuthorizationStrategy(strategy); 42 | instance.save(); 43 | 44 | // after enabling security shell command contains secret 45 | assertThat(builder.buildJnlpSecret(), containsString("-secret")); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /dcos-testing/conf/jenkins/configuration.yaml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | agentProtocols: 3 | - "JNLP4-connect" 4 | - "Ping" 5 | numExecutors: 0 6 | clouds: 7 | - mesos: 8 | agentUser: "${JENKINS_AGENT_USER:-root}" 9 | frameworkName: "${JENKINS_FRAMEWORK_NAME:-Jenkins Scheduler}" 10 | jenkinsURL: "http://${JENKINS_FRAMEWORK_NAME:-jenkins}.${MARATHON_NAME:-marathon}.mesos:${PORT0:-8080}" 11 | mesosAgentSpecTemplates: 12 | - label: "linux" 13 | agentAttributes: "" 14 | agentCommandStyle: Linux 15 | containerInfo: 16 | dockerForcePullImage: false 17 | dockerImage: "mesosphere/jenkins-dind:0.8.0" 18 | dockerPrivilegedMode: true 19 | isDind: true 20 | networking: HOST 21 | type: "DOCKER" 22 | cpus: "0.1" 23 | disk: "0.0" 24 | domainFilterModel: "home" 25 | idleTerminationMinutes: 3 26 | jnlpArgs: "-noReconnect" 27 | maxExecutors: 1 28 | mem: "512" 29 | minExecutors: 1 30 | mode: EXCLUSIVE 31 | - label: "windows" 32 | agentAttributes: "os:windows" 33 | agentCommandStyle: Windows 34 | containerInfo: 35 | dockerForcePullImage: false 36 | dockerImage: "mesosphere/jenkins-windows-node:latest" 37 | dockerPrivilegedMode: false 38 | isDind: false 39 | networking: BRIDGE 40 | type: "DOCKER" 41 | cpus: "1.0" 42 | disk: "0.0" 43 | domainFilterModel: "any" 44 | idleTerminationMinutes: 3 45 | jnlpArgs: "-noReconnect" 46 | maxExecutors: 1 47 | mem: "4096" 48 | minExecutors: 1 49 | mode: EXCLUSIVE 50 | mesosMasterUrl: "${JENKINS_MESOS_MASTER:-http://leader.mesos:5050}" 51 | role: "${JENKINS_AGENT_ROLE:-*}" 52 | unclassified: 53 | location: 54 | adminAddress: "address not configured yet " 55 | url: "http://jenkins.marathon.mesos:${PORT0:-8080}/" 56 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - security 16 | 17 | # Set to true to ignore issues in a project (defaults to false) 18 | exemptProjects: false 19 | 20 | # Set to true to ignore issues in a milestone (defaults to false) 21 | exemptMilestones: false 22 | 23 | # Set to true to ignore issues with an assignee (defaults to false) 24 | exemptAssignees: false 25 | 26 | # Label to use when marking as stale 27 | staleLabel: stale 28 | 29 | # Comment to post when marking as stale. Set to `false` to disable 30 | markComment: > 31 | This issue has been automatically marked as stale because it has not had 32 | recent activity. It will be closed if no further activity occurs. Thank you 33 | for your contributions. 34 | 35 | # Comment to post when removing the stale label. 36 | # unmarkComment: > 37 | # Your comment here. 38 | 39 | # Comment to post when closing a stale Issue or Pull Request. 40 | # closeComment: > 41 | # Your comment here. 42 | 43 | # Limit the number of actions per hour, from 1-30. Default is 30 44 | limitPerRun: 30 45 | 46 | # Limit to only `issues` or `pulls` 47 | # only: issues 48 | 49 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 50 | # pulls: 51 | # daysUntilStale: 30 52 | # markComment: > 53 | # This pull request has been automatically marked as stale because it has not had 54 | # recent activity. It will be closed if no further activity occurs. Thank you 55 | # for your contributions. 56 | 57 | # issues: 58 | # exemptLabels: 59 | # - confirmed 60 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/ContainerInfo/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ${it.toString()} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

33 |
34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /dcos-testing/jenkins-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/jenkins", 3 | "cmd": null, 4 | "cpus": 1, 5 | "mem": 2048, 6 | "disk": 0, 7 | "instances": 1, 8 | "acceptedResourceRoles": [ 9 | "*" 10 | ], 11 | "constraints": [ 12 | [ 13 | "os", 14 | "UNLIKE", 15 | "windows" 16 | ] 17 | ], 18 | "container": { 19 | "type": "DOCKER", 20 | "docker": { 21 | "forcePullImage": true, 22 | "image": "mesosphere/jenkins:unstable", 23 | "parameters": [], 24 | "privileged": false 25 | }, 26 | "volumes": [ 27 | { 28 | "containerPath": "/var/jenkins_home", 29 | "hostPath": "/tmp/jenkins", 30 | "mode": "RW" 31 | } 32 | ] 33 | }, 34 | "secrets": { 35 | "private_key": { 36 | "source": "jenkins/private_key" 37 | } 38 | }, 39 | "env": { 40 | "SSH_KNOWN_HOSTS": "github.com", 41 | "JENKINS_CONTEXT": "/service/jenkins", 42 | "JENKINS_SLAVE_AGENT_PORT": "50000", 43 | "JENKINS_AGENT_ROLE": "*", 44 | "JVM_OPTS": "-Xms1024m -Xmx1024m", 45 | "JENKINS_AGENT_USER": "nobody", 46 | "JENKINS_OPTS": "", 47 | "JENKINS_FRAMEWORK_NAME": "jenkins", 48 | "JENKINS_MESOS_MASTER": "https://leader.mesos:5050", 49 | "MARATHON_NAME": "marathon", 50 | "DCOS_SERVICE_ACCOUNT": "jenkins", 51 | "DCOS_SERVICE_ACCOUNT_PRIVATE_KEY": { 52 | "secret": "private_key" 53 | } 54 | }, 55 | "labels": { 56 | "DCOS_SERVICE_SCHEME": "http", 57 | "DCOS_SERVICE_NAME": "jenkins", 58 | "DCOS_SERVICE_PORT_INDEX": "0", 59 | "MARATHON_SINGLE_INSTANCE_APP": "true" 60 | }, 61 | "portDefinitions": [ 62 | { 63 | "port": 10000, 64 | "name": "nginx", 65 | "protocol": "tcp" 66 | }, 67 | { 68 | "port": 10001, 69 | "name": "jenkins", 70 | "protocol": "tcp" 71 | } 72 | ], 73 | "maxLaunchDelaySeconds": 300, 74 | "upgradeStrategy": { 75 | "maximumOverCapacity": 0, 76 | "minimumHealthCapacity": 0 77 | }, 78 | "healthChecks": [ 79 | { 80 | "path": "/service/jenkins", 81 | "portIndex": 0, 82 | "protocol": "MESOS_HTTP", 83 | "gracePeriodSeconds": 60, 84 | "intervalSeconds": 60, 85 | "timeoutSeconds": 20, 86 | "maxConsecutiveFailures": 3 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /dcos-testing/conf/nginx/nginx.conf.template: -------------------------------------------------------------------------------- 1 | error_log stderr; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | client_max_body_size 1024M; 9 | server { 10 | listen $PORT0 default_server; 11 | 12 | access_log /var/log/nginx/jenkins/access.log; 13 | error_log /var/log/nginx/jenkins/error.log debug; 14 | 15 | location ^~ $JENKINS_CONTEXT { 16 | proxy_pass http://127.0.0.1:$PORT1; 17 | proxy_set_header Host $http_host; 18 | proxy_set_header X-Real-IP $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | proxy_max_temp_file_size 0; 21 | 22 | # Based on https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+behind+an+NGinX+reverse+proxy 23 | client_body_buffer_size 128k; 24 | 25 | proxy_connect_timeout 600; 26 | proxy_send_timeout 600; 27 | proxy_read_timeout 600; 28 | send_timeout 600; 29 | 30 | proxy_buffer_size 4k; 31 | proxy_buffers 4 32k; 32 | proxy_busy_buffers_size 64k; 33 | proxy_temp_file_write_size 64k; 34 | } 35 | 36 | location ~ ^/(?.*)$ { 37 | rewrite ^/(?.*)$ $JENKINS_CONTEXT/$url break; 38 | proxy_pass http://127.0.0.1:$PORT1; 39 | proxy_set_header Host $http_host; 40 | proxy_set_header X-Real-IP $remote_addr; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_max_temp_file_size 0; 43 | 44 | # Based on https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+behind+an+NGinX+reverse+proxy 45 | client_body_buffer_size 128k; 46 | 47 | proxy_connect_timeout 600; 48 | proxy_send_timeout 600; 49 | proxy_read_timeout 600; 50 | send_timeout 600; 51 | 52 | proxy_buffer_size 4k; 53 | proxy_buffers 4 32k; 54 | proxy_busy_buffers_size 64k; 55 | proxy_temp_file_write_size 64k; 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/TestUtils.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import java.util.Optional; 4 | import org.junit.jupiter.api.extension.AfterEachCallback; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | import org.junit.jupiter.api.extension.ParameterContext; 7 | import org.junit.jupiter.api.extension.ParameterResolutionException; 8 | import org.junit.jupiter.api.extension.ParameterResolver; 9 | import org.jvnet.hudson.test.JenkinsRecipe; 10 | 11 | // Copied from https://issues.jenkins-ci.org/browse/JENKINS-48466 12 | public class TestUtils { 13 | public static class JenkinsRule extends org.jvnet.hudson.test.JenkinsRule { 14 | private final ParameterContext context; 15 | 16 | JenkinsRule(ParameterContext context) { 17 | this.context = context; 18 | } 19 | 20 | @Override 21 | public void recipe() throws Exception { 22 | Optional a = context.findAnnotation(JenkinsRecipe.class); 23 | if (!a.isPresent()) return; 24 | final JenkinsRecipe.Runner runner = a.get().value().newInstance(); 25 | recipes.add(runner); 26 | tearDowns.add(() -> runner.tearDown(this, a.get())); 27 | } 28 | } 29 | 30 | public static class JenkinsParameterResolver implements ParameterResolver, AfterEachCallback { 31 | private static final String key = "jenkins-instance"; 32 | private static final ExtensionContext.Namespace ns = 33 | ExtensionContext.Namespace.create(JenkinsParameterResolver.class); 34 | 35 | @Override 36 | public boolean supportsParameter( 37 | ParameterContext parameterContext, ExtensionContext extensionContext) 38 | throws ParameterResolutionException { 39 | return parameterContext.getParameter().getType().equals(JenkinsRule.class); 40 | } 41 | 42 | @Override 43 | public Object resolveParameter( 44 | ParameterContext parameterContext, ExtensionContext extensionContext) 45 | throws ParameterResolutionException { 46 | JenkinsRule instance = 47 | extensionContext 48 | .getStore(ns) 49 | .getOrComputeIfAbsent( 50 | key, key -> new JenkinsRule(parameterContext), JenkinsRule.class); 51 | try { 52 | instance.before(); 53 | return instance; 54 | } catch (Throwable t) { 55 | throw new ParameterResolutionException(t.toString()); 56 | } 57 | } 58 | 59 | @Override 60 | public void afterEach(ExtensionContext context) throws Exception { 61 | JenkinsRule rule = context.getStore(ns).remove(key, JenkinsRule.class); 62 | if (rule != null) rule.after(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosComputer.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import hudson.model.Executor; 4 | import hudson.model.Queue; 5 | import hudson.slaves.AbstractCloudComputer; 6 | import java.io.IOException; 7 | import org.kohsuke.stapler.HttpRedirect; 8 | import org.kohsuke.stapler.HttpResponse; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** 13 | * The running state of a {@link hudson.model.Node} or rather {@link MesosJenkinsAgent} in our case. 14 | */ 15 | public class MesosComputer extends AbstractCloudComputer { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(MesosComputer.class); 18 | 19 | private final boolean reusable; 20 | private final String podId; 21 | 22 | /** 23 | * Constructs a new computer. This is called by {@link MesosJenkinsAgent#createComputer()}. 24 | * 25 | * @param agent The {@link hudson.model.Node} this computer belongs to. 26 | */ 27 | public MesosComputer(MesosJenkinsAgent agent) { 28 | super(agent); 29 | this.reusable = agent.getReusable(); 30 | this.podId = agent.getPodId(); 31 | } 32 | 33 | @Override 34 | public void taskAccepted(Executor executor, Queue.Task task) { 35 | super.taskAccepted(executor, task); 36 | if (!reusable) { 37 | // single use computer will only accept one task, after completing task it will go idle and be 38 | // killed by MesosRetentionStrategy 39 | logger.info("Computer {}: is no longer accepting tasks and was marked as single-use", this); 40 | setAcceptingTasks(false); 41 | } 42 | logger.info("Computer {}: task accepted", this); 43 | } 44 | 45 | @Override 46 | public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { 47 | super.taskCompleted(executor, task, durationMS); 48 | logger.info("Computer {}: task completed", this); 49 | } 50 | 51 | @Override 52 | public void taskCompletedWithProblems( 53 | Executor executor, Queue.Task task, long durationMS, Throwable problems) { 54 | super.taskCompletedWithProblems(executor, task, durationMS, problems); 55 | logger.warn("Computer {} task completed with problems", this); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return String.format("%s (slave: %s)", getName(), getNode()); 61 | } 62 | 63 | @Override 64 | public MesosJenkinsAgent getNode() { 65 | return super.getNode(); 66 | } 67 | 68 | @Override 69 | public HttpResponse doDoDelete() throws IOException { 70 | try { 71 | final MesosJenkinsAgent agent = getNode(); 72 | if (agent != null) { 73 | agent.terminate(); 74 | } 75 | } catch (InterruptedException e) { 76 | logger.warn("Failure to terminate agent {}", podId, e); 77 | } 78 | return new HttpRedirect(".."); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ${it.toString()} 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |
67 | 68 |
-------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage: Build 2 | FROM gradle:5.4.1-jdk8 AS build 3 | ADD . /home/gradle/project 4 | WORKDIR /home/gradle/project 5 | RUN gradle jpi 6 | 7 | 8 | # Stage: Prod 9 | FROM jenkins/jenkins:2.222.3 as prod 10 | WORKDIR /tmp 11 | 12 | # Environment variables used throughout this Dockerfile 13 | # 14 | # $JENKINS_HOME will be the final destination that Jenkins will use as its 15 | # data directory. This cannot be populated before Marathon 16 | # has a chance to create the host-container volume mapping. 17 | # 18 | ENV JENKINS_FOLDER /usr/share/jenkins 19 | 20 | # Build Args 21 | ARG JENKINS_STAGING=/usr/share/jenkins/ref/ 22 | 23 | # Default policy according to https://wiki.jenkins.io/display/JENKINS/Configuring+Content+Security+Policy 24 | ENV JENKINS_CSP_OPTS="sandbox; default-src 'none'; img-src 'self'; style-src 'self';" 25 | 26 | USER root 27 | 28 | # install dependencies 29 | RUN apt-get update && apt-get install -y nginx python zip jq gettext-base 30 | # update to newer git version 31 | RUN echo "deb http://ftp.debian.org/debian testing main" >> /etc/apt/sources.list \ 32 | && apt-get update && apt-get -t testing install -y git 33 | 34 | # Override the default property for DNS lookup caching 35 | RUN echo 'networkaddress.cache.ttl=60' >> ${JAVA_HOME}/jre/lib/security/java.security 36 | 37 | # Create needed dir 38 | RUN mkdir -p "$JENKINS_HOME" "${JENKINS_FOLDER}/war" 39 | 40 | # Nginx setup 41 | RUN mkdir -p /var/log/nginx/jenkins 42 | COPY dcos-testing/conf/nginx/nginx.conf.template /etc/nginx/nginx.conf.template 43 | 44 | # Jenkins setup and configuration. 45 | ENV CASC_JENKINS_CONFIG /usr/local/jenkins/jenkins.yaml 46 | COPY dcos-testing/conf/jenkins/configuration.yaml "${CASC_JENKINS_CONFIG}" 47 | 48 | # Add plugins 49 | COPY dcos-testing/conf/plugins.conf /tmp/ 50 | RUN /usr/local/bin/install-plugins.sh < /tmp/plugins.conf 51 | 52 | # Add Mesos plugin 53 | COPY --from=build /home/gradle/project/build/libs/mesos.hpi "${JENKINS_STAGING}/plugins/mesos.hpi" 54 | 55 | # Disable first-run wizard 56 | RUN echo 2.0 > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state 57 | 58 | CMD envsubst '\$PORT0 \$PORT1 \$JENKINS_CONTEXT' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx \ 59 | && java ${JVM_OPTS} \ 60 | -Dhudson.model.DirectoryBrowserSupport.CSP="${JENKINS_CSP_OPTS}" \ 61 | -Dhudson.udp=-1 \ 62 | -Djava.awt.headless=true \ 63 | -Dhudson.DNSMultiCast.disabled=true \ 64 | -Djenkins.install.runSetupWizard=false \ 65 | -jar ${JENKINS_FOLDER}/jenkins.war \ 66 | ${JENKINS_OPTS} \ 67 | --httpPort=${PORT1} \ 68 | --webroot=${JENKINS_FOLDER}/war \ 69 | --ajp13Port=-1 \ 70 | --httpListenAddress=127.0.0.1 \ 71 | --ajp13ListenAddress=127.0.0.1 \ 72 | --prefix=${JENKINS_CONTEXT} 73 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MesosCloudTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.equalTo; 5 | import static org.hamcrest.Matchers.hasSize; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.hamcrest.Matchers.notNullValue; 8 | 9 | import hudson.util.XStream2; 10 | import io.jenkins.plugins.casc.ConfigurationAsCode; 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.Collections; 14 | import java.util.concurrent.ExecutionException; 15 | import org.apache.commons.io.IOUtils; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.ExtendWith; 18 | 19 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 20 | public class MesosCloudTest { 21 | 22 | @Test 23 | void deserializeOldConfig(TestUtils.JenkinsRule j) throws IOException { 24 | final String oldConfig = 25 | IOUtils.resourceToString( 26 | "config_1.x.xml", 27 | StandardCharsets.UTF_8, 28 | Thread.currentThread().getContextClassLoader()) 29 | // Master URL resolution requires a separate test. 30 | .replaceAll( 31 | ".*", 32 | String.format("%s", "http://localhost:5050")); 33 | 34 | final XStream2 xstream = new XStream2(); 35 | MesosCloud cloud = (MesosCloud) xstream.fromXML(oldConfig); 36 | 37 | assertThat(cloud.getMesosMasterUrl(), is("http://localhost:5050")); 38 | assertThat(cloud.getMesosAgentSpecTemplates(), hasSize(39)); 39 | cloud 40 | .getMesosAgentSpecTemplates() 41 | .forEach( 42 | template -> { 43 | assertThat(template.getCpus(), is(notNullValue())); 44 | }); 45 | } 46 | 47 | @Test 48 | void serializationRoundTrip(TestUtils.JenkinsRule j) 49 | throws IOException, InterruptedException, ExecutionException { 50 | final MesosCloud cloud = 51 | new MesosCloud( 52 | "http://localhost:5050", 53 | "jenkins-framework", 54 | null, 55 | "*", 56 | "root", 57 | j.getURL().toString(), 58 | Collections.emptyList()); 59 | 60 | final XStream2 xstream = new XStream2(); 61 | final MesosCloud reloadedCloud = (MesosCloud) xstream.fromXML(xstream.toXML(cloud)); 62 | 63 | assertThat(reloadedCloud.getMesosMasterUrl(), is(equalTo(cloud.getMesosMasterUrl()))); 64 | assertThat(reloadedCloud.getFrameworkName(), is(equalTo(cloud.getFrameworkName()))); 65 | assertThat(reloadedCloud.getFrameworkId(), is(equalTo(cloud.getFrameworkId()))); 66 | assertThat(reloadedCloud.getRole(), is(equalTo(cloud.getRole()))); 67 | assertThat(reloadedCloud.getAgentUser(), is(equalTo(cloud.getAgentUser()))); 68 | } 69 | 70 | @Test 71 | void configureAsCode(TestUtils.JenkinsRule j) throws IOException { 72 | final String config = 73 | IOUtils.resourceToURL("configuration.yaml", this.getClass().getClassLoader()) 74 | .toExternalForm(); 75 | ConfigurationAsCode.get().configure(config); 76 | 77 | assertThat(j.jenkins.clouds.getAll(MesosCloud.class), hasSize(1)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/integration/DockerAgentTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.integration; 2 | 3 | import static org.awaitility.Awaitility.await; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.is; 6 | 7 | import akka.actor.ActorSystem; 8 | import akka.stream.ActorMaterializer; 9 | import com.mesosphere.utils.mesos.MesosAgentConfig; 10 | import com.mesosphere.utils.mesos.MesosClusterExtension; 11 | import com.mesosphere.utils.zookeeper.ZookeeperServerExtension; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.concurrent.TimeUnit; 15 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate; 16 | import org.jenkinsci.plugins.mesos.MesosCloud; 17 | import org.jenkinsci.plugins.mesos.MesosJenkinsAgent; 18 | import org.jenkinsci.plugins.mesos.TestUtils; 19 | import org.jenkinsci.plugins.mesos.fixture.AgentSpecMother; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.condition.EnabledOnOs; 22 | import org.junit.jupiter.api.condition.OS; 23 | import org.junit.jupiter.api.extension.ExtendWith; 24 | import org.junit.jupiter.api.extension.RegisterExtension; 25 | import scala.Option; 26 | import scala.concurrent.duration.FiniteDuration; 27 | 28 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 29 | @IntegrationTest 30 | @EnabledOnOs(OS.LINUX) 31 | public class DockerAgentTest { 32 | 33 | @RegisterExtension static ZookeeperServerExtension zkServer = new ZookeeperServerExtension(); 34 | 35 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 36 | static ActorMaterializer materializer = ActorMaterializer.create(system); 37 | 38 | static MesosAgentConfig config = 39 | new MesosAgentConfig( 40 | "linux", 41 | "mesos,docker", 42 | Option.apply("filesystem/linux,docker/runtime"), 43 | Option.apply("docker"), 44 | Option.empty(), 45 | Option.empty(), 46 | new FiniteDuration(7, TimeUnit.MINUTES), 47 | new FiniteDuration(5, TimeUnit.MINUTES), 48 | false); 49 | 50 | @RegisterExtension 51 | static MesosClusterExtension mesosCluster = 52 | MesosClusterExtension.builder() 53 | .withMesosMasterUrl(String.format("zk://%s/mesos", zkServer.getConnectionUrl())) 54 | .withLogPrefix(DockerAgentTest.class.getCanonicalName()) 55 | .withAgentConfig(config) 56 | .build(system, materializer); 57 | 58 | @Test 59 | public void testJenkinsAgentWithDockerImage(TestUtils.JenkinsRule j) throws Exception { 60 | final MesosAgentSpecTemplate spec = AgentSpecMother.docker; 61 | List specTemplates = Collections.singletonList(spec); 62 | 63 | MesosCloud cloud = 64 | new MesosCloud( 65 | mesosCluster.getMesosUrl().toString(), 66 | "MesosTest", 67 | null, 68 | "*", 69 | System.getProperty("user.name"), 70 | j.getURL().toString(), 71 | specTemplates); 72 | 73 | final String name = "jenkins-docker-agent"; 74 | 75 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 76 | 77 | // verify agent is running when the future completes; 78 | await().atMost(5, TimeUnit.MINUTES).until(agent::isRunning); 79 | assertThat(agent.isRunning(), is(true)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MesosJenkinsAgentTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.equalTo; 5 | import static org.hamcrest.Matchers.instanceOf; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import akka.actor.ActorSystem; 10 | import akka.stream.ActorMaterializer; 11 | import com.mesosphere.usi.core.models.PodId; 12 | import com.mesosphere.usi.core.models.PodStatus; 13 | import com.mesosphere.usi.core.models.PodStatusUpdatedEvent; 14 | import com.mesosphere.usi.core.models.TaskId; 15 | import hudson.model.Descriptor; 16 | import hudson.model.Node; 17 | import java.io.IOException; 18 | import java.net.URL; 19 | import java.time.Duration; 20 | import java.util.Collections; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.ExecutionException; 23 | import org.apache.mesos.v1.Protos.TaskID; 24 | import org.apache.mesos.v1.Protos.TaskState; 25 | import org.apache.mesos.v1.Protos.TaskStatus; 26 | import org.jenkinsci.plugins.mesos.fixture.AgentSpecMother; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.extension.ExtendWith; 29 | import scala.Option; 30 | 31 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 32 | public class MesosJenkinsAgentTest { 33 | 34 | static ActorSystem system = ActorSystem.create("agent-test"); 35 | static ActorMaterializer materializer = ActorMaterializer.create(system); 36 | 37 | @Test 38 | void shortcircuitWaitUntilOnline(TestUtils.JenkinsRule j) 39 | throws Descriptor.FormException, IOException { 40 | // Given a Mesos Jenkins agent. 41 | final MesosJenkinsAgent agent = 42 | new MesosJenkinsAgent( 43 | null, 44 | "failed-agent", 45 | AgentSpecMother.simple, 46 | "A failing agent.", 47 | new URL("http://localhost:8080"), 48 | 5, 49 | false, 50 | Collections.emptyList(), 51 | Duration.ofMinutes(5)); 52 | 53 | // And we are waiting for it to come online. 54 | final CompletableFuture futureNode = agent.waitUntilOnlineAsync(materializer); 55 | 56 | // When the agent receives a fails task status event. 57 | PodId podId = new PodId("failed-agent"); 58 | TaskStatus taskStatus = 59 | TaskStatus.newBuilder() 60 | .setTaskId(TaskID.newBuilder().setValue("failed-agent-1234").build()) 61 | .setState(TaskState.TASK_FAILED) 62 | .setMessage("could not start agent.jar") 63 | .build(); 64 | scala.collection.immutable.Map taskStatusMap = 65 | new scala.collection.immutable.Map.Map1(new TaskId("failed-agent-1234"), taskStatus); 66 | PodStatus status = new PodStatus(podId, taskStatusMap); 67 | PodStatusUpdatedEvent failed = new PodStatusUpdatedEvent(podId, Option.apply(status)); 68 | agent.update(failed); 69 | 70 | // Then we finish immediately. 71 | ExecutionException exception = assertThrows(ExecutionException.class, () -> futureNode.get()); 72 | assertThat(exception.getCause(), is(instanceOf(IllegalStateException.class))); 73 | assertThat( 74 | exception.getCause().getMessage(), 75 | is(equalTo("Agent failed-agent became TASK_FAILED: could not start agent.jar"))); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/MesosCloudDescriptorTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import hudson.util.FormValidation.Kind; 7 | import java.io.IOException; 8 | import java.util.concurrent.ExecutionException; 9 | import org.jenkinsci.plugins.mesos.MesosCloud.DescriptorImpl; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | 13 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 14 | public class MesosCloudDescriptorTest { 15 | 16 | @Test 17 | void validateMesosMasterUrl(TestUtils.JenkinsRule j) { 18 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 19 | assertThat(descriptor.doCheckMesosMasterUrl("http/other").kind, is(Kind.ERROR)); 20 | assertThat( 21 | descriptor.doCheckMesosMasterUrl("zk://user@pass@localhost:5050").kind, is(Kind.ERROR)); 22 | assertThat(descriptor.doCheckMesosMasterUrl("zk://localhost:5050").kind, is(Kind.OK)); 23 | assertThat(descriptor.doCheckMesosMasterUrl("http://localhost:5050").kind, is(Kind.OK)); 24 | assertThat(descriptor.doCheckMesosMasterUrl("https://localhost:5050").kind, is(Kind.OK)); 25 | } 26 | 27 | @Test 28 | void validateFrameworkName(TestUtils.JenkinsRule j) { 29 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 30 | assertThat(descriptor.doCheckFrameworkName("").kind, is(Kind.ERROR)); 31 | assertThat(descriptor.doCheckFrameworkName("something").kind, is(Kind.OK)); 32 | } 33 | 34 | @Test 35 | void validateRole(TestUtils.JenkinsRule j) { 36 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 37 | assertThat(descriptor.doCheckRole("").kind, is(Kind.ERROR)); 38 | assertThat(descriptor.doCheckRole("-something").kind, is(Kind.ERROR)); 39 | assertThat(descriptor.doCheckRole(".").kind, is(Kind.ERROR)); 40 | assertThat(descriptor.doCheckRole("..").kind, is(Kind.ERROR)); 41 | assertThat(descriptor.doCheckRole(" ").kind, is(Kind.ERROR)); 42 | assertThat(descriptor.doCheckRole("/something").kind, is(Kind.ERROR)); 43 | assertThat(descriptor.doCheckRole("some/thing").kind, is(Kind.ERROR)); 44 | assertThat(descriptor.doCheckRole("\\something").kind, is(Kind.ERROR)); 45 | assertThat(descriptor.doCheckRole("some\\thing").kind, is(Kind.ERROR)); 46 | assertThat(descriptor.doCheckRole("something").kind, is(Kind.OK)); 47 | assertThat(descriptor.doCheckRole("some.thing.").kind, is(Kind.OK)); 48 | assertThat(descriptor.doCheckRole("some-thing").kind, is(Kind.OK)); 49 | assertThat(descriptor.doCheckRole("*").kind, is(Kind.OK)); 50 | } 51 | 52 | @Test 53 | void validateAgentUser(TestUtils.JenkinsRule j) { 54 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 55 | assertThat(descriptor.doCheckAgentUser("").kind, is(Kind.ERROR)); 56 | assertThat(descriptor.doCheckAgentUser("Invalid").kind, is(Kind.ERROR)); 57 | assertThat(descriptor.doCheckAgentUser("Inval$d").kind, is(Kind.ERROR)); 58 | assertThat(descriptor.doCheckAgentUser("something").kind, is(Kind.OK)); 59 | } 60 | 61 | @Test 62 | void validateJenkinsUrl(TestUtils.JenkinsRule j) { 63 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 64 | assertThat(descriptor.doCheckJenkinsUrl("http/other").kind, is(Kind.ERROR)); 65 | assertThat(descriptor.doCheckJenkinsUrl("zk://localhost:5050").kind, is(Kind.ERROR)); 66 | assertThat(descriptor.doCheckJenkinsUrl("http://localhost:5050").kind, is(Kind.OK)); 67 | assertThat(descriptor.doCheckJenkinsUrl("https://localhost:5050").kind, is(Kind.OK)); 68 | } 69 | 70 | @Test 71 | void connectionTest(TestUtils.JenkinsRule j) 72 | throws ExecutionException, InterruptedException, IOException { 73 | MesosCloud.DescriptorImpl descriptor = new DescriptorImpl(); 74 | assertThat(descriptor.doTestConnection("invalid url").kind, is(Kind.ERROR)); 75 | assertThat(descriptor.doTestConnection("http://unknownhost.foo").kind, is(Kind.ERROR)); 76 | assertThat(descriptor.doTestConnection(j.getURL().toString()).kind, is(Kind.OK)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/api/SessionTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import akka.NotUsed; 7 | import akka.actor.ActorSystem; 8 | import akka.stream.ActorMaterializer; 9 | import akka.stream.QueueOfferResult; 10 | import akka.stream.javadsl.Flow; 11 | import akka.stream.javadsl.SourceQueueWithComplete; 12 | import com.mesosphere.usi.core.models.PodId; 13 | import com.mesosphere.usi.core.models.StateEvent; 14 | import com.mesosphere.usi.core.models.commands.KillPod; 15 | import com.mesosphere.usi.core.models.commands.SchedulerCommand; 16 | import java.net.URL; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.CompletionStage; 19 | import org.jenkinsci.plugins.mesos.TestUtils; 20 | import org.jenkinsci.plugins.mesos.TestUtils.JenkinsParameterResolver; 21 | import org.jenkinsci.plugins.mesos.fixture.AgentSpecMother; 22 | import org.junit.jupiter.api.Test; 23 | import org.junit.jupiter.api.extension.ExtendWith; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | @ExtendWith(JenkinsParameterResolver.class) 28 | public class SessionTest { 29 | 30 | private static final Logger logger = LoggerFactory.getLogger(SessionTest.class); 31 | 32 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 33 | static ActorMaterializer materializer = ActorMaterializer.create(system); 34 | 35 | @Test 36 | public void testLaunchOverflow(TestUtils.JenkinsRule j) throws Exception { 37 | // Given a scheduler flow that never processes commands. 38 | final URL jenkinsUrl = new URL("https://jenkins.com"); 39 | Settings settings = Settings.load().withCommandQueueBufferSize(1); 40 | final CompletableFuture ignore = new CompletableFuture<>(); 41 | final Flow schedulerFlow = 42 | Flow.of(SchedulerCommand.class).mapAsync(1, command -> ignore); 43 | 44 | // And a running session. 45 | SourceQueueWithComplete sourceQueue = 46 | Session.runScheduler( 47 | settings, 48 | schedulerFlow, 49 | event -> logger.debug("Received event {}", event), 50 | materializer) 51 | .first(); 52 | Session session = new Session(sourceQueue); 53 | 54 | // And one agent is processed and one is queued. 55 | session 56 | .getCommands() 57 | .offer(AgentSpecMother.simple.buildLaunchCommand(jenkinsUrl, "agent1", "*")); 58 | session 59 | .getCommands() 60 | .offer(AgentSpecMother.simple.buildLaunchCommand(jenkinsUrl, "agent2", "*")); 61 | 62 | // When we enqueue a third agent 63 | CompletionStage result = 64 | session 65 | .getCommands() 66 | .offer(AgentSpecMother.simple.buildLaunchCommand(jenkinsUrl, "agent3", "*")); 67 | 68 | // Then backpressure hits us. 69 | QueueOfferResult offerFeedback = result.toCompletableFuture().get(); 70 | assertThat(offerFeedback, is(QueueOfferResult.dropped())); 71 | } 72 | 73 | @Test 74 | void testKillOverflow(TestUtils.JenkinsRule j) throws Exception { 75 | // Given a scheduler flow that never processes commands. 76 | Settings settings = Settings.load().withCommandQueueBufferSize(1); 77 | final CompletableFuture ignore = new CompletableFuture<>(); 78 | final Flow schedulerFlow = 79 | Flow.of(SchedulerCommand.class).mapAsync(1, command -> ignore); 80 | 81 | // And a running session. 82 | SourceQueueWithComplete sourceQueue = 83 | Session.runScheduler( 84 | settings, 85 | schedulerFlow, 86 | event -> logger.debug("Received event {}", event), 87 | materializer) 88 | .first(); 89 | Session session = new Session(sourceQueue); 90 | 91 | // And one agent is processed and one is queued. 92 | session.getCommands().offer(new KillPod(new PodId("agent1"))); 93 | session.getCommands().offer(new KillPod(new PodId("agent2"))); 94 | 95 | // When we kill a third agent 96 | CompletionStage result = 97 | session.getCommands().offer(new KillPod(new PodId("agent3"))); 98 | 99 | // Then backpressure hits us. 100 | QueueOfferResult offerFeedback = result.toCompletableFuture().get(); 101 | assertThat(offerFeedback, is(QueueOfferResult.dropped())); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/NoDelayProvisionerStrategy.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Label; 5 | import hudson.model.LoadStatistics; 6 | import hudson.slaves.Cloud; 7 | import hudson.slaves.CloudProvisioningListener; 8 | import hudson.slaves.NodeProvisioner; 9 | import hudson.slaves.NodeProvisioner.PlannedNode; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import jenkins.model.Jenkins; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * This is a fork of the Kubernetes plugin provision strategy. 20 | * 21 | *

Implementation of {@link NodeProvisioner.Strategy} which will provision a new node immediately 22 | * as a task enter the queue. In kubernetes, we don't really need to wait before provisioning a new 23 | * node, because kubernetes agents can be started and destroyed quickly 24 | * 25 | * @author runzexia 26 | */ 27 | @Extension(ordinal = 100) 28 | public class NoDelayProvisionerStrategy extends NodeProvisioner.Strategy { 29 | 30 | private static final Logger logger = LoggerFactory.getLogger(MesosCloud.class); 31 | 32 | private static final boolean DISABLE_NODELAY_PROVISING = 33 | Boolean.valueOf(System.getProperty("io.jenkins.plugins.mesos.disableNoDelayProvisioning")); 34 | 35 | @Override 36 | public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState strategyState) { 37 | if (DISABLE_NODELAY_PROVISING) { 38 | logger.info("Provisioning not complete, NoDelayProvisionerStrategy is disabled"); 39 | return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES; 40 | } 41 | 42 | final Label label = strategyState.getLabel(); 43 | 44 | LoadStatistics.LoadStatisticsSnapshot snapshot = strategyState.getSnapshot(); 45 | int availableCapacity = 46 | snapshot.getAvailableExecutors() // live executors 47 | + snapshot.getConnectingExecutors() // executors present but not yet connected 48 | + strategyState 49 | .getPlannedCapacitySnapshot() // capacity added by previous strategies from previous 50 | // rounds 51 | + strategyState 52 | .getAdditionalPlannedCapacity(); // capacity added by previous strategies _this 53 | // round_ 54 | int currentDemand = snapshot.getQueueLength(); 55 | logger.info("Available capacity={}, currentDemand={}", availableCapacity, currentDemand); 56 | if (availableCapacity < currentDemand) { 57 | List jenkinsClouds = new ArrayList<>(Jenkins.get().clouds); 58 | Collections.shuffle(jenkinsClouds); 59 | for (Cloud cloud : jenkinsClouds) { 60 | int workloadToProvision = currentDemand - availableCapacity; 61 | if (!(cloud instanceof MesosCloud)) continue; 62 | if (!cloud.canProvision(label)) continue; 63 | for (CloudProvisioningListener cl : CloudProvisioningListener.all()) { 64 | if (cl.canProvision(cloud, strategyState.getLabel(), workloadToProvision) != null) { 65 | continue; 66 | } 67 | } 68 | Collection plannedNodes = cloud.provision(label, workloadToProvision); 69 | logger.info("Planned {} new nodes", plannedNodes.size()); 70 | fireOnStarted(cloud, strategyState.getLabel(), plannedNodes); 71 | strategyState.recordPendingLaunches(plannedNodes); 72 | availableCapacity += plannedNodes.size(); 73 | logger.info( 74 | "After provisioning, available capacity={}, currentDemand={}", 75 | availableCapacity, 76 | currentDemand); 77 | break; 78 | } 79 | } 80 | if (availableCapacity >= currentDemand) { 81 | logger.info("Provisioning completed"); 82 | return NodeProvisioner.StrategyDecision.PROVISIONING_COMPLETED; 83 | } else { 84 | logger.info("Provisioning not complete, consulting remaining strategies"); 85 | return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES; 86 | } 87 | } 88 | 89 | private static void fireOnStarted( 90 | final Cloud cloud, 91 | final Label label, 92 | final Collection plannedNodes) { 93 | for (CloudProvisioningListener cl : CloudProvisioningListener.all()) { 94 | try { 95 | cl.onStarted(cloud, label, plannedNodes); 96 | } catch (Error e) { 97 | throw e; 98 | } catch (Throwable e) { 99 | logger.error( 100 | "Unexpected uncaught exception encountered while " 101 | + "processing onStarted() listener call in " 102 | + cl 103 | + " for label " 104 | + label.toString(), 105 | e); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosSlaveInfo.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 5 | import hudson.Extension; 6 | import hudson.model.AbstractDescribableImpl; 7 | import hudson.model.Descriptor; 8 | import hudson.model.Node; 9 | import java.util.Iterator; 10 | import java.util.List; 11 | import net.sf.json.JSONException; 12 | import net.sf.json.JSONObject; 13 | import net.sf.json.JSONSerializer; 14 | import org.apache.commons.lang.StringUtils; 15 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate.ContainerInfo; 16 | import org.kohsuke.stapler.DataBoundConstructor; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | /** 21 | * This POJO describes a Jenkins agent for Mesos on 0.x and 1.x of the plugin. It is used to migrate 22 | * older configurations to {@link MesosAgentSpecTemplate} during deserialization. See {@link 23 | * MesosCloud#readResolve()} for the full migration. 24 | */ 25 | public class MesosSlaveInfo { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(MesosSlaveInfo.class); 28 | 29 | private transient Node.Mode mode; 30 | private transient String labelString; 31 | private transient Double slaveCpus; 32 | private transient Double diskNeeded; 33 | private transient int slaveMem; 34 | private transient List additionalURIs; 35 | private transient ContainerInfo containerInfo; 36 | private transient String jnlpArgs; 37 | private transient String agentAttributes; 38 | 39 | // The following fields are dropped during the migration. 40 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 41 | private transient Double executorCpus; 42 | 43 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 44 | private transient int executorMem; 45 | 46 | private transient int minExecutors; 47 | private transient int maxExecutors; 48 | 49 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 50 | private transient String remoteFSRoot; 51 | 52 | private transient int idleTerminationMinutes; 53 | 54 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 55 | private transient String jvmArgs; 56 | 57 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 58 | private transient boolean defaultSlave; 59 | 60 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 61 | private transient String slaveAttributesString; // Agent attributes JSON representation. 62 | 63 | /** 64 | * Resolves the old agent configuration after deserialization. 65 | * 66 | * @return the agent config as a {@link MesosAgentSpecTemplate}. 67 | */ 68 | private Object readResolve() { 69 | this.agentAttributes = 70 | this.migrateAttributeFilter(this.parseSlaveAttributes(this.slaveAttributesString)); 71 | 72 | // Migrate to 2.x spec template 73 | return new MesosAgentSpecTemplate( 74 | this.labelString, 75 | this.mode, 76 | this.slaveCpus.toString(), 77 | Integer.toString(this.slaveMem), 78 | this.idleTerminationMinutes, 79 | this.minExecutors, 80 | this.maxExecutors, 81 | this.diskNeeded.toString(), 82 | this.jnlpArgs, 83 | this.agentAttributes, 84 | this.additionalURIs, 85 | this.containerInfo, 86 | null, 87 | null); 88 | } 89 | 90 | @VisibleForTesting 91 | JSONObject parseSlaveAttributes(String slaveAttributes) { 92 | if (StringUtils.isNotBlank(slaveAttributes)) { 93 | try { 94 | return (JSONObject) JSONSerializer.toJSON(slaveAttributes); 95 | } catch (JSONException e) { 96 | logger.warn( 97 | "Ignoring Mesos agent attributes JSON due to parsing error : " + slaveAttributes); 98 | } 99 | } 100 | 101 | return null; 102 | } 103 | 104 | @VisibleForTesting 105 | String migrateAttributeFilter(JSONObject slaveAttributes) { 106 | if (slaveAttributes == null) { 107 | return ""; 108 | } 109 | 110 | StringBuilder attributes = new StringBuilder(); 111 | // Filtering code from JenkinsScheduler#slaveAttributesMatch in 1.x. 112 | final Iterator iterator = slaveAttributes.keys(); 113 | while (iterator.hasNext()) { 114 | final String key = (String) iterator.next(); 115 | final String value = slaveAttributes.getString(key); 116 | if (attributes.length() > 0) { 117 | attributes.append(","); 118 | } 119 | attributes.append(key).append(":").append(value); 120 | } 121 | 122 | return attributes.toString(); 123 | } 124 | 125 | public static class URI extends AbstractDescribableImpl { 126 | @Extension 127 | public static class DescriptorImpl extends Descriptor { 128 | public String getDisplayName() { 129 | return ""; 130 | } 131 | } 132 | 133 | private final String value; 134 | private final boolean executable; 135 | private final boolean extract; 136 | 137 | @DataBoundConstructor 138 | public URI(String value, boolean executable, boolean extract) { 139 | this.value = value; 140 | this.executable = executable; 141 | this.extract = extract; 142 | } 143 | 144 | public String getValue() { 145 | return value; 146 | } 147 | 148 | public boolean isExecutable() { 149 | return executable; 150 | } 151 | 152 | public boolean isExtract() { 153 | return extract; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/integration/MesosApiTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.integration; 2 | 3 | import static org.awaitility.Awaitility.await; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.equalTo; 6 | 7 | import akka.actor.ActorSystem; 8 | import akka.stream.ActorMaterializer; 9 | import akka.stream.Materializer; 10 | import com.mesosphere.utils.mesos.MesosClusterExtension; 11 | import com.mesosphere.utils.zookeeper.ZookeeperServerExtension; 12 | import hudson.model.Descriptor.FormException; 13 | import java.io.IOException; 14 | import java.net.URISyntaxException; 15 | import java.net.URL; 16 | import java.util.Optional; 17 | import java.util.UUID; 18 | import java.util.concurrent.ExecutionException; 19 | import java.util.concurrent.TimeUnit; 20 | import org.awaitility.Awaitility; 21 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate; 22 | import org.jenkinsci.plugins.mesos.MesosApi; 23 | import org.jenkinsci.plugins.mesos.MesosJenkinsAgent; 24 | import org.jenkinsci.plugins.mesos.TestUtils.JenkinsParameterResolver; 25 | import org.jenkinsci.plugins.mesos.TestUtils.JenkinsRule; 26 | import org.jenkinsci.plugins.mesos.fixture.AgentSpecMother; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.extension.ExtendWith; 29 | import org.junit.jupiter.api.extension.RegisterExtension; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | @ExtendWith(JenkinsParameterResolver.class) 34 | @IntegrationTest 35 | class MesosApiTest { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(MesosApiTest.class); 38 | 39 | @RegisterExtension static ZookeeperServerExtension zkServer = new ZookeeperServerExtension(); 40 | 41 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 42 | static Materializer materializer = ActorMaterializer.create(system); 43 | 44 | @RegisterExtension 45 | static MesosClusterExtension mesosCluster = 46 | MesosClusterExtension.builder() 47 | .withMesosMasterUrl(String.format("zk://%s/mesos", zkServer.getConnectionUrl())) 48 | .withNumMasters(3) 49 | .withLogPrefix(MesosApiTest.class.getCanonicalName()) 50 | .build(system, materializer); 51 | 52 | @Test 53 | public void startAgent(JenkinsRule j) 54 | throws InterruptedException, ExecutionException, IOException, FormException, 55 | URISyntaxException { 56 | 57 | URL jenkinsUrl = j.getURL(); 58 | 59 | String mesosUrl = mesosCluster.getMesosUrl().toString(); 60 | MesosApi api = 61 | new MesosApi( 62 | mesosUrl, 63 | jenkinsUrl, 64 | System.getProperty("user.name"), 65 | "MesosTest-startAgent", 66 | UUID.randomUUID().toString(), 67 | "*", 68 | Optional.empty(), 69 | Optional.empty()); 70 | 71 | final String name = "jenkins-start-agent"; 72 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 73 | 74 | MesosJenkinsAgent agent = api.enqueueAgent(name, spec).toCompletableFuture().get(); 75 | 76 | Awaitility.await().atMost(5, TimeUnit.MINUTES).until(agent::isRunning); 77 | } 78 | 79 | @Test 80 | public void stopAgent(JenkinsRule j) throws Exception { 81 | 82 | String mesosUrl = mesosCluster.getMesosUrl().toString(); 83 | URL jenkinsUrl = j.getURL(); 84 | MesosApi api = 85 | new MesosApi( 86 | mesosUrl, 87 | jenkinsUrl, 88 | System.getProperty("user.name"), 89 | "MesosTest-stopAgent", 90 | UUID.randomUUID().toString(), 91 | "*", 92 | Optional.empty(), 93 | Optional.empty()); 94 | final String name = "jenkins-stop-agent"; 95 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 96 | 97 | MesosJenkinsAgent agent = api.enqueueAgent(name, spec).toCompletableFuture().get(); 98 | // Poll state until we get something. 99 | await().atMost(5, TimeUnit.MINUTES).until(agent::isRunning); 100 | assertThat(agent.isRunning(), equalTo(true)); 101 | 102 | api.killAgent(agent.getPodId()); 103 | await().atMost(5, TimeUnit.MINUTES).until(agent::isKilled); 104 | assertThat(agent.isKilled(), equalTo(true)); 105 | } 106 | 107 | @Test 108 | public void reconnectMesos(JenkinsRule j) throws Exception { 109 | URL jenkinsUrl = j.getURL(); 110 | String mesosUrl = mesosCluster.getMesosUrl().toString(); 111 | 112 | MesosApi api = 113 | new MesosApi( 114 | mesosUrl, 115 | jenkinsUrl, 116 | System.getProperty("user.name"), 117 | "MesosTest-reconnect", 118 | UUID.randomUUID().toString(), 119 | "*", 120 | Optional.empty(), 121 | Optional.empty()); 122 | 123 | // Given a running agent 124 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 125 | MesosJenkinsAgent agent1 = 126 | api.enqueueAgent("agent-before-failover", spec).toCompletableFuture().get(); 127 | // Poll state until we get something. 128 | await().atMost(5, TimeUnit.MINUTES).until(agent1::isRunning); 129 | assertThat(agent1.isRunning(), equalTo(true)); 130 | 131 | // When Mesos has a failover. 132 | mesosCluster.mesosCluster().failover(); 133 | 134 | // Then we can start a new agent 135 | MesosJenkinsAgent agent2 = 136 | api.enqueueAgent("agent-after-failover", spec).toCompletableFuture().get(); 137 | // Poll state until we get something. 138 | await().atMost(5, TimeUnit.MINUTES).until(agent2::isRunning); 139 | assertThat(agent2.isRunning(), equalTo(true)); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/api/Settings.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import java.time.Duration; 6 | 7 | /** 8 | * Operation settings for the Jenkins plugin. These should not be set by Jenkins admins and users 9 | * but rather by Jenkins operators. 10 | */ 11 | public class Settings { 12 | 13 | private final Duration agentTimeout; 14 | private final int commandQueueBufferSize; 15 | private final Duration failoverTimeout; 16 | 17 | private final int connectionRetries; 18 | private final Duration connectionMinBackoff; 19 | private final Duration connectionMaxBackoff; 20 | 21 | /** Internal constructor */ 22 | private Settings( 23 | Duration agentTimeout, 24 | int commandQueueBufferSize, 25 | Duration failoverTimeout, 26 | int connectionRetries, 27 | Duration connectionMinBackoff, 28 | Duration connectionMaxBackoff) { 29 | this.agentTimeout = agentTimeout; 30 | this.commandQueueBufferSize = commandQueueBufferSize; 31 | this.failoverTimeout = failoverTimeout; 32 | this.connectionRetries = connectionRetries; 33 | this.connectionMinBackoff = connectionMinBackoff; 34 | this.connectionMaxBackoff = connectionMaxBackoff; 35 | } 36 | 37 | /** @return copy of these settings with overridden command queue buffer size. */ 38 | public Settings withCommandQueueBufferSize(int commandQueueBufferSize) { 39 | return new Settings( 40 | this.agentTimeout, 41 | commandQueueBufferSize, 42 | this.failoverTimeout, 43 | this.connectionRetries, 44 | this.connectionMinBackoff, 45 | this.connectionMaxBackoff); 46 | } 47 | 48 | /** @return copy of these settings with overridden agent timeout. */ 49 | public Settings withAgentTimeout(Duration agentTimeout) { 50 | return new Settings( 51 | agentTimeout, 52 | this.commandQueueBufferSize, 53 | this.failoverTimeout, 54 | connectionRetries, 55 | this.connectionMinBackoff, 56 | this.connectionMaxBackoff); 57 | } 58 | 59 | /** @return copy of these settings with overridden failover timeout. */ 60 | public Settings withFailoverTimeout(Duration failoverTimeout) { 61 | return new Settings( 62 | this.agentTimeout, 63 | this.commandQueueBufferSize, 64 | failoverTimeout, 65 | connectionRetries, 66 | this.connectionMinBackoff, 67 | this.connectionMaxBackoff); 68 | } 69 | 70 | /** @return copy of these settings with overridden connection retries. */ 71 | public Settings withConnectionRetries(int connectionRetries) { 72 | return new Settings( 73 | this.agentTimeout, 74 | this.commandQueueBufferSize, 75 | this.failoverTimeout, 76 | connectionRetries, 77 | this.connectionMinBackoff, 78 | this.connectionMaxBackoff); 79 | } 80 | 81 | /** @return copy of these settings with overridden connection min backoff. */ 82 | public Settings withConnectionMinBackoff(Duration connectionMinBackoff) { 83 | return new Settings( 84 | this.agentTimeout, 85 | this.commandQueueBufferSize, 86 | this.failoverTimeout, 87 | this.connectionRetries, 88 | connectionMinBackoff, 89 | this.connectionMaxBackoff); 90 | } 91 | 92 | /** @return copy of these settings with overridden connection max backoff. */ 93 | public Settings withConnectionMaxBackoff(Duration connectionMaxBackoff) { 94 | return new Settings( 95 | this.agentTimeout, 96 | this.commandQueueBufferSize, 97 | this.failoverTimeout, 98 | this.connectionRetries, 99 | this.connectionMinBackoff, 100 | connectionMaxBackoff); 101 | } 102 | 103 | /** @return agent timeout setting. */ 104 | public Duration getAgentTimeout() { 105 | return this.agentTimeout; 106 | } 107 | 108 | /** @return command queue buffer size setting. */ 109 | public int getCommandQueueBufferSize() { 110 | return this.commandQueueBufferSize; 111 | } 112 | 113 | /** @return failover timeout setting. */ 114 | public Duration getFailoverTimeout() { 115 | return this.failoverTimeout; 116 | } 117 | 118 | /** @return number of times Jenkins should try to reconnect to Mesos via USI. */ 119 | public int getConnectionRetries() { 120 | return this.connectionRetries; 121 | } 122 | 123 | /** @return minimum backoff for reconnecting to Mesos via USI */ 124 | public Duration getConnectionMinBackoff() { 125 | return this.connectionMinBackoff; 126 | } 127 | 128 | /** @return maximum backoff for reconnecting to Mesos via USI */ 129 | public Duration getConnectionMaxBackoff() { 130 | return this.connectionMaxBackoff; 131 | } 132 | 133 | /** 134 | * Factory method to construct {@link Settings} from a Lightbend {@link Config}. 135 | * 136 | * @param conf The Lightbend config object. 137 | * @return a new settings instances. 138 | */ 139 | public static Settings fromConfig(Config conf) { 140 | return new Settings( 141 | conf.getDuration("agent-timeout"), 142 | conf.getInt("command-queue-buffer-size"), 143 | conf.getDuration("failover-timeout"), 144 | conf.getInt("connection-retries"), 145 | conf.getDuration("connection-min-backoff"), 146 | conf.getDuration("connection-max-backoff")); 147 | } 148 | 149 | /** 150 | * Factory method to construct {@link Settings} with Lightbend {@link ConfigFactory}. 151 | * 152 | * @param loader The class loader used to find application.conf and reference.conf. 153 | * @return a new settings instance loaded from "usi.jenkins" in resources. 154 | */ 155 | public static Settings load(ClassLoader loader) { 156 | Config conf = ConfigFactory.load(loader).getConfig("usi.jenkins"); 157 | return fromConfig(conf); 158 | } 159 | 160 | /** @return a new settings instance loaded from "usi.jenkins" in resources. */ 161 | public static Settings load() { 162 | Config conf = ConfigFactory.load().getConfig("usi.jenkins"); 163 | return fromConfig(conf); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/api/Session.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import akka.Done; 4 | import akka.NotUsed; 5 | import akka.actor.ActorSystem; 6 | import akka.japi.Pair; 7 | import akka.stream.ActorMaterializer; 8 | import akka.stream.OverflowStrategy; 9 | import akka.stream.javadsl.Flow; 10 | import akka.stream.javadsl.Keep; 11 | import akka.stream.javadsl.RestartFlow; 12 | import akka.stream.javadsl.RestartSource; 13 | import akka.stream.javadsl.Sink; 14 | import akka.stream.javadsl.Source; 15 | import akka.stream.javadsl.SourceQueueWithComplete; 16 | import com.mesosphere.mesos.client.CredentialsProvider; 17 | import com.mesosphere.mesos.client.MesosClient; 18 | import com.mesosphere.mesos.client.MesosClient$; 19 | import com.mesosphere.mesos.conf.MesosClientSettings; 20 | import com.mesosphere.usi.core.SchedulerFactory; 21 | import com.mesosphere.usi.core.conf.SchedulerSettings; 22 | import com.mesosphere.usi.core.japi.Scheduler; 23 | import com.mesosphere.usi.core.models.StateEvent; 24 | import com.mesosphere.usi.core.models.StateEventOrSnapshot; 25 | import com.mesosphere.usi.core.models.commands.SchedulerCommand; 26 | import com.mesosphere.usi.repository.PodRecordRepository; 27 | import java.time.Duration; 28 | import java.util.Optional; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.CompletionStage; 31 | import java.util.function.BiFunction; 32 | import java.util.function.Consumer; 33 | import javax.annotation.Nonnull; 34 | import org.apache.mesos.v1.Protos; 35 | import org.apache.mesos.v1.Protos.FrameworkInfo; 36 | import org.jenkinsci.plugins.mesos.MesosApi; 37 | import org.jenkinsci.plugins.mesos.Metrics; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | import scala.compat.java8.OptionConverters; 41 | import scala.concurrent.ExecutionContext; 42 | 43 | /** 44 | * A representation of the connection to Mesos. 45 | * 46 | *

This class should only be used by {@link org.jenkinsci.plugins.mesos.MesosApi}. 47 | */ 48 | public class Session { 49 | 50 | private static final Logger logger = LoggerFactory.getLogger(Session.class); 51 | 52 | // Interface to USI. 53 | @Nonnull private final SourceQueueWithComplete commands; 54 | 55 | public static Session create( 56 | FrameworkInfo frameworkInfo, 57 | MesosClientSettings clientSettings, 58 | Optional provider, 59 | SchedulerSettings schedulerSettings, 60 | PodRecordRepository repository, 61 | Settings operationalSettings, 62 | Consumer eventHandler, 63 | BiFunction terminationHandler, 64 | ExecutionContext context, 65 | ActorSystem system, 66 | ActorMaterializer materializer) { 67 | Flow schedulerFlow = 68 | RestartFlow.withBackoff( 69 | Duration.ofSeconds(3), 70 | Duration.ofSeconds(30), 71 | 0.2, 72 | 20, 73 | () -> { 74 | return connectClient( 75 | frameworkInfo, 76 | operationalSettings, 77 | clientSettings, 78 | provider, 79 | system, 80 | materializer) 81 | .thenCompose( 82 | client -> { 83 | final SchedulerFactory schedulerFactory = 84 | SchedulerFactory.create( 85 | client, 86 | repository, 87 | schedulerSettings, 88 | Metrics.getInstance(frameworkInfo.getName()), 89 | context); 90 | return Scheduler.asFlow(schedulerFactory); 91 | }) 92 | .get() // TODO: avoid blocking. 93 | .getFlow(); 94 | }); 95 | 96 | Pair, CompletionStage> pair = 97 | runScheduler(operationalSettings, schedulerFlow, eventHandler, materializer); 98 | 99 | // TODO: handle termination 100 | // pair.second().handle() 101 | 102 | return new Session(pair.first()); 103 | } 104 | 105 | public Session(SourceQueueWithComplete commands) { 106 | this.commands = commands; 107 | } 108 | 109 | /** Establish a connection to Mesos via the v1 client. */ 110 | private static CompletableFuture connectClient( 111 | Protos.FrameworkInfo frameworkInfo, 112 | Settings operationalSettings, 113 | MesosClientSettings clientSettings, 114 | Optional authorization, 115 | ActorSystem system, 116 | ActorMaterializer materializer) { 117 | 118 | return RestartSource.onFailuresWithBackoff( 119 | operationalSettings.getConnectionMinBackoff(), 120 | operationalSettings.getConnectionMaxBackoff(), 121 | 0.2, 122 | operationalSettings.getConnectionRetries(), 123 | () -> 124 | MesosClient$.MODULE$ 125 | .apply( 126 | clientSettings, 127 | frameworkInfo, 128 | OptionConverters.toScala(authorization), 129 | system, 130 | materializer) 131 | .asJava()) 132 | .runWith(Sink.head(), materializer) 133 | .toCompletableFuture(); 134 | } 135 | 136 | /** 137 | * Constructs a queue of {@link SchedulerCommand}. All state events are processed by {@link 138 | * MesosApi#updateState(StateEventOrSnapshot)}. 139 | * 140 | * @param schedulerFlow The scheduler flow from commands to events provided by USI. 141 | * @param materializer The {@link ActorMaterializer} used for the source queue. 142 | * @return A running source queue. 143 | */ 144 | public static Pair, CompletionStage> runScheduler( 145 | Settings operationalSettings, 146 | Flow schedulerFlow, 147 | Consumer eventHandler, 148 | ActorMaterializer materializer) { 149 | return Source.queue( 150 | operationalSettings.getCommandQueueBufferSize(), OverflowStrategy.dropNew()) 151 | .via(schedulerFlow) 152 | .toMat(Sink.foreach(eventHandler::accept), Keep.both()) 153 | .run(materializer); 154 | } 155 | 156 | public SourceQueueWithComplete getCommands() { 157 | return this.commands; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/integration/MesosCloudProvisionTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.integration; 2 | 3 | import static org.awaitility.Awaitility.await; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsString; 6 | import static org.hamcrest.Matchers.hasSize; 7 | import static org.hamcrest.Matchers.is; 8 | import static org.hamcrest.Matchers.lessThan; 9 | 10 | import akka.actor.ActorSystem; 11 | import akka.stream.ActorMaterializer; 12 | import com.mesosphere.utils.mesos.MesosClusterExtension; 13 | import com.mesosphere.utils.zookeeper.ZookeeperServerExtension; 14 | import hudson.model.FreeStyleBuild; 15 | import hudson.model.FreeStyleProject; 16 | import hudson.model.Node.Mode; 17 | import hudson.model.labels.LabelAtom; 18 | import hudson.slaves.NodeProvisioner; 19 | import hudson.tasks.Builder; 20 | import hudson.tasks.Shell; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.concurrent.TimeUnit; 25 | import jenkins.model.Jenkins; 26 | import okhttp3.Response; 27 | import org.jenkinsci.plugins.mesos.JenkinsConfigClient; 28 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate; 29 | import org.jenkinsci.plugins.mesos.MesosCloud; 30 | import org.jenkinsci.plugins.mesos.MesosJenkinsAgent; 31 | import org.jenkinsci.plugins.mesos.TestUtils; 32 | import org.junit.jupiter.api.Test; 33 | import org.junit.jupiter.api.Timeout; 34 | import org.junit.jupiter.api.extension.ExtendWith; 35 | import org.junit.jupiter.api.extension.RegisterExtension; 36 | 37 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 38 | @IntegrationTest 39 | public class MesosCloudProvisionTest { 40 | 41 | @RegisterExtension static ZookeeperServerExtension zkServer = new ZookeeperServerExtension(); 42 | 43 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 44 | static ActorMaterializer materializer = ActorMaterializer.create(system); 45 | 46 | @RegisterExtension 47 | static MesosClusterExtension mesosCluster = 48 | MesosClusterExtension.builder() 49 | .withMesosMasterUrl(String.format("zk://%s/mesos", zkServer.getConnectionUrl())) 50 | .withLogPrefix(MesosCloudProvisionTest.class.getCanonicalName()) 51 | .build(system, materializer); 52 | 53 | @Test 54 | public void testJenkinsProvision(TestUtils.JenkinsRule j) throws Exception { 55 | LabelAtom label = new LabelAtom("label"); 56 | final int idleMin = 1; 57 | final MesosAgentSpecTemplate spec = 58 | new MesosAgentSpecTemplate( 59 | label.toString(), 60 | Mode.EXCLUSIVE, 61 | "0.1", 62 | "32", 63 | idleMin, 64 | 1, 65 | 1, 66 | "0", 67 | "", 68 | "", 69 | Collections.emptyList(), 70 | null, 71 | null, 72 | null); 73 | List specTemplates = Collections.singletonList(spec); 74 | 75 | MesosCloud cloud = 76 | new MesosCloud( 77 | mesosCluster.getMesosUrl().toString(), 78 | "MesosTest", 79 | null, 80 | "*", 81 | System.getProperty("user.name"), 82 | j.getURL().toString(), 83 | specTemplates); 84 | 85 | int workload = 3; 86 | Collection plannedNodes = cloud.provision(label, workload); 87 | 88 | assertThat(plannedNodes, hasSize(workload)); 89 | for (NodeProvisioner.PlannedNode node : plannedNodes) { 90 | // resolve all plannedNodes 91 | MesosJenkinsAgent agent = (MesosJenkinsAgent) node.future.get(); 92 | 93 | // ensure all plannedNodes are now running 94 | assertThat(agent.isRunning(), is(true)); 95 | assertThat(agent.getComputer().isOnline(), is(true)); 96 | } 97 | 98 | // check that jenkins knows about all the plannedNodes 99 | assertThat(Jenkins.getInstanceOrNull().getNodes(), hasSize(workload)); 100 | } 101 | 102 | @Test 103 | public void testStartAgent(TestUtils.JenkinsRule j) throws Exception { 104 | final String name = "jenkins-agent"; 105 | final int idleMin = 1; 106 | LabelAtom label = new LabelAtom("label"); 107 | final MesosAgentSpecTemplate spec = 108 | new MesosAgentSpecTemplate( 109 | label.toString(), 110 | Mode.EXCLUSIVE, 111 | "0.1", 112 | "32", 113 | idleMin, 114 | 1, 115 | 1, 116 | "0", 117 | "", 118 | "", 119 | Collections.emptyList(), 120 | null, 121 | null, 122 | null); 123 | 124 | List specTemplates = Collections.singletonList(spec); 125 | MesosCloud cloud = 126 | new MesosCloud( 127 | mesosCluster.getMesosUrl().toString(), 128 | "MesosTest", 129 | null, 130 | "*", 131 | System.getProperty("user.name"), 132 | j.getURL().toString(), 133 | specTemplates); 134 | 135 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 136 | 137 | await().atMost(10, TimeUnit.SECONDS).until(agent::isRunning); 138 | 139 | assertThat(agent.isRunning(), is(true)); 140 | assertThat(agent.isOnline(), is(true)); 141 | 142 | // assert jenkins has the 1 added nodes 143 | assertThat(Jenkins.getInstanceOrNull().getNodes(), hasSize(1)); 144 | } 145 | 146 | @Test 147 | @Timeout(value = 5, unit = TimeUnit.MINUTES) 148 | public void runSimpleBuild(TestUtils.JenkinsRule j) throws Exception { 149 | 150 | // Given: a configured Mesos Cloud. 151 | final String label = "mesos"; 152 | final JenkinsConfigClient jenkinsClient = new JenkinsConfigClient(j.createWebClient()); 153 | final Response response = 154 | jenkinsClient.addMesosCloud( 155 | mesosCluster.getMesosUrl().toString(), 156 | "Jenkins Scheduler", 157 | "*", 158 | System.getProperty("user.name"), 159 | j.getURL().toURI().resolve("jenkins").toString(), 160 | label, 161 | "EXCLUSIVE"); 162 | assertThat(response.code(), is(lessThan(400))); 163 | 164 | // And: a project with a simple build command. 165 | FreeStyleProject project = j.createFreeStyleProject("mesos-test"); 166 | final Builder step = new Shell("echo Hello"); 167 | project.getBuildersList().add(step); 168 | project.setAssignedLabel(new LabelAtom(label)); 169 | 170 | // When: we run a build 171 | FreeStyleBuild build = j.buildAndAssertSuccess(project); 172 | 173 | // Then it finishes successfully and the logs contain our command. 174 | assertThat(j.getLog(build), containsString("echo Hello")); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/integration/MesosJenkinsAgentLifecycleTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.integration; 2 | 3 | import static org.awaitility.Awaitility.await; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.hasSize; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import akka.actor.ActorSystem; 10 | import akka.stream.ActorMaterializer; 11 | import com.mesosphere.utils.mesos.MesosClusterExtension; 12 | import com.mesosphere.utils.zookeeper.ZookeeperServerExtension; 13 | import hudson.model.Slave; 14 | import hudson.slaves.SlaveComputer; 15 | import java.time.Duration; 16 | import java.util.Collections; 17 | import java.util.concurrent.*; 18 | import jenkins.model.Jenkins; 19 | import org.jenkinsci.plugins.mesos.*; 20 | import org.jenkinsci.plugins.mesos.fixture.AgentSpecMother; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | import org.junit.jupiter.api.extension.RegisterExtension; 24 | 25 | @ExtendWith(TestUtils.JenkinsParameterResolver.class) 26 | @IntegrationTest 27 | public class MesosJenkinsAgentLifecycleTest { 28 | 29 | @RegisterExtension static ZookeeperServerExtension zkServer = new ZookeeperServerExtension(); 30 | 31 | static ActorSystem system = ActorSystem.create("mesos-scheduler-test"); 32 | static ActorMaterializer materializer = ActorMaterializer.create(system); 33 | 34 | @RegisterExtension 35 | static MesosClusterExtension mesosCluster = 36 | MesosClusterExtension.builder() 37 | .withMesosMasterUrl(String.format("zk://%s/mesos", zkServer.getConnectionUrl())) 38 | .withLogPrefix(MesosJenkinsAgentLifecycleTest.class.getCanonicalName()) 39 | .build(system, materializer); 40 | 41 | @Test 42 | public void testAgentLifecycle(TestUtils.JenkinsRule j) throws Exception { 43 | MesosCloud cloud = 44 | new MesosCloud( 45 | mesosCluster.getMesosUrl().toString(), 46 | "MesosTest", 47 | null, 48 | "*", 49 | System.getProperty("user.name"), 50 | j.getURL().toString(), 51 | Collections.emptyList()); 52 | 53 | final String name = "jenkins-lifecycle"; 54 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 55 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 56 | agent.waitUntilOnlineAsync(materializer).get(); 57 | 58 | // verify slave is running when the future completes; 59 | assertThat(agent.isRunning(), is(true)); 60 | 61 | assertThat(Jenkins.getInstanceOrNull().getNodes(), hasSize(1)); 62 | 63 | agent.terminate(); 64 | await().atMost(10, TimeUnit.SECONDS).until(agent::isKilled); 65 | assertThat(agent.isKilled(), is(true)); 66 | 67 | assertThat(Jenkins.getInstanceOrNull().getNodes(), hasSize(0)); 68 | } 69 | 70 | @Test 71 | public void testComputerNodeTermination(TestUtils.JenkinsRule j) throws Exception { 72 | MesosCloud cloud = 73 | new MesosCloud( 74 | mesosCluster.getMesosUrl().toString(), 75 | "MesosTest", 76 | null, 77 | "*", 78 | System.getProperty("user.name"), 79 | j.getURL().toString(), 80 | Collections.emptyList()); 81 | 82 | final String name = "jenkins-node-terminate"; 83 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 84 | 85 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 86 | agent.waitUntilOnlineAsync(materializer).get(); 87 | 88 | assertThat(agent.isRunning(), is(true)); 89 | assertThat(agent.getComputer().isOnline(), is(true)); 90 | 91 | SlaveComputer computer = agent.getComputer(); 92 | assert (computer != null); 93 | Slave node = computer.getNode(); 94 | assert (node != null); 95 | MesosJenkinsAgent shouldBeParent = (MesosJenkinsAgent) node; 96 | shouldBeParent.terminate(); 97 | await().atMost(10, TimeUnit.SECONDS).until(agent::isKilled); 98 | assertThat(agent.isKilled(), is(true)); 99 | } 100 | 101 | @Test 102 | public void testComputerNodeDeletion(TestUtils.JenkinsRule j) throws Exception { 103 | MesosCloud cloud = 104 | new MesosCloud( 105 | mesosCluster.getMesosUrl().toString(), 106 | "MesosTest", 107 | null, 108 | "*", 109 | System.getProperty("user.name"), 110 | j.getURL().toString(), 111 | Collections.emptyList()); 112 | 113 | final String name = "jenkins-node-delete"; 114 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 115 | 116 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 117 | agent.waitUntilOnlineAsync(materializer).get(); 118 | 119 | assertThat(agent.isRunning(), is(true)); 120 | assertThat(agent.getComputer().isOnline(), is(true)); 121 | 122 | SlaveComputer computer = agent.getComputer(); 123 | assert (computer != null); 124 | computer.doDoDelete(); 125 | 126 | await().atMost(10, TimeUnit.SECONDS).until(agent::isKilled); 127 | assertThat(agent.isKilled(), is(true)); 128 | } 129 | 130 | @Test 131 | public void testRetentionStrategy(TestUtils.JenkinsRule j) throws Exception { 132 | MesosCloud cloud = 133 | new MesosCloud( 134 | mesosCluster.getMesosUrl().toString(), 135 | "MesosTest", 136 | null, 137 | "*", 138 | System.getProperty("user.name"), 139 | j.getURL().toString(), 140 | Collections.emptyList()); 141 | 142 | final String name = "jenkins-node-delete"; 143 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 144 | 145 | MesosJenkinsAgent agent = (MesosJenkinsAgent) cloud.startAgent(name, spec).get(); 146 | agent.waitUntilOnlineAsync(materializer).get(); 147 | 148 | assertThat(agent.isRunning(), is(true)); 149 | 150 | SlaveComputer computer = agent.getComputer(); 151 | assert (computer != null); 152 | assertThat(computer.isOnline(), is(true)); 153 | assertThat(computer.isIdle(), is(true)); 154 | 155 | // after 1 minute MesosRetentionStrategy will kill the task 156 | await().atMost(3, TimeUnit.MINUTES).until(agent::isKilled); 157 | } 158 | 159 | @Test 160 | public void testAgentTimeout(TestUtils.JenkinsRule j) throws Exception { 161 | MesosCloud cloud = 162 | new MesosCloud( 163 | mesosCluster.getMesosUrl().toString(), 164 | "MesosTest", 165 | null, 166 | "*", 167 | System.getProperty("user.name"), 168 | j.getURL().toString(), 169 | Collections.emptyList()); 170 | 171 | MesosApi.getInstance(cloud).setAgentTimeout(Duration.ofSeconds(1)); 172 | 173 | final String name = "jenkins-agent-timeout"; 174 | 175 | final MesosAgentSpecTemplate spec = AgentSpecMother.simple; 176 | 177 | ExecutionException e = 178 | assertThrows( 179 | ExecutionException.class, 180 | () -> { 181 | cloud.startAgent(name, spec).get(); 182 | }); 183 | 184 | assertThat( 185 | e.getCause().getMessage(), 186 | is( 187 | "java.util.concurrent.TimeoutException: The stream has not been completed in 1 second.")); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosJenkinsAgent.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import akka.NotUsed; 4 | import akka.stream.ActorMaterializer; 5 | import akka.stream.KillSwitches; 6 | import akka.stream.SharedKillSwitch; 7 | import akka.stream.javadsl.Sink; 8 | import akka.stream.javadsl.Source; 9 | import com.mesosphere.usi.core.models.*; 10 | import hudson.Extension; 11 | import hudson.model.Computer; 12 | import hudson.model.Descriptor; 13 | import hudson.model.Node; 14 | import hudson.model.TaskListener; 15 | import hudson.slaves.*; 16 | import java.io.IOException; 17 | import java.io.NotSerializableException; 18 | import java.net.URL; 19 | import java.time.Duration; 20 | import java.util.List; 21 | import java.util.Optional; 22 | import java.util.concurrent.CompletableFuture; 23 | import jenkins.metrics.api.Metrics; 24 | import org.apache.mesos.v1.Protos.TaskState; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | /** Representation of a Jenkins node on Mesos. */ 30 | public class MesosJenkinsAgent extends AbstractCloudSlave implements EphemeralNode { 31 | 32 | private static final Logger logger = LoggerFactory.getLogger(MesosJenkinsAgent.class); 33 | 34 | private final Duration onlineTimeout; 35 | 36 | // Holds the current USI status for this agent. 37 | Optional currentStatus = Optional.empty(); 38 | 39 | private final boolean reusable; 40 | 41 | private final MesosApi api; 42 | 43 | private final String podId; 44 | 45 | private final URL jenkinsUrl; 46 | 47 | private final SharedKillSwitch waitUntilOnlineKillSwitch; 48 | 49 | @DataBoundConstructor 50 | public MesosJenkinsAgent( 51 | MesosApi api, 52 | String name, 53 | MesosAgentSpecTemplate spec, 54 | String nodeDescription, 55 | URL jenkinsUrl, 56 | Integer idleTerminationInMinutes, 57 | boolean reusable, 58 | List> nodeProperties, 59 | Duration agentTimeout) 60 | throws Descriptor.FormException, IOException { 61 | super( 62 | name, 63 | nodeDescription, 64 | "jenkins", 65 | 1, 66 | spec.getMode(), 67 | spec.getLabel(), 68 | new JNLPLauncher(), 69 | new MesosRetentionStrategy(idleTerminationInMinutes), 70 | nodeProperties); 71 | // pass around the MesosApi connection 72 | this.api = api; 73 | this.reusable = reusable; 74 | this.podId = name; 75 | this.jenkinsUrl = jenkinsUrl; 76 | this.onlineTimeout = agentTimeout; 77 | 78 | this.waitUntilOnlineKillSwitch = 79 | KillSwitches.shared(String.format("wait-until-online-{}", name)); 80 | } 81 | 82 | @Extension 83 | public static final class DescriptorImpl extends NodeDescriptor { 84 | public String getDisplayName() { 85 | return ""; 86 | } 87 | } 88 | 89 | /** 90 | * Polls the agent until it is online. Note: This is a non-blocking call in contrast to the 91 | * blocking {@link AbstractCloudComputer#waitUntilOnline}. 92 | * 93 | * @return The future agent that will come online. 94 | */ 95 | public CompletableFuture waitUntilOnlineAsync(ActorMaterializer materializer) { 96 | return Source.tick(Duration.ofSeconds(0), Duration.ofSeconds(1), NotUsed.notUsed()) 97 | .via(this.waitUntilOnlineKillSwitch.flow()) 98 | .completionTimeout(onlineTimeout) 99 | .filter(ignored -> this.isOnline()) 100 | .map(ignored -> this.asNode()) 101 | .runWith(Sink.head(), materializer) 102 | .toCompletableFuture(); 103 | } 104 | 105 | /** @return whether the agent is running or not. */ 106 | public synchronized boolean isRunning() { 107 | if (currentStatus.isPresent()) { 108 | return currentStatus 109 | .get() 110 | .taskStatuses() 111 | .values() 112 | .forall(taskStatus -> taskStatus.getState() == TaskState.TASK_RUNNING); 113 | } else { 114 | return false; 115 | } 116 | } 117 | 118 | /** @return whether the agent is killed or not. */ 119 | public synchronized boolean isKilled() { 120 | if (currentStatus.isPresent()) { 121 | return currentStatus 122 | .get() 123 | .taskStatuses() 124 | .values() 125 | .forall(taskStatus -> taskStatus.getState() == TaskState.TASK_KILLED); 126 | } else { 127 | return false; 128 | } 129 | } 130 | 131 | /** @return whether the agent is terminal or unreachable. */ 132 | public synchronized boolean isTerminalOrUnreachable() { 133 | return currentStatus.map(PodStatus::isTerminalOrUnreachable).orElse(false); 134 | } 135 | 136 | /** @return whether the Jenkins agent connected and is online. */ 137 | public synchronized boolean isOnline() { 138 | final Computer computer = this.toComputer(); 139 | if (computer != null) { 140 | return computer.isOnline(); 141 | } else { 142 | logger.warn("No computer for node {}.", getNodeName()); 143 | return false; 144 | } 145 | } 146 | 147 | /** @return whether the agent is launching and not connected yet. */ 148 | public synchronized boolean isPending() { 149 | return (!isTerminalOrUnreachable() && !isOnline()); 150 | } 151 | 152 | /** 153 | * Updates the state of the agent and takes action on certain events. 154 | * 155 | * @param event The state event from USI which informs about the task status. 156 | */ 157 | public synchronized void update(PodStatusUpdatedEvent event) { 158 | if (event.newStatus().isDefined()) { 159 | logger.info("Received new status for {}", event.id().value()); 160 | this.currentStatus = Optional.of(event.newStatus().get()); 161 | 162 | // Handle state change. 163 | if (this.isTerminalOrUnreachable()) { 164 | Metrics.metricRegistry().meter("mesos.agent.terminal").mark(); 165 | String message = 166 | String.format( 167 | "Agent %s became %s: %s", 168 | this.getNodeName(), 169 | this.currentStatus.get().taskStatuses().values().head().getState(), 170 | this.currentStatus.get().taskStatuses().values().head().getMessage()); 171 | waitUntilOnlineKillSwitch.abort(new IllegalStateException(message)); 172 | } 173 | } 174 | } 175 | 176 | @Override 177 | public Node asNode() { 178 | return this; 179 | } 180 | 181 | @Override 182 | public AbstractCloudComputer createComputer() { 183 | return new MesosComputer(this); 184 | } 185 | 186 | @Override 187 | protected void _terminate(TaskListener listener) { 188 | try { 189 | logger.info("Killing task {}", this.podId); 190 | this.api.killAgent(this.podId); 191 | } catch (Exception ex) { 192 | logger.warn("Error when killing task {}", this.podId, ex); 193 | } 194 | } 195 | 196 | public boolean getReusable() { 197 | // TODO: implement reusable agents DCOS_OSS-5048 198 | return reusable; 199 | } 200 | 201 | /** get the podId tied to this task. */ 202 | public String getPodId() { 203 | return podId; 204 | } 205 | 206 | // Mark as not serializable. 207 | 208 | private void writeObject(java.io.ObjectOutputStream stream) throws IOException { 209 | throw new NotSerializableException(); 210 | } 211 | 212 | private void readObject(java.io.ObjectInputStream stream) 213 | throws IOException, ClassNotFoundException { 214 | throw new NotSerializableException(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/api/LaunchCommandBuilder.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.google.common.collect.ImmutableList; 5 | import com.mesosphere.usi.core.models.PodId; 6 | import com.mesosphere.usi.core.models.commands.LaunchPod; 7 | import com.mesosphere.usi.core.models.constraints.AgentFilter; 8 | import com.mesosphere.usi.core.models.constraints.AttributeStringIsFilter; 9 | import com.mesosphere.usi.core.models.faultdomain.DomainFilter; 10 | import com.mesosphere.usi.core.models.faultdomain.HomeRegionFilter$; 11 | import com.mesosphere.usi.core.models.resources.ScalarRequirement; 12 | import com.mesosphere.usi.core.models.template.FetchUri; 13 | import com.mesosphere.usi.core.models.template.RunTemplate; 14 | import java.net.MalformedURLException; 15 | import java.net.URI; 16 | import java.net.URISyntaxException; 17 | import java.net.URL; 18 | import java.nio.file.Paths; 19 | import java.util.Arrays; 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.stream.Collectors; 24 | import jenkins.model.Jenkins; 25 | import org.checkerframework.checker.nullness.qual.NonNull; 26 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate.ContainerInfo; 27 | import scala.Option; 28 | 29 | /** 30 | * A simpler factory for building {@link com.mesosphere.usi.core.models.commands.LaunchPod} for 31 | * Jenkins agents. 32 | */ 33 | public class LaunchCommandBuilder { 34 | 35 | public LaunchCommandBuilder() {} 36 | 37 | private static final String AGENT_JAR_URI_SUFFIX = "jnlpJars/agent.jar"; 38 | 39 | // We allocate extra memory for the JVM 40 | private static final int JVM_XMX = 32; 41 | 42 | public static enum AgentCommandStyle { 43 | Linux, 44 | Windows 45 | } 46 | 47 | private static final String LINUX_AGENT_COMMAND_TEMPLATE = 48 | "java -DHUDSON_HOME=jenkins -server -Xmx%dm %s -jar ${MESOS_SANDBOX-.}/agent.jar %s %s -jnlpUrl %s"; 49 | private static final String WINDOWS_AGENT_COMMAND_TEMPLATE = 50 | "java -DHUDSON_HOME=jenkins -server -Xmx%dm %s -jar %%MESOS_SANDBOX%%/agent.jar %s %s -jnlpUrl %s"; 51 | 52 | private static final String JNLP_SECRET_FORMAT = "-secret %s"; 53 | 54 | private PodId id = null; 55 | private ScalarRequirement cpus = null; 56 | private ScalarRequirement memory = null; 57 | private ScalarRequirement disk = null; 58 | private String role = null; 59 | private List additionalFetchUris = Collections.emptyList(); 60 | private Optional containerInfo = Optional.empty(); 61 | private AgentCommandStyle agentCommandStyle = AgentCommandStyle.Linux; 62 | private DomainFilter domainInfoFilter = HomeRegionFilter$.MODULE$; 63 | 64 | private int xmx = 0; 65 | 66 | private String jvmArgString = ""; 67 | private String jnlpArgString = ""; 68 | private String agentAttributeString = ""; 69 | 70 | private URL jenkinsMaster = null; 71 | 72 | /** 73 | * Sets the name of the Mesos task. 74 | * 75 | * @param name Unique name of Mesos task. 76 | * @return this pod spec builder. 77 | */ 78 | public LaunchCommandBuilder withName(String name) { 79 | this.id = new PodId(name); 80 | return this; 81 | } 82 | 83 | public LaunchCommandBuilder withCpu(Double cpus) { 84 | this.cpus = ScalarRequirement.cpus(cpus); 85 | return this; 86 | } 87 | 88 | /** 89 | * Sets the maximum memory pool for the JVM aka Xmx. Please note that the Mesos task will have 90 | * {@link LaunchCommandBuilder#JVM_XMX} more memory allocated. 91 | * 92 | * @param memory Memory in megabyte. 93 | * @return the pod spec builder. 94 | */ 95 | public LaunchCommandBuilder withMemory(int memory) { 96 | this.memory = ScalarRequirement.memory(memory + JVM_XMX); 97 | this.xmx = JVM_XMX; 98 | return this; 99 | } 100 | 101 | public LaunchCommandBuilder withDisk(Double disk) { 102 | this.disk = ScalarRequirement.disk(disk); 103 | return this; 104 | } 105 | 106 | public LaunchCommandBuilder withJenkinsUrl(URL url) { 107 | this.jenkinsMaster = url; 108 | return this; 109 | } 110 | 111 | public LaunchCommandBuilder withRole(String role) { 112 | this.role = role; 113 | return this; 114 | } 115 | 116 | public LaunchCommandBuilder withContainerInfo(Optional containerInfo) { 117 | this.containerInfo = containerInfo; 118 | return this; 119 | } 120 | 121 | public LaunchCommandBuilder withDomainInfoFilter(Optional domainInfoFilter) { 122 | this.domainInfoFilter = domainInfoFilter.orElse(HomeRegionFilter$.MODULE$); 123 | return this; 124 | } 125 | 126 | public LaunchCommandBuilder withAdditionalFetchUris(List additionalFetchUris) { 127 | 128 | this.additionalFetchUris = additionalFetchUris; 129 | return this; 130 | } 131 | 132 | public LaunchCommandBuilder withAgentCommandStyle(Optional maybeStyle) { 133 | maybeStyle.ifPresent(style -> this.agentCommandStyle = style); 134 | return this; 135 | } 136 | 137 | public LaunchCommandBuilder withJnlpArguments(String args) { 138 | this.jnlpArgString = args; 139 | return this; 140 | } 141 | 142 | public LaunchCommandBuilder withAgentAttribute(String agentAttribute) { 143 | this.agentAttributeString = agentAttribute; 144 | return this; 145 | } 146 | 147 | public LaunchPod build() throws MalformedURLException, URISyntaxException { 148 | final RunTemplate runTemplate = 149 | RunTemplateFactory.newRunTemplate( 150 | this.id.value(), 151 | Arrays.asList(this.cpus, this.memory, this.disk), 152 | this.buildCommand(), 153 | this.role, 154 | this.buildFetchUris(), 155 | this.containerInfo); 156 | 157 | return LaunchPod.create( 158 | this.id, runTemplate, this.domainInfoFilter, buildAgentAttributeFilters()); 159 | } 160 | 161 | /** @return the agent shell command for the Mesos task. */ 162 | private String buildCommand() throws MalformedURLException { 163 | final String template; 164 | switch (this.agentCommandStyle) { 165 | case Linux: 166 | template = LINUX_AGENT_COMMAND_TEMPLATE; 167 | break; 168 | case Windows: 169 | template = WINDOWS_AGENT_COMMAND_TEMPLATE; 170 | break; 171 | default: 172 | template = LINUX_AGENT_COMMAND_TEMPLATE; 173 | break; 174 | } 175 | return String.format( 176 | template, 177 | this.xmx, 178 | this.jvmArgString, 179 | this.jnlpArgString, 180 | buildJnlpSecret(), 181 | buildJnlpUrl()); 182 | } 183 | 184 | @VisibleForTesting 185 | String buildJnlpSecret() { 186 | String jnlpSecret = ""; 187 | if (getJenkins().isUseSecurity()) { 188 | jnlpSecret = 189 | String.format( 190 | JNLP_SECRET_FORMAT, 191 | jenkins.slaves.JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(this.id.value())); 192 | } 193 | return jnlpSecret; 194 | } 195 | 196 | @NonNull 197 | private static Jenkins getJenkins() { 198 | Jenkins jenkins = Jenkins.getInstanceOrNull(); 199 | if (jenkins == null) { 200 | throw new IllegalStateException("Jenkins is null"); 201 | } 202 | return jenkins; 203 | } 204 | 205 | private Iterable buildAgentAttributeFilters() { 206 | if (agentAttributeString.isEmpty()) { 207 | return Collections.emptyList(); 208 | } else { 209 | return Arrays.stream(agentAttributeString.split(",")) 210 | .map( 211 | attribute -> { 212 | final String name = attribute.split(":")[0]; 213 | final String value = attribute.split(":")[1]; 214 | return new AttributeStringIsFilter(name, value); 215 | }) 216 | .collect(Collectors.toList()); 217 | } 218 | } 219 | 220 | /** 221 | * @return the Jnlp url for the agent: http://[controller]/computer/[agentName]/slave-agent.jnlp 222 | */ 223 | private URL buildJnlpUrl() throws MalformedURLException { 224 | final String path = Paths.get("computer", this.id.value(), "slave-agent.jnlp").toString(); 225 | return new URL(this.jenkinsMaster, path); 226 | } 227 | 228 | /** @return the {@link FetchUri} for the Jenkins agent jar file. */ 229 | private List buildFetchUris() throws MalformedURLException, URISyntaxException { 230 | final URI uri = new URL(this.jenkinsMaster, AGENT_JAR_URI_SUFFIX).toURI(); 231 | final FetchUri jenkinsAgentFetchUri = new FetchUri(uri, false, false, false, Option.empty()); 232 | 233 | return ImmutableList.builder() 234 | .addAll(this.additionalFetchUris) 235 | .add(jenkinsAgentFetchUri) 236 | .build(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Docker Pulls 4 |

5 | 6 | Jenkins on Mesos 7 | ---------------- 8 | 9 | The `jenkins-mesos` plugin allows Jenkins to dynamically launch Jenkins agents on a 10 | Mesos cluster depending on the workload! 11 | 12 | Put simply, whenever the Jenkins `Build Queue` starts getting bigger, this plugin 13 | automatically spins up additional Jenkins agent(s) on Mesos so that jobs can be 14 | immediately scheduled! Similarly, when a Jenkins agent is idle for a long time it 15 | is automatically shut down. 16 | 17 | ## Table of Contents 18 | 19 | - __[Prerequisite](#prerequisite)__ 20 | - __[Installing the Plugin](#installing-the-plugin)__ 21 | - __[Configuring the Plugin](#configuring-the-plugin)__ 22 | - __[Adding Agent Specs](#adding-agent-specs)__ 23 | - __[DC/OS Authentication](#dcos-authentication)__ 24 | - __[Configuring Jenkins Jobs](#configuring-jenkins-jobs)__ 25 | - __[Docker Containers](#docker-containers)__ 26 | - __[Docker Configuration](#docker-configuration)__ 27 | - __[Over provisioning flags](#over-provisioning-flags)__ 28 | - __[Single-Use Agent](#single-use-agent)__ 29 | - __[Freestyle jobs](#freestyle-jobs)__ 30 | - __[Pipeline jobs](#pipeline-jobs)__ 31 | - __[Plugin Development](#plugin-development)__ 32 | - __[Building the plugin](#building-the-plugin)__ 33 | - __[Testing On DC/OS Enterprise](#testing-on-dcos-enterprise)__ 34 | - __[Release](#release)__ 35 | 36 | 37 | 38 | ## Prerequisite ## 39 | 40 | You need to have access to a running Mesos cluster. For instructions on setting up a Mesos cluster, please refer to the [Mesos website](http://mesos.apache.org). 41 | 42 | ## Installing the Plugin ## 43 | 44 | * Go to 'Manage Plugins' page in the Jenkins Web UI, you'll find the plugin in the 'Available' tab under the name 'mesos'. 45 | 46 | * (Optional) Install the metrics plugin which is an optional dependency of this plugin, used for additional but not essential features. 47 | 48 | ### Configuring the Plugin ### 49 | 50 | Now go to 'Configure' page in Jenkins. If the plugin is successfully installed 51 | you should see an option to 'Add a new cloud' at the bottom of the page. 52 | 53 | 1. Add the 'Mesos Cloud'. 54 | 2. Give the path to the address `http://HOST:PORT` of a running Mesos master. On DC/OS this can be as simple as `https://leader.mesos:5050`. 55 | 3. Set the user name agents should start as. Ensure that the Mesos agents have have the user available. 56 | 4. Set the Jenkins URL. 57 | 5. Click `Save`. 58 | 59 | You can click `Test Conection` to see if the Mesos client of the plugin can find the Mesos master. 60 | 61 | If the Mesos master uses a secured connection with a custom certificate you can supply it under 62 | `Use a custom SSL certificate`. 63 | 64 | ### Adding Agent Specs ### 65 | 66 | An `Agent Spec` describes a Jenkins node for Mesos. 67 | 68 | You can update the values/Add more 'Agent Specs'/Delete 'Agent Specs' by clicking on 'Advanced'. 69 | 'Agent Specs' can hold required information(Executor CPU, Executor Mem etc) for an agent that needs 70 | to be matched against Mesos offers. 71 | Label name is the key between the job and the required agent to execute the job. See [Configuring Jenkins Jobs](#configuring-jenkins-jobs). 72 | For instance, heavy jobs can be assigned label 'powerful_agent'(which has 20 Executor CPU, 10240M Executor Mem etc) 73 | and light weight jobs can be assigned label 'light_weight_agent'(which has 1 Executor CPU, 128M Executor Mem etc). 74 | 75 | The [Jenkins Configuration as Code](https://jenkins.io/projects/jcasc/) in [dcos/conf/jenkins](dcos/conf/jenkins/configuration.yaml) configures a Linux agent based on the [amazoncorretto:8](https://hub.docker.com/_/amazoncorretto) Docker image and a Windows agent based on [mesosphere/jenkins-windows-node:latest](https://hub.docker.com/repository/docker/mesosphere/jenkins-windows-node/) Docker image. See https://github.com/jeschkies/hello-world-fsharp/blob/master/Jenkinsfile for an example build. 76 | 77 | ### DC/OS Authentication ### 78 | 79 | The plugin can authenticate with a [DC/OS](https://docs.d2iq.com/mesosphere/dcos/1.13/security/ent/service-auth/) enterprise cluster. 80 | Simply run the environment variables `DCOS_SERVICE_ACCOUNT` containing the service account name and 81 | `DCOS_SERVICE_ACCOUNT_PRIVATE_KEY` containing the private key for the service account. See [On DC/OS Enterprise](#on-dcos-enterprise) for details. 82 | 83 | ### Configuring Jenkins Jobs ### 84 | 85 | Finally, just add the label name you have configured in Mesos cloud configuration -> Advanced -> Agent Info -> Label String (default is `mesos`) 86 | to the jobs (configure -> Restrict where this project can run checkbox) that you want to run on a specific agent type inside Mesos cluster. 87 | 88 | ### Docker Containers ### 89 | 90 | By default, the Jenkins agents are run in the default Mesos container. To run the Jenkins agent inside a Docker container, there are two options. 91 | 92 | 1) "Use Native Docker Containerizer" : Select this option if Mesos agent(s) are configured with "--containerizers=docker" (recommended). 93 | 94 | 2) "Use External Containerizer" : Select this option if Mesos agent(s) are configured with "--containerizers=external". 95 | 96 | ### Docker Configuration ### 97 | 98 | #### Volumes #### 99 | 100 | At a minimum, a container path must be entered to mount the volume. A host path can also be specified to bind mount the container path to the host path. This will allow persistence of data between agents on the same node. The default setting is read-write, but an option is provided for read-only use. 101 | 102 | #### Parameters #### 103 | 104 | Additional parameters are available for the `docker run` command, but there are too many and they change too often to list all separately. This section allows you to provide any parameter you want. Ensure that your Docker version on your Mesos agents is compatible with the parameters you add and that the values are correctly formatted. Use the full-word parameter and not the shortcut version, as these may not work properly. Also, exclude the preceding double-dash on the parameter name. For example, enter `volumes-from` and `my_container_name` to recieve the volumes from `my_container_name`. Of course `my_container_name` must already be on the Mesos agent where the Jenkins agent will run. This shouldn't cause problems in a homogenous environment where Jenkins agents only run on particular Mesos agents. 105 | 106 | ### Over provisioning flags ### 107 | 108 | By default, Jenkins spawns slaves conservatively. Say, if there are 2 builds in queue, it won't spawn 2 executors immediately. It will spawn one executor and wait for sometime for the first executor to be freed before deciding to spawn the second executor. Jenkins makes sure every executor it spawns is utilized to the maximum. 109 | If you want to override this behaviour and spawn an executor for each build in queue immediately without waiting, you can use these flags during Jenkins startup: 110 | `-Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85` 111 | 112 | ## Single-Use Agent ## 113 | 114 | ### Freestyle jobs ### 115 | 116 | In the Build Environment settings, you may select "Mesos Single-Use Agent" to schedule disposal of the agent after the build finishes. 117 | 118 | ### Pipeline jobs ### 119 | 120 | To schedule agent disposal from a Pipeline job: 121 | 122 | node('mylabel') { 123 | wrap([$class: 'MesosSingleUseSlave']) { 124 | // build actions 125 | } 126 | } 127 | 128 | 129 | ## Plugin Development 130 | 131 | ### Building the plugin ### 132 | 133 | Build the plugin as follows: 134 | 135 | $ ./gradlew check 136 | 137 | This should build the Mesos plugin as `mesos.hpi` in the `target` folder. A test Jenkins server can be 138 | started with 139 | 140 | $ ./gradlew server 141 | 142 | The integration tests require an installation of Mesos and Docker. You can run just the unit tests with 143 | 144 | $ ./gradlew test 145 | 146 | and the integration tests with 147 | 148 | $ ./gradlew integrationTest 149 | 150 | The code is formatted following the [Google Style Guide](https://github.com/google/styleguide). 151 | 152 | ### Testing On DC/OS Enterprise 153 | 154 | See the [dcos folder](dcos-testing/README.md). 155 | 156 | ## Release 157 | 158 | You must have publish rights and the credentials set in `~/.m2/settings.xml` and `~/.jenkins-ci.org` as described 159 | [here](https://wiki.jenkins.io/display/JENKINS/Hosting+Plugins#HostingPlugins-Releasingtojenkins-ci.org). 160 | 161 | To release this plugin 162 | 163 | 1. Set the version in `build.gradle`. 164 | 2. Publish the plugin with `./gradlew publish`. 165 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/api/RunTemplateFactory.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos.api; 2 | 3 | import static org.apache.mesos.v1.Protos.ContainerInfo.Type.MESOS; 4 | import static org.apache.mesos.v1.Protos.Image.Type.DOCKER; 5 | 6 | import com.mesosphere.usi.core.models.TaskBuilder; 7 | import com.mesosphere.usi.core.models.TaskName; 8 | import com.mesosphere.usi.core.models.resources.ResourceRequirement; 9 | import com.mesosphere.usi.core.models.template.FetchUri; 10 | import com.mesosphere.usi.core.models.template.LegacyLaunchRunTemplate; 11 | import com.mesosphere.usi.core.models.template.RunTemplate; 12 | import com.mesosphere.usi.core.models.template.SimpleRunTemplateFactory.DockerEntrypoint$; 13 | import com.mesosphere.usi.core.models.template.SimpleRunTemplateFactory.Shell; 14 | import com.mesosphere.usi.core.models.template.SimpleRunTemplateFactory.SimpleTaskInfoBuilder; 15 | import com.mesosphere.usi.core.models.template.SimpleRunTemplateFactory.SimpleTaskInfoBuilder$; 16 | import java.util.List; 17 | import java.util.Optional; 18 | import org.apache.mesos.v1.Protos.ContainerInfo; 19 | import org.apache.mesos.v1.Protos.ContainerInfo.DockerInfo; 20 | import org.apache.mesos.v1.Protos.ContainerInfo.DockerInfo.Network; 21 | import org.apache.mesos.v1.Protos.Image; 22 | import org.apache.mesos.v1.Protos.Offer; 23 | import org.apache.mesos.v1.Protos.Resource; 24 | import org.apache.mesos.v1.Protos.TaskInfo; 25 | import org.apache.mesos.v1.Protos.Volume; 26 | import org.apache.mesos.v1.Protos.Volume.Mode; 27 | import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | import scala.collection.immutable.Map; 31 | import scala.collection.immutable.Seq; 32 | 33 | /** The builder is used by {@link LaunchCommandBuilder} to construct a USI {@link RunTemplate}. */ 34 | public class RunTemplateFactory { 35 | 36 | /** 37 | * Constructs a {@link RunTemplate} based on the passed parameters. 38 | * 39 | *

The template uses either the {@link SimpleTaskInfoBuilder} or a custom {@link 40 | * ContainerInfoTaskInfoBuilder}. 41 | * 42 | * @param agentName The name of the Mesos task/Jenkins agent, {@link 43 | * LaunchCommandBuilder#withName(String)}. 44 | * @param requirements The resource requirements for a Jenkins agent. 45 | * @param shellCommand The shell command built by {@link LaunchCommandBuilder}. 46 | * @param role The Mesos role the Jenkins agent will assume. 47 | * @param fetchUris Artifacts that are fetched, eg the Jenkins agent.jar. 48 | * @param containerInfo Optional information for a Docker or Mesos container. 49 | * @return the new USI run template. 50 | */ 51 | static RunTemplate newRunTemplate( 52 | String agentName, 53 | List requirements, 54 | String shellCommand, 55 | String role, 56 | List fetchUris, 57 | Optional containerInfo) { 58 | 59 | // If a container info is set we assume its Docker image defines an entrypoint. 60 | TaskBuilder taskBuilder; 61 | if (containerInfo.isPresent()) { 62 | taskBuilder = 63 | SimpleTaskInfoBuilder$.MODULE$.create( 64 | requirements, 65 | DockerEntrypoint$.MODULE$.create(shellCommand), 66 | fetchUris, 67 | containerInfo.map(MesosAgentSpecTemplate.ContainerInfo::getDockerImage)); 68 | taskBuilder = new ContainerInfoTaskInfoBuilder(agentName, taskBuilder, containerInfo.get()); 69 | } else { 70 | taskBuilder = 71 | SimpleTaskInfoBuilder$.MODULE$.create( 72 | requirements, new Shell(shellCommand), fetchUris, Optional.empty()); 73 | } 74 | return new LegacyLaunchRunTemplate(role, taskBuilder); 75 | } 76 | 77 | /** 78 | * This is a small USI {@link TaskBuilder} that wraps the {@link SimpleTaskInfoBuilder} and adds 79 | * {@link org.apache.mesos.v1.Protos.ContainerInfo} to the Mesos task info if defined. 80 | */ 81 | public static class ContainerInfoTaskInfoBuilder implements TaskBuilder { 82 | 83 | private static final Logger logger = 84 | LoggerFactory.getLogger(ContainerInfoTaskInfoBuilder.class); 85 | 86 | public static final String PORT_RESOURCE_NAME = "ports"; 87 | public static final String MESOS_DEFAULT_ROLE = "*"; 88 | public static final Network DEFAULT_NETWORKING = Network.BRIDGE; 89 | 90 | final TaskBuilder simpleTaskInfoBuilder; 91 | final MesosAgentSpecTemplate.ContainerInfo containerInfo; 92 | final String agentName; 93 | 94 | /** 95 | * Constructs a new {@link TaskBuilder}. 96 | * 97 | *

This is basically a port of JenkinsScheduler.getContainerInfoBuilder from v1.1 of the 98 | * plugin. 99 | * 100 | * @param agentName The name of the Jenkins agent. 101 | * @param taskInfoBuilder The original {@link SimpleTaskInfoBuilder}. 102 | * @param containerInfo The additional container information. 103 | */ 104 | public ContainerInfoTaskInfoBuilder( 105 | String agentName, 106 | TaskBuilder taskInfoBuilder, 107 | MesosAgentSpecTemplate.ContainerInfo containerInfo) { 108 | this.agentName = agentName; 109 | this.simpleTaskInfoBuilder = taskInfoBuilder; 110 | this.containerInfo = containerInfo; 111 | } 112 | 113 | @Override 114 | public Seq resourceRequirements() { 115 | return this.simpleTaskInfoBuilder.resourceRequirements(); 116 | } 117 | 118 | @Override 119 | public void buildTask( 120 | TaskInfo.Builder builder, 121 | Offer matchedOffer, 122 | Seq taskResources, 123 | Map> peerTaskResources) { 124 | this.simpleTaskInfoBuilder.buildTask(builder, matchedOffer, taskResources, peerTaskResources); 125 | this.getContainerInfoBuilder(matchedOffer, builder); 126 | } 127 | 128 | /** 129 | * This is the original v1.1 JenkinsScheduler.getContainerInfoBuilder. 130 | * 131 | * @param offer The Mesos offer. 132 | * @param agentName The name of the Jenkins agent 133 | * @param taskBuilder The Mesos task info builder. 134 | */ 135 | private void getContainerInfoBuilder(Offer offer, TaskInfo.Builder taskBuilder) { 136 | ContainerInfo.Type containerType = ContainerInfo.Type.valueOf(this.containerInfo.getType()); 137 | 138 | ContainerInfo.Builder containerInfoBuilder = 139 | ContainerInfo.newBuilder().setType(containerType); 140 | 141 | switch (containerType) { 142 | case DOCKER: 143 | logger.info("Launching in Docker Mode:" + this.containerInfo.getDockerImage()); 144 | DockerInfo.Builder dockerInfoBuilder = 145 | DockerInfo.newBuilder() 146 | .setImage(this.containerInfo.getDockerImage()) 147 | .setPrivileged(this.containerInfo.getDockerPrivilegedMode()) 148 | .setForcePullImage(this.containerInfo.getDockerForcePullImage()); 149 | 150 | dockerInfoBuilder.setNetwork(this.containerInfo.getNetworking()); 151 | 152 | // https://github.com/jenkinsci/mesos-plugin/issues/109 153 | if (dockerInfoBuilder.getNetwork() != Network.HOST) { 154 | containerInfoBuilder.setHostname(agentName); 155 | } 156 | 157 | containerInfoBuilder.setDocker(dockerInfoBuilder); 158 | break; 159 | case MESOS: 160 | logger.info("Launching in UCR Mode:" + this.containerInfo.getDockerImage()); 161 | 162 | Image dockerImage = 163 | Image.newBuilder() 164 | .setType(DOCKER) 165 | .setDocker( 166 | Image.Docker.newBuilder() 167 | .setName(this.containerInfo.getDockerImage()) 168 | .build()) 169 | .build(); 170 | 171 | containerInfoBuilder 172 | .setType(MESOS) 173 | .setMesos(ContainerInfo.MesosInfo.newBuilder().setImage(dockerImage).build()); 174 | 175 | if (this.containerInfo.getIsDind()) { 176 | containerInfoBuilder.addVolumes( 177 | Volume.newBuilder() 178 | .setContainerPath("/var/lib/docker") 179 | .setHostPath("docker") 180 | .setMode(Mode.RW)); 181 | } 182 | break; 183 | 184 | default: 185 | logger.warn("Unknown container type:" + this.containerInfo.getType()); 186 | } 187 | 188 | for (MesosAgentSpecTemplate.Volume volume : this.containerInfo.getVolumesOrEmpty()) { 189 | logger.info("Adding volume '" + volume.getContainerPath() + "'"); 190 | Volume.Builder volumeBuilder = 191 | Volume.newBuilder() 192 | .setContainerPath(volume.getContainerPath()) 193 | .setMode(volume.isReadOnly() ? Mode.RO : Mode.RW); 194 | if (!volume.getHostPath().isEmpty()) { 195 | volumeBuilder.setHostPath(volume.getHostPath()); 196 | } 197 | containerInfoBuilder.addVolumes(volumeBuilder.build()); 198 | } 199 | 200 | taskBuilder.setContainer(containerInfoBuilder.build()); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosAgentSpecTemplate.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import com.mesosphere.usi.core.models.commands.LaunchPod; 4 | import com.mesosphere.usi.core.models.template.FetchUri; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import hudson.Extension; 7 | import hudson.model.AbstractDescribableImpl; 8 | import hudson.model.Descriptor; 9 | import hudson.model.Label; 10 | import hudson.model.Node; 11 | import hudson.model.labels.LabelAtom; 12 | import hudson.util.FormValidation; 13 | import java.net.MalformedURLException; 14 | import java.net.URISyntaxException; 15 | import java.net.URL; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.Objects; 19 | import java.util.Optional; 20 | import java.util.Set; 21 | import java.util.UUID; 22 | import java.util.stream.Collectors; 23 | import org.apache.commons.lang.StringUtils; 24 | import org.apache.mesos.v1.Protos.ContainerInfo.DockerInfo.Network; 25 | import org.jenkinsci.plugins.mesos.api.LaunchCommandBuilder; 26 | import org.jenkinsci.plugins.mesos.api.RunTemplateFactory.ContainerInfoTaskInfoBuilder; 27 | import org.jenkinsci.plugins.mesos.config.models.faultdomain.DomainFilterModel; 28 | import org.kohsuke.stapler.DataBoundConstructor; 29 | import org.kohsuke.stapler.QueryParameter; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | import scala.Option; 33 | 34 | /** This is the Mesos agent pod spec config set by a user. */ 35 | public class MesosAgentSpecTemplate extends AbstractDescribableImpl { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(MesosAgentSpecTemplate.class); 38 | 39 | private final String label; 40 | private Set labelSet; 41 | 42 | private final Node.Mode mode; 43 | private final int idleTerminationMinutes; 44 | private final boolean reusable; 45 | private final double cpus; 46 | private final int mem; 47 | private final double disk; 48 | private final int minExecutors; 49 | private final int maxExecutors; 50 | private final String jnlpArgs; 51 | private final String agentAttributes; 52 | private final List additionalURIs; 53 | private final LaunchCommandBuilder.AgentCommandStyle agentCommandStyle; 54 | private final ContainerInfo containerInfo; 55 | private final DomainFilterModel domainFilterModel; 56 | 57 | @DataBoundConstructor 58 | public MesosAgentSpecTemplate( 59 | String label, 60 | Node.Mode mode, 61 | String cpus, 62 | String mem, 63 | int idleTerminationMinutes, 64 | int minExecutors, 65 | int maxExecutors, 66 | String disk, 67 | String jnlpArgs, 68 | String agentAttributes, 69 | List additionalURIs, 70 | ContainerInfo containerInfo, 71 | LaunchCommandBuilder.AgentCommandStyle agentCommandStyle, 72 | DomainFilterModel domainFilterModel) { 73 | this.label = label; 74 | this.mode = mode; 75 | this.idleTerminationMinutes = idleTerminationMinutes; 76 | this.reusable = false; // TODO: DCOS_OSS-5048. 77 | this.cpus = (cpus != null) ? Double.parseDouble(cpus) : 0.1; 78 | this.mem = Integer.parseInt(mem); 79 | this.minExecutors = minExecutors; 80 | this.maxExecutors = maxExecutors; 81 | this.disk = (disk != null) ? Double.parseDouble(disk) : 0.0; 82 | this.jnlpArgs = StringUtils.isNotBlank(jnlpArgs) ? jnlpArgs : ""; 83 | this.agentAttributes = StringUtils.isNotBlank(agentAttributes) ? agentAttributes : ""; 84 | this.additionalURIs = (additionalURIs != null) ? additionalURIs : Collections.emptyList(); 85 | this.containerInfo = containerInfo; 86 | this.domainFilterModel = domainFilterModel; 87 | this.agentCommandStyle = agentCommandStyle; 88 | validate(); 89 | } 90 | 91 | private void validate() {} 92 | 93 | @Extension 94 | public static final class DescriptorImpl extends Descriptor { 95 | 96 | public DescriptorImpl() { 97 | load(); 98 | } 99 | 100 | /** 101 | * Validate that CPUs is a positive double. 102 | * 103 | * @param cpus The number of CPUs to user for agent. 104 | * @return Whether the supplied CPUs is valid. 105 | */ 106 | public FormValidation doCheckCpus(@QueryParameter String cpus) { 107 | try { 108 | if (Double.valueOf(cpus) > 0.0) { 109 | return FormValidation.ok(); 110 | } else { 111 | return FormValidation.error(cpus + " must be a positive floating-point-number."); 112 | } 113 | } catch (NumberFormatException e) { 114 | return FormValidation.error(cpus + " must be a positive floating-point-number."); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Creates a LaunchPod command to to create a new Jenkins agent via USI 121 | * 122 | * @param jenkinsUrl the URL of the Jenkins controller. 123 | * @param name The name of the node to launch. 124 | * @param role The Mesos role for the task. 125 | * @return a LaunchPod command to be passed to USI. 126 | * @throws MalformedURLException If a fetch URL is not well formed. 127 | * @throws URISyntaxException IF the fetch URL cannot be converted into a proper URI. 128 | */ 129 | public LaunchPod buildLaunchCommand(URL jenkinsUrl, String name, String role) 130 | throws MalformedURLException, URISyntaxException { 131 | List fetchUris = 132 | additionalURIs.stream() 133 | .map( 134 | uri -> { 135 | try { 136 | return new FetchUri( 137 | new java.net.URI(uri.getValue()), 138 | uri.isExtract(), 139 | uri.isExecutable(), 140 | false, 141 | Option.empty()); 142 | } catch (URISyntaxException e) { 143 | logger.warn(String.format("Could not migrate URI: %s", uri.getValue()), e); 144 | return null; 145 | } 146 | }) 147 | .filter(Objects::nonNull) 148 | .collect(Collectors.toList()); 149 | 150 | return new LaunchCommandBuilder() 151 | .withCpu(this.getCpus()) 152 | .withMemory(this.getMem()) 153 | .withDisk(this.getDisk()) 154 | .withName(name) 155 | .withRole(role) 156 | .withJenkinsUrl(jenkinsUrl) 157 | .withContainerInfo(Optional.ofNullable(this.getContainerInfo())) 158 | .withDomainInfoFilter( 159 | Optional.ofNullable(this.getDomainFilterModel()).map(model -> model.getFilter())) 160 | .withJnlpArguments(this.getJnlpArgs()) 161 | .withAgentAttribute(this.getAgentAttributes()) 162 | .withAgentCommandStyle(Optional.ofNullable(this.agentCommandStyle)) 163 | .withAdditionalFetchUris(fetchUris) 164 | .build(); 165 | } 166 | 167 | public String getLabel() { 168 | return this.label; 169 | } 170 | 171 | public Set getLabelSet() { 172 | // Label.parse requires a Jenkins instance so we initialize it lazily 173 | if (this.labelSet == null) { 174 | this.labelSet = Label.parse(label); 175 | } 176 | return this.labelSet; 177 | } 178 | 179 | public Node.Mode getMode() { 180 | return this.mode; 181 | } 182 | 183 | /** 184 | * Generate a new unique name for a new agent. Note: multiple calls will yield different names. 185 | * 186 | * @return A new unique name for an agent. 187 | */ 188 | public String generateName() { 189 | return String.format("jenkins-agent-%s-%s", this.label, UUID.randomUUID().toString()); 190 | } 191 | 192 | public double getCpus() { 193 | return this.cpus; 194 | } 195 | 196 | public double getDisk() { 197 | return this.disk; 198 | } 199 | 200 | public int getMem() { 201 | return this.mem; 202 | } 203 | 204 | public int getIdleTerminationMinutes() { 205 | return this.idleTerminationMinutes; 206 | } 207 | 208 | public boolean getReusable() { 209 | return this.reusable; 210 | } 211 | 212 | public List getAdditionalURIs() { 213 | return additionalURIs; 214 | } 215 | 216 | public int getMinExecutors() { 217 | return minExecutors; 218 | } 219 | 220 | public int getMaxExecutors() { 221 | return maxExecutors; 222 | } 223 | 224 | public LaunchCommandBuilder.AgentCommandStyle getAgentCommandStyle() { 225 | return this.agentCommandStyle; 226 | } 227 | 228 | public String getJnlpArgs() { 229 | return jnlpArgs; 230 | } 231 | 232 | public String getAgentAttributes() { 233 | return agentAttributes; 234 | } 235 | 236 | public ContainerInfo getContainerInfo() { 237 | return this.containerInfo; 238 | } 239 | 240 | public DomainFilterModel getDomainFilterModel() { 241 | return this.domainFilterModel; 242 | } 243 | 244 | public static class ContainerInfo extends AbstractDescribableImpl { 245 | 246 | private final String type; 247 | private final String dockerImage; 248 | private final List volumes; 249 | private final Network networking; 250 | private final boolean dockerPrivilegedMode; 251 | private final boolean dockerForcePullImage; 252 | private boolean isDind; 253 | 254 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 255 | private transient List portMappings; 256 | 257 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 258 | private transient boolean dockerImageCustomizable; 259 | 260 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 261 | private transient List parameters; 262 | 263 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 264 | private transient List networkInfos; 265 | 266 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 267 | private transient boolean useCustomDockerCommandShell; 268 | 269 | @SuppressFBWarnings("UUF_UNUSED_FIELD") 270 | private transient String customDockerCommandShell; 271 | 272 | @DataBoundConstructor 273 | public ContainerInfo( 274 | String type, 275 | String dockerImage, 276 | boolean isDind, 277 | boolean dockerPrivilegedMode, 278 | boolean dockerForcePullImage, 279 | List volumes, 280 | Network networking) { 281 | this.type = type; 282 | this.dockerImage = dockerImage; 283 | this.dockerPrivilegedMode = dockerPrivilegedMode; 284 | this.dockerForcePullImage = dockerForcePullImage; 285 | this.volumes = volumes; 286 | this.isDind = isDind; 287 | this.networking = 288 | (networking != null) ? networking : ContainerInfoTaskInfoBuilder.DEFAULT_NETWORKING; 289 | } 290 | 291 | public boolean getIsDind() { 292 | return this.isDind; 293 | } 294 | 295 | public Network getNetworking() { 296 | return this.networking; 297 | } 298 | 299 | public String getType() { 300 | return type; 301 | } 302 | 303 | public String getDockerImage() { 304 | return dockerImage; 305 | } 306 | 307 | public boolean getDockerPrivilegedMode() { 308 | return dockerPrivilegedMode; 309 | } 310 | 311 | public boolean getDockerForcePullImage() { 312 | return dockerForcePullImage; 313 | } 314 | 315 | public List getVolumes() { 316 | return volumes; 317 | } 318 | 319 | public List getVolumesOrEmpty() { 320 | return (this.volumes != null) ? this.volumes : Collections.emptyList(); 321 | } 322 | 323 | @Extension 324 | public static final class DescriptorImpl extends Descriptor { 325 | 326 | public DescriptorImpl() { 327 | load(); 328 | } 329 | } 330 | } 331 | 332 | public static class Volume extends AbstractDescribableImpl { 333 | 334 | private final String containerPath; 335 | private final String hostPath; 336 | private final boolean readOnly; 337 | 338 | @DataBoundConstructor 339 | public Volume(String containerPath, String hostPath, boolean readOnly) { 340 | this.containerPath = containerPath; 341 | this.hostPath = hostPath; 342 | this.readOnly = readOnly; 343 | } 344 | 345 | public String getContainerPath() { 346 | return containerPath; 347 | } 348 | 349 | public String getHostPath() { 350 | return hostPath; 351 | } 352 | 353 | public boolean isReadOnly() { 354 | return readOnly; 355 | } 356 | 357 | @Extension 358 | public static final class DescriptorImpl extends Descriptor { 359 | 360 | public DescriptorImpl() { 361 | load(); 362 | } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/mesos/JenkinsConfigClient.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import java.io.IOException; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.URL; 6 | import java.net.URLEncoder; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import javax.json.Json; 10 | import javax.json.JsonObjectBuilder; 11 | import okhttp3.MediaType; 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.Request; 14 | import okhttp3.RequestBody; 15 | import okhttp3.Response; 16 | import org.jvnet.hudson.test.JenkinsRule.WebClient; 17 | 18 | /** 19 | * A simple client that hacks around the Jenkins configure form submit. 20 | * 21 | *

It allows to add a simple Mesos Cloud with one label. 22 | */ 23 | public class JenkinsConfigClient { 24 | final OkHttpClient client; 25 | final URL jenkinsConfigUrl; 26 | 27 | public JenkinsConfigClient(WebClient jenkinsClient) throws IOException { 28 | this.client = new OkHttpClient(); 29 | this.jenkinsConfigUrl = jenkinsClient.createCrumbedUrl("configSubmit"); 30 | } 31 | 32 | /** 33 | * Submits a Jenkins configuration form and adds a Mesos Cloud with one agent specs. 34 | * 35 | * @param mesosMasterUrl The URL of the Mesos master to connect to. 36 | * @param frameworkName The Mesos framework name the plugin should use. 37 | * @param role 38 | * @param agentUser 39 | * @param jenkinsUrl The Jenkins URL used as the base for Mesos tasks to download the Jenkins 40 | * agent. 41 | * @param label The Jenkins node label of the Mesos task. 42 | * @param mode Jenkins mode, can be "NORMAL" or "EXCLUSIVE". 43 | * @return A {@link Response} of the {@link OkHttpClient} request. 44 | * @throws IOException 45 | * @throws UnsupportedEncodingException 46 | */ 47 | public Response addMesosCloud( 48 | String mesosMasterUrl, 49 | String frameworkName, 50 | String role, 51 | String agentUser, 52 | String jenkinsUrl, 53 | String label, 54 | String mode) 55 | throws IOException, UnsupportedEncodingException { 56 | final String jsonData = 57 | addJsonDefaults(Json.createObjectBuilder(), jenkinsUrl) 58 | .add( 59 | "jenkins-model-GlobalCloudConfiguration", 60 | Json.createObjectBuilder() 61 | .add( 62 | "cloud", 63 | Json.createObjectBuilder() 64 | .add("mesosMasterUrl", mesosMasterUrl) 65 | .add("frameworkName", frameworkName) 66 | .add("role", role) 67 | .add("agentUser", agentUser) 68 | .add("jenkinsURL", jenkinsUrl) 69 | .add( 70 | "mesosAgentSpecTemplates", 71 | Json.createObjectBuilder() 72 | .add("label", label) 73 | .add("mode", mode) 74 | .add("idleTerminationMinutes", "1") 75 | .add("cpus", "0.1") 76 | .add("mem", "32") 77 | .add("reusable", true) 78 | .add("minExecutors", 1) 79 | .add("maxExecutors", 1) 80 | .add("disk", "0") 81 | .add("jnlpArgs", "") 82 | .add("defaultAgent", false) 83 | .add("additionalUris", Json.createArrayBuilder().build()) 84 | .add("containerImage", "") 85 | .build()) 86 | .add("stapler-class", "org.jenkinsci.plugins.mesos.MesosCloud") 87 | .add("$class", "org.jenkinsci.plugins.mesos.MesosCloud") 88 | .build()) 89 | .build()) 90 | .add("core:apply", "") 91 | .build() 92 | .toString(); 93 | 94 | final String formData = 95 | addFormDefaults(new FormDataBuilder(), jenkinsUrl) 96 | .add("_.mesosMasterUrl", mesosMasterUrl) 97 | .add("_.frameworkName", frameworkName) 98 | .add("_.role", "*") 99 | .add("_.agentUser", agentUser) 100 | .add("_.jenkinsURL", jenkinsUrl) 101 | .add("_.label", label) 102 | .add("mode", mode) 103 | .add("stapler-class", "org.jenkinsci.plugins.mesos.MesosCloud") 104 | .add("$class", "org.jenkinsci.plugins.mesos.MesosCloud") 105 | .add("core:apply", "") 106 | .add("json", jsonData) 107 | .build(); 108 | 109 | final MediaType FORM = MediaType.get("application/x-www-form-urlencoded"); 110 | RequestBody rawBody = RequestBody.create(FORM, formData); 111 | Request request = 112 | new Request.Builder() 113 | .url(this.jenkinsConfigUrl.toString()) 114 | .addHeader("Accept", "text/html,application/xhtml+xml") 115 | .addHeader("Origin", jenkinsUrl) 116 | .addHeader("Upgrade-Insecure-Requests", "1") 117 | .post(rawBody) 118 | .build(); 119 | return this.client.newCall(request).execute(); 120 | } 121 | 122 | /** 123 | * Adds defaults from a manual form submit with Chrome Dev Tools. 124 | * 125 | *

The form includes a JSON field with all default configurations. 126 | * 127 | * @param builder The json builder that will be changed. 128 | * @param jenkinsUrl URL for jenkins. 129 | * @return The changed builder. 130 | */ 131 | private JsonObjectBuilder addJsonDefaults(JsonObjectBuilder builder, String jenkinsUrl) { 132 | return builder 133 | .add("system_message", "") 134 | .add( 135 | "jenkins-model-MasterBuildConfiguration", 136 | Json.createObjectBuilder() 137 | .add("numExecutors", "2") 138 | .add("labelString", "") 139 | .add("mode", "NORMAL") 140 | .build()) 141 | .add( 142 | "jenkins-model-GlobalQuietPeriodConfiguration", 143 | Json.createObjectBuilder().add("quietPeriod", "5").build()) 144 | .add( 145 | "jenkins-model-GlobalSCMRetryCountConfiguration", 146 | Json.createObjectBuilder().add("scmCheckoutRetryCount", "0").build()) 147 | .add( 148 | "jenkins-model-GlobalProjectNamingStrategyConfiguration", 149 | Json.createObjectBuilder().build()) 150 | .add( 151 | "jenkins-model-GlobalNodePropertiesConfiguration", 152 | Json.createObjectBuilder() 153 | .add("globalNodeProperties", Json.createObjectBuilder().build()) 154 | .build()) 155 | .add( 156 | "hudson-model-UsageStatistics", 157 | Json.createObjectBuilder() 158 | .add("usageStatisticsCollected", Json.createObjectBuilder().build()) 159 | .build()) 160 | .add( 161 | "jenkins-management-AdministrativeMonitorsConfiguration", 162 | Json.createObjectBuilder() 163 | .add( 164 | "administrativeMonitor", 165 | Json.createArrayBuilder() 166 | .add("hudson.PluginManager$PluginCycleDependenciesMonitor") 167 | .add("hudson.PluginManager$PluginUpdateMonitor") 168 | .add("hudson.PluginWrapper$PluginWrapperAdministrativeMonitor") 169 | .add("hudsonHomeIsFull") 170 | .add("hudson.diagnosis.NullIdDescriptorMonitor") 171 | .add("OldData") 172 | .add("hudson.diagnosis.ReverseProxySetupMonitor") 173 | .add("hudson.diagnosis.TooManyJobsButNoView") 174 | .add("hudson.model.UpdateCenter$CoreUpdateMonitor") 175 | .add("hudson.node_monitors.MonitorMarkedNodeOffline") 176 | .add("hudson.triggers.SCMTrigger$AdministrativeMonitorImpl") 177 | .add("jenkins.CLI") 178 | .add("jenkins.diagnosis.HsErrPidList") 179 | .add("jenkins.diagnostics.CompletedInitializationMonitor") 180 | .add("jenkins.diagnostics.RootUrlNotSetMonitor") 181 | .add("jenkins.diagnostics.SecurityIsOffMonitor") 182 | .add("jenkins.diagnostics.URICheckEncodingMonitor") 183 | .add("jenkins.model.DownloadSettings$Warning") 184 | .add("jenkins.model.Jenkins$EnforceSlaveAgentPortAdministrativeMonitor") 185 | .add("jenkins.security.RekeySecretAdminMonitor") 186 | .add("jenkins.security.UpdateSiteWarningsMonitor") 187 | .add( 188 | "jenkins.security.apitoken.ApiTokenPropertyDisabledDefaultAdministrativeMonitor") 189 | .add( 190 | "jenkins.security.apitoken.ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor") 191 | .add("legacyApiToken") 192 | .add("jenkins.security.csrf.CSRFAdministrativeMonitor") 193 | .add("slaveToMasterAccessControl") 194 | .add("jenkins.security.s2m.MasterKillSwitchWarning") 195 | .add("jenkins.slaves.DeprecatedAgentProtocolMonitor") 196 | .build()) 197 | .build()) 198 | .add( 199 | "jenkins-model-JenkinsLocationConfiguration", 200 | Json.createObjectBuilder().add("url", jenkinsUrl).add("adminAddress", "").build()) 201 | .add("hudson-task-Shell", Json.createObjectBuilder().add("shell", "").build()); 202 | } 203 | 204 | /** 205 | * Adds defaults from a manual form submit with Chrome Dev Tools. 206 | * 207 | * @param builder The form data builder that will be changed. 208 | * @param jenkinsUrl URL for jenkins. 209 | * @return The changed builder. 210 | * @throws UnsupportedEncodingException 211 | */ 212 | private FormDataBuilder addFormDefaults(FormDataBuilder builder, String jenkinsUrl) 213 | throws UnsupportedEncodingException { 214 | return builder 215 | .add("system_message", "") 216 | .add("_.numExecutors", "2") 217 | .add("_.labelString", "") 218 | .add("master.mode", "NORMAL") 219 | .add("_.quietPeriod", "5") 220 | .add("_.scmCheckoutRetryCount", "0") 221 | .add("stapler-class", "jenkins.model.ProjectNamingStrategy$PatternProjectNamingStrategy") 222 | .add("$class", "jenkins.model.ProjectNamingStrategy$PatternProjectNamingStrategy") 223 | .add("_.namePattern", ".*") 224 | .add("_.description", "") 225 | .add("namingStrategy", "1") 226 | .add("stapler-class", "jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy") 227 | .add("$class", "jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy") 228 | .add("_.usageStatisticsCollected", "on") 229 | .add("administrativeMonitor", "on") 230 | .add("administrativeMonitor", "on") 231 | .add("administrativeMonitor", "on") 232 | .add("administrativeMonitor", "on") 233 | .add("administrativeMonitor", "on") 234 | .add("administrativeMonitor", "on") 235 | .add("administrativeMonitor", "on") 236 | .add("administrativeMonitor", "on") 237 | .add("administrativeMonitor", "on") 238 | .add("administrativeMonitor", "on") 239 | .add("administrativeMonitor", "on") 240 | .add("administrativeMonitor", "on") 241 | .add("administrativeMonitor", "on") 242 | .add("administrativeMonitor", "on") 243 | .add("administrativeMonitor", "on") 244 | .add("administrativeMonitor", "on") 245 | .add("administrativeMonitor", "on") 246 | .add("administrativeMonitor", "on") 247 | .add("administrativeMonitor", "on") 248 | .add("administrativeMonitor", "on") 249 | .add("administrativeMonitor", "on") 250 | .add("administrativeMonitor", "on") 251 | .add("administrativeMonitor", "on") 252 | .add("_.url", jenkinsUrl) 253 | .add("_.adminAddress", "") 254 | .add("_.shell", ""); 255 | } 256 | 257 | /** Jenkins submits forms in URL encoding. This builder helps to construct such a request body. */ 258 | private static class FormDataBuilder { 259 | final List values = new ArrayList<>(); 260 | 261 | /** 262 | * Add a key value pair to the form. Duplicates are allowed. Keys and values are URL encoded. 263 | * 264 | * @param key The form field key/name. 265 | * @param value The value for the key. 266 | * @return This builder. 267 | * @throws UnsupportedEncodingException 268 | */ 269 | public FormDataBuilder add(String key, String value) throws UnsupportedEncodingException { 270 | final String pair = URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8"); 271 | this.values.add(pair); 272 | return this; 273 | } 274 | 275 | /** @return a joined string of all key/value pairs. */ 276 | public String build() { 277 | return String.join("&", this.values); 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/mesos/MesosApi.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.mesos; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.stream.ActorMaterializer; 5 | import akka.stream.QueueOfferResult; 6 | import akka.stream.javadsl.*; 7 | import com.mesosphere.mesos.MasterDetector$; 8 | import com.mesosphere.mesos.client.CredentialsProvider; 9 | import com.mesosphere.mesos.client.DcosServiceAccountProvider; 10 | import com.mesosphere.mesos.conf.MesosClientSettings; 11 | import com.mesosphere.usi.core.conf.SchedulerSettings; 12 | import com.mesosphere.usi.core.models.*; 13 | import com.mesosphere.usi.core.models.commands.KillPod; 14 | import com.mesosphere.usi.core.models.commands.LaunchPod; 15 | import com.mesosphere.usi.core.models.commands.SchedulerCommand; 16 | import com.mesosphere.usi.repository.PodRecordRepository; 17 | import com.typesafe.config.Config; 18 | import com.typesafe.config.ConfigFactory; 19 | import com.typesafe.config.ConfigValueFactory; 20 | import hudson.model.Descriptor.FormException; 21 | import java.io.IOException; 22 | import java.net.MalformedURLException; 23 | import java.net.URISyntaxException; 24 | import java.net.URL; 25 | import java.time.Duration; 26 | import java.util.*; 27 | import java.util.concurrent.CompletionStage; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | import java.util.concurrent.ExecutionException; 30 | import javax.annotation.Nonnull; 31 | import jenkins.model.Jenkins; 32 | import org.apache.mesos.v1.Protos; 33 | import org.jenkinsci.plugins.mesos.MesosCloud.DcosAuthorization; 34 | import org.jenkinsci.plugins.mesos.api.Session; 35 | import org.jenkinsci.plugins.mesos.api.Settings; 36 | import org.slf4j.Logger; 37 | import org.slf4j.LoggerFactory; 38 | import scala.concurrent.ExecutionContext; 39 | 40 | /** 41 | * Provides a simplified interface to Mesos through USI. 42 | * 43 | *

Each connection should be a singleton. New instance are create via {@link 44 | * MesosApi#getInstance(String, URL, String, String, String, String, Optional, Optional)}. 45 | */ 46 | public class MesosApi { 47 | 48 | private static final Logger logger = LoggerFactory.getLogger(MesosApi.class); 49 | 50 | static HashMap sessions = new HashMap<>(); 51 | 52 | /** 53 | * Fetching an existing connection or constructs a new one. 54 | * 55 | *

This is modelled after the KubernetesClientProvider of the Kubernetes plugin. 56 | */ 57 | public static synchronized MesosApi getInstance(MesosCloud cloud) 58 | throws InterruptedException, ExecutionException { 59 | final URL jenkinsURL; 60 | try { 61 | jenkinsURL = new URL(cloud.getJenkinsURL()); 62 | } catch (MalformedURLException ex) { 63 | throw new ExecutionException("Could not parse Jenkins URL", ex); 64 | } 65 | return getInstance( 66 | cloud.getMesosMasterUrl(), 67 | jenkinsURL, 68 | cloud.getAgentUser(), 69 | cloud.getFrameworkName(), 70 | cloud.getFrameworkId(), 71 | cloud.getRole(), 72 | cloud.getSslCert(), 73 | cloud.getAuthorization()); 74 | } 75 | 76 | private static synchronized MesosApi getInstance( 77 | String master, 78 | URL jenkinsUrl, 79 | String agentUser, 80 | String frameworkName, 81 | String frameworkId, 82 | String role, 83 | Optional sslCert, 84 | Optional authorization) 85 | throws ExecutionException, InterruptedException { 86 | if (!sessions.containsKey(frameworkId)) { 87 | final MesosApi session = 88 | new MesosApi( 89 | master, 90 | jenkinsUrl, 91 | agentUser, 92 | frameworkName, 93 | frameworkId, 94 | role, 95 | sslCert, 96 | authorization); 97 | logger.info("Initialized Mesos API object for framework {}", frameworkId); 98 | sessions.put(frameworkId, session); 99 | return session; 100 | } else { 101 | // Override Jenkins URL and agent user if they changed. 102 | MesosApi session = sessions.get(frameworkId); 103 | logger.debug("Fetched Mesos API object for framework {}", frameworkId); 104 | 105 | session.setJenkinsUrl(jenkinsUrl); 106 | session.setAgentUser(agentUser); 107 | return session; 108 | } 109 | } 110 | 111 | private final Settings operationalSettings; 112 | 113 | private final String frameworkName; 114 | private final Optional frameworkPrincipal; 115 | private String role; 116 | private String agentUser; 117 | private final String frameworkId; 118 | private URL jenkinsUrl; 119 | private Duration agentTimeout; 120 | 121 | // Connection to Mesos through USI 122 | @Nonnull private final Session session; 123 | 124 | // Internal state. 125 | @Nonnull private final ConcurrentHashMap stateMap; 126 | @Nonnull private final PodRecordRepository repository; 127 | 128 | // Actor system. 129 | @Nonnull private final ActorSystem system; 130 | @Nonnull private final ActorMaterializer materializer; 131 | @Nonnull private final ExecutionContext context; 132 | 133 | /** 134 | * Establishes a connection to Mesos and provides a simple interface to start and stop {@link 135 | * MesosJenkinsAgent} instances. 136 | * 137 | * @param master The Mesos master address to connect to. Should be one of host:port 138 | * http://host:port zk://host1:port1,host2:port2,.../path 139 | * zk://username:password@host1:port1,host2:port2,.../path 140 | * @param jenkinsUrl The Jenkins address to fetch the agent jar from. 141 | * @param agentUser The username used for executing Mesos tasks. 142 | * @param frameworkName The name of the framework the Mesos client should register as. 143 | * @param frameworkId The id of the framework the Mesos client should register for. 144 | * @param role The Mesos role to assume. 145 | * @param sslCert An optional custom SSL certificate to secure the connection to Mesos. 146 | * @param authorization An optional {@link CredentialsProvider} used to authorize with Mesos. 147 | * @throws InterruptedException 148 | * @throws ExecutionException 149 | */ 150 | public MesosApi( 151 | String master, 152 | URL jenkinsUrl, 153 | String agentUser, 154 | String frameworkName, 155 | String frameworkId, 156 | String role, 157 | Optional sslCert, 158 | Optional authorization) 159 | throws InterruptedException, ExecutionException { 160 | this.frameworkName = frameworkName; 161 | this.frameworkId = frameworkId; 162 | this.role = role; 163 | this.agentUser = agentUser; 164 | this.jenkinsUrl = jenkinsUrl; 165 | 166 | // Load settings. 167 | final ClassLoader classLoader = Jenkins.get().pluginManager.uberClassLoader; 168 | 169 | @Nonnull Config conf; 170 | if (sslCert.isPresent()) { 171 | conf = 172 | ConfigFactory.parseString( 173 | "akka.ssl-config.trustManager.stores = [{ type: \"PEM\", data: ${cert.pem} }]") 174 | .withValue("cert.pem", ConfigValueFactory.fromAnyRef(sslCert.get())) 175 | .resolve() 176 | .withFallback(ConfigFactory.load(classLoader)); 177 | } else { 178 | conf = ConfigFactory.load(classLoader); 179 | } 180 | 181 | // Create actor system. 182 | this.system = ActorSystem.create("mesos-scheduler", conf, classLoader); 183 | this.materializer = ActorMaterializer.create(system); 184 | this.context = system.dispatcher(); 185 | 186 | URL masterUrl = 187 | MasterDetector$.MODULE$ 188 | .apply(master, Metrics.getInstance(frameworkName)) 189 | .getMaster(context) 190 | .toCompletableFuture() 191 | .get(); 192 | 193 | MesosClientSettings clientSettings = 194 | MesosClientSettings.load(classLoader).withMasters(Collections.singletonList(masterUrl)); 195 | SchedulerSettings schedulerSettings = SchedulerSettings.load(classLoader); 196 | this.operationalSettings = Settings.load(classLoader); 197 | 198 | // Initialize state. 199 | this.stateMap = new ConcurrentHashMap<>(); 200 | this.repository = new MesosPodRecordRepository(); 201 | 202 | // Inject metrics and credentials provider. 203 | this.frameworkPrincipal = authorization.map(auth -> auth.getUid()); 204 | Optional credentialsProvider = 205 | authorization.map( 206 | auth -> { 207 | try { 208 | CredentialsProvider p = 209 | new DcosServiceAccountProvider( 210 | auth.getUid(), 211 | auth.getSecret(), 212 | new URL("https://master.mesos"), // TODO: do not hardcode DC/OS URL. 213 | this.system, 214 | this.materializer, 215 | this.context); 216 | return p; 217 | } catch (MalformedURLException e) { 218 | throw new RuntimeException("DC/OS URL validation failed", e); 219 | } 220 | }); 221 | 222 | // Initialize scheduler flow. 223 | logger.info("Starting USI scheduler flow."); 224 | this.session = 225 | Session.create( 226 | buildFrameworkInfo(), 227 | clientSettings, 228 | credentialsProvider, 229 | schedulerSettings, 230 | repository, 231 | this.operationalSettings, 232 | this::updateState, 233 | null, 234 | context, 235 | system, 236 | materializer); 237 | 238 | this.agentTimeout = this.operationalSettings.getAgentTimeout(); 239 | } 240 | 241 | private Protos.FrameworkInfo buildFrameworkInfo() { 242 | Protos.FrameworkID frameworkId = 243 | Protos.FrameworkID.newBuilder().setValue(this.frameworkId).build(); 244 | Protos.FrameworkInfo.Builder frameworkInfoBuilder = 245 | Protos.FrameworkInfo.newBuilder() 246 | .setUser(this.agentUser) 247 | .setName(this.frameworkName) 248 | .setId(frameworkId) 249 | .addRoles(role) 250 | .addCapabilities( 251 | Protos.FrameworkInfo.Capability.newBuilder() 252 | .setType(Protos.FrameworkInfo.Capability.Type.MULTI_ROLE)) 253 | .addCapabilities( 254 | Protos.FrameworkInfo.Capability.newBuilder() 255 | .setType(Protos.FrameworkInfo.Capability.Type.REGION_AWARE)) 256 | .addCapabilities( 257 | Protos.FrameworkInfo.Capability.newBuilder() 258 | .setType(Protos.FrameworkInfo.Capability.Type.PARTITION_AWARE)) 259 | .setFailoverTimeout(this.operationalSettings.getFailoverTimeout().getSeconds()); 260 | 261 | this.frameworkPrincipal.ifPresent(principal -> frameworkInfoBuilder.setPrincipal(principal)); 262 | 263 | return frameworkInfoBuilder.build(); 264 | } 265 | 266 | /** 267 | * Enqueue spec for a Jenkins event, passing a non-null existing podId will trigger a kill for 268 | * that pod 269 | * 270 | * @return a {@link MesosJenkinsAgent} once it's queued for running. 271 | */ 272 | public CompletionStage killAgent(String id) { 273 | return killAgent(new PodId(id)); 274 | } 275 | 276 | /** 277 | * Enqueue spec for a Jenkins event, passing a non-null existing podId will trigger a kill for 278 | * that pod 279 | * 280 | * @return a {@link MesosJenkinsAgent} once it's queued for running. 281 | */ 282 | public CompletionStage killAgent(PodId podId) { 283 | logger.info("Kill agent {}.", podId.value()); 284 | SchedulerCommand command = new KillPod(podId); 285 | return this.session 286 | .getCommands() 287 | .offer(command) 288 | .thenAccept( 289 | result -> { 290 | if (result == QueueOfferResult.dropped()) { 291 | logger.warn("USI command queue is full. Fail kill for {}", podId.value()); 292 | throw new IllegalStateException( 293 | String.format("Kill command for %s was dropped.", podId.value())); 294 | } else if (result == QueueOfferResult.enqueued()) { 295 | logger.debug("Successfully queued kill command for {}", podId.value()); 296 | } else if (result instanceof QueueOfferResult.Failure) { 297 | final Throwable ex = ((QueueOfferResult.Failure) result).cause(); 298 | throw new IllegalStateException("The USI stream failed or is closed.", ex); 299 | } else { 300 | throw new IllegalStateException( 301 | String.format("Unknown queue result %s", result.toString())); 302 | } 303 | }); 304 | } 305 | 306 | /** 307 | * Enqueue launch command for a new Jenkins agent. 308 | * 309 | * @return a {@link MesosJenkinsAgent} once it's queued for running. 310 | */ 311 | public CompletionStage enqueueAgent(String name, MesosAgentSpecTemplate spec) 312 | throws IOException, FormException, URISyntaxException { 313 | 314 | MesosJenkinsAgent mesosJenkinsAgent = 315 | new MesosJenkinsAgent( 316 | this, 317 | name, 318 | spec, 319 | "Mesos Jenkins Slave", 320 | jenkinsUrl, 321 | spec.getIdleTerminationMinutes(), 322 | spec.getReusable(), 323 | Collections.emptyList(), 324 | this.agentTimeout); 325 | LaunchPod launchCommand = spec.buildLaunchCommand(jenkinsUrl, name, this.role); 326 | 327 | stateMap.put(launchCommand.podId(), mesosJenkinsAgent); 328 | 329 | // async add agent to queue 330 | return this.session 331 | .getCommands() 332 | .offer(launchCommand) 333 | .thenApply( 334 | result -> { 335 | if (result == QueueOfferResult.enqueued()) { 336 | logger.info("Queued new agent {}", name); 337 | return mesosJenkinsAgent; 338 | } else if (result == QueueOfferResult.dropped()) { 339 | logger.warn("USI command queue is full. Fail provisioning for {}", name); 340 | throw new IllegalStateException( 341 | String.format("Launch command for %s was dropped.", name)); 342 | } else if (result instanceof QueueOfferResult.Failure) { 343 | final Throwable ex = ((QueueOfferResult.Failure) result).cause(); 344 | throw new IllegalStateException("The USI stream failed or is closed.", ex); 345 | } else { 346 | throw new IllegalStateException( 347 | String.format("Unknown queue result %s", result.toString())); 348 | } 349 | }); 350 | } 351 | 352 | public ActorMaterializer getMaterializer() { 353 | return materializer; 354 | } 355 | 356 | /** 357 | * Callback for USI to process state events. 358 | * 359 | *

This method will filter out {@link PodStatusUpdatedEvent} and pass them on to their {@link 360 | * MesosJenkinsAgent}. It should be threadsafe. 361 | * 362 | * @param event The {@link PodStatusUpdatedEvent} for a USI pod. 363 | */ 364 | private void updateState(StateEventOrSnapshot event) { 365 | if (event instanceof PodStatusUpdatedEvent) { 366 | PodStatusUpdatedEvent podStateEvent = (PodStatusUpdatedEvent) event; 367 | logger.info("Got status update for pod {}", podStateEvent.id().value()); 368 | MesosJenkinsAgent updated = 369 | stateMap.computeIfPresent( 370 | podStateEvent.id(), 371 | (id, agent) -> { 372 | agent.update(podStateEvent); 373 | return agent; 374 | }); 375 | 376 | // The agent, ie the pod, is not terminal and unknown to us. Kill it. 377 | boolean terminal = podStateEvent.newStatus().forall(PodStatus::isTerminalOrUnreachable); 378 | if (updated == null && !terminal) { 379 | killAgent(podStateEvent.id()); 380 | } 381 | if (terminal) { 382 | stateMap.remove(podStateEvent.id()); 383 | } 384 | } 385 | } 386 | 387 | // Setters 388 | 389 | public void setJenkinsUrl(URL jenkinsUrl) { 390 | this.jenkinsUrl = jenkinsUrl; 391 | } 392 | 393 | public void setAgentUser(String user) { 394 | this.agentUser = user; 395 | } 396 | 397 | /** test method to set the agent timeout duration */ 398 | public void setAgentTimeout(Duration agentTimeout) { 399 | this.agentTimeout = agentTimeout; 400 | } 401 | 402 | // Getters 403 | 404 | /** @return the name of the registered Mesos framework. */ 405 | public String getFrameworkName() { 406 | return this.frameworkName; 407 | } 408 | 409 | /** @return the id of the registered Mesos framework. */ 410 | public String getFrameworkId() { 411 | return this.frameworkId; 412 | } 413 | 414 | /** @return the role of the registered Mesos framework. */ 415 | public String getRole() { 416 | return this.role; 417 | } 418 | 419 | /** @return the current state map. */ 420 | public Map getState() { 421 | return Collections.unmodifiableMap(this.stateMap); 422 | } 423 | } 424 | --------------------------------------------------------------------------------