Skip to content

Commit

Permalink
Run simple build on Mesos Plugin in integration test. (#35)
Browse files Browse the repository at this point in the history
Summary:
This patch introduces a simple integration test that runs a build using the Mesos plugin.
The mayor part is the introduction of a Jenkins configuration client that sets all form fields
according the the Jenkins UI.

JIRA issues: DCOS_OSS-4890
  • Loading branch information
jeschkies authored Apr 26, 2019
1 parent 3107100 commit 52b54d7
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 2 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ dependencies {
api group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'

// Test dependencies
testImplementation 'com.squareup.okhttp3:okhttp:3.14.1'
testImplementation 'javax.json:javax.json-api:1.1'
testImplementation 'org.glassfish:javax.json:1.0.4'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
testImplementation('com.mesosphere.usi:test-utils') { version { branch = usiBranch } }
Expand Down
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#Tue Apr 23 17:36:56 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
1 change: 0 additions & 1 deletion src/main/java/org/jenkinsci/plugins/mesos/MesosApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public MesosApi(
conf.getConfig("mesos-client")
.withValue("master-url", ConfigValueFactory.fromAnyRef(masterUrl.toString()));

logger.info("Config: {}", conf);
MesosClientSettings clientSettings = MesosClientSettings.fromConfig(clientConf);
system = ActorSystem.create("mesos-scheduler", conf, classLoader);
context = system.dispatcher();
Expand Down
274 changes: 274 additions & 0 deletions src/test/java/org/jenkinsci/plugins/mesos/JenkinsConfigClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package org.jenkinsci.plugins.mesos;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.jvnet.hudson.test.JenkinsRule.WebClient;

/**
* A simple client that hacks around the Jenkins configure form submit.
*
* <p>It allows to add a simple Mesos Cloud with one label.
*/
public class JenkinsConfigClient {
final OkHttpClient client;
final URL jenkinsConfigUrl;

public JenkinsConfigClient(WebClient jenkinsClient) throws IOException {
this.client = new OkHttpClient();
this.jenkinsConfigUrl = jenkinsClient.createCrumbedUrl("configSubmit");
}

/**
* Submits a Jenkins configuration form and adds a Mesos Cloud with one agent specs.
*
* @param mesosMasterUrl The URL of the Mesos master to connect to.
* @param frameworkName The Mesos framework name the plugin should use.
* @param role
* @param agentUser
* @param jenkinsUrl The Jenkins URL used as the base for Mesos tasks to download the Jenkins
* agent.
* @param label The Jenkins node label of the Mesos task.
* @param mode Jenkins mode, can be "NORMAL" or "EXCLUSIVE".
* @return A {@link Response} of the {@link OkHttpClient} request.
* @throws IOException
* @throws UnsupportedEncodingException
*/
public Response addMesosCloud(
String mesosMasterUrl,
String frameworkName,
String role,
String agentUser,
String jenkinsUrl,
String label,
String mode)
throws IOException, UnsupportedEncodingException {
final String jsonData =
addJsonDefaults(Json.createObjectBuilder(), jenkinsUrl)
.add(
"jenkins-model-GlobalCloudConfiguration",
Json.createObjectBuilder()
.add(
"cloud",
Json.createObjectBuilder()
.add("mesosMasterUrl", mesosMasterUrl)
.add("frameworkName", frameworkName)
.add("role", role)
.add("agentUser", agentUser)
.add("jenkinsUrl", jenkinsUrl)
.add(
"mesosAgentSpecTemplates",
Json.createObjectBuilder()
.add("label", label)
.add("mode", mode)
.build())
.add("stapler-class", "org.jenkinsci.plugins.mesos.MesosCloud")
.add("$class", "org.jenkinsci.plugins.mesos.MesosCloud")
.build())
.build())
.add("core:apply", "")
.build()
.toString();

final String formData =
addFormDefaults(new FormDataBuilder(), jenkinsUrl)
.add("_.mesosMasterUrl", mesosMasterUrl)
.add("_.frameworkName", frameworkName)
.add("_.role", "*")
.add("_.agentUser", agentUser)
.add("_.jenkinsUrl", jenkinsUrl)
.add("_.label", label)
.add("mode", mode)
.add("stapler-class", "org.jenkinsci.plugins.mesos.MesosCloud")
.add("$class", "org.jenkinsci.plugins.mesos.MesosCloud")
.add("core:apply", "")
.add("json", jsonData)
.build();

final MediaType FORM = MediaType.get("application/x-www-form-urlencoded");
RequestBody rawBody = RequestBody.create(FORM, formData);
Request request =
new Request.Builder()
.url(this.jenkinsConfigUrl.toString())
.addHeader("Accept", "text/html,application/xhtml+xml")
.addHeader("Origin", jenkinsUrl)
.addHeader("Upgrade-Insecure-Requests", "1")
.post(rawBody)
.build();
return this.client.newCall(request).execute();
}

/**
* Adds defaults from a manual form submit with Chrome Dev Tools.
*
* <p>The form includes a JSON field with all default configurations.
*
* @param builder The json builder that will be changed.
* @param jenkinsUrl URL for jenkins.
* @return The changed builder.
*/
private JsonObjectBuilder addJsonDefaults(JsonObjectBuilder builder, String jenkinsUrl) {
return builder
.add("system_message", "")
.add(
"jenkins-model-MasterBuildConfiguration",
Json.createObjectBuilder()
.add("numExecutors", "2")
.add("labelString", "")
.add("mode", "NORMAL")
.build())
.add(
"jenkins-model-GlobalQuietPeriodConfiguration",
Json.createObjectBuilder().add("quietPeriod", "5").build())
.add(
"jenkins-model-GlobalSCMRetryCountConfiguration",
Json.createObjectBuilder().add("scmCheckoutRetryCount", "0").build())
.add(
"jenkins-model-GlobalProjectNamingStrategyConfiguration",
Json.createObjectBuilder().build())
.add(
"jenkins-model-GlobalNodePropertiesConfiguration",
Json.createObjectBuilder()
.add("globalNodeProperties", Json.createObjectBuilder().build())
.build())
.add(
"hudson-model-UsageStatistics",
Json.createObjectBuilder()
.add("usageStatisticsCollected", Json.createObjectBuilder().build())
.build())
.add(
"jenkins-management-AdministrativeMonitorsConfiguration",
Json.createObjectBuilder()
.add(
"administrativeMonitor",
Json.createArrayBuilder()
.add("hudson.PluginManager$PluginCycleDependenciesMonitor")
.add("hudson.PluginManager$PluginUpdateMonitor")
.add("hudson.PluginWrapper$PluginWrapperAdministrativeMonitor")
.add("hudsonHomeIsFull")
.add("hudson.diagnosis.NullIdDescriptorMonitor")
.add("OldData")
.add("hudson.diagnosis.ReverseProxySetupMonitor")
.add("hudson.diagnosis.TooManyJobsButNoView")
.add("hudson.model.UpdateCenter$CoreUpdateMonitor")
.add("hudson.node_monitors.MonitorMarkedNodeOffline")
.add("hudson.triggers.SCMTrigger$AdministrativeMonitorImpl")
.add("jenkins.CLI")
.add("jenkins.diagnosis.HsErrPidList")
.add("jenkins.diagnostics.CompletedInitializationMonitor")
.add("jenkins.diagnostics.RootUrlNotSetMonitor")
.add("jenkins.diagnostics.SecurityIsOffMonitor")
.add("jenkins.diagnostics.URICheckEncodingMonitor")
.add("jenkins.model.DownloadSettings$Warning")
.add("jenkins.model.Jenkins$EnforceSlaveAgentPortAdministrativeMonitor")
.add("jenkins.security.RekeySecretAdminMonitor")
.add("jenkins.security.UpdateSiteWarningsMonitor")
.add(
"jenkins.security.apitoken.ApiTokenPropertyDisabledDefaultAdministrativeMonitor")
.add(
"jenkins.security.apitoken.ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor")
.add("legacyApiToken")
.add("jenkins.security.csrf.CSRFAdministrativeMonitor")
.add("slaveToMasterAccessControl")
.add("jenkins.security.s2m.MasterKillSwitchWarning")
.add("jenkins.slaves.DeprecatedAgentProtocolMonitor")
.build())
.build())
.add(
"jenkins-model-JenkinsLocationConfiguration",
Json.createObjectBuilder().add("url", jenkinsUrl).add("adminAddress", "").build())
.add("hudson-task-Shell", Json.createObjectBuilder().add("shell", "").build());
}

/**
* Adds defaults from a manual form submit with Chrome Dev Tools.
*
* @param builder The form data builder that will be changed.
* @param jenkinsUrl URL for jenkins.
* @return The changed builder.
* @throws UnsupportedEncodingException
*/
private FormDataBuilder addFormDefaults(FormDataBuilder builder, String jenkinsUrl)
throws UnsupportedEncodingException {
return builder
.add("system_message", "")
.add("_.numExecutors", "2")
.add("_.labelString", "")
.add("master.mode", "NORMAL")
.add("_.quietPeriod", "5")
.add("_.scmCheckoutRetryCount", "0")
.add("stapler-class", "jenkins.model.ProjectNamingStrategy$PatternProjectNamingStrategy")
.add("$class", "jenkins.model.ProjectNamingStrategy$PatternProjectNamingStrategy")
.add("_.namePattern", ".*")
.add("_.description", "")
.add("namingStrategy", "1")
.add("stapler-class", "jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy")
.add("$class", "jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy")
.add("_.usageStatisticsCollected", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("administrativeMonitor", "on")
.add("_.url", jenkinsUrl)
.add("_.adminAddress", "")
.add("_.shell", "");
}

/** Jenkins submits forms in URL encoding. This builder helps to construct such a request body. */
private static class FormDataBuilder {
final List<String> values = new ArrayList<>();

/**
* Add a key value pair to the form. Duplicates are allowed. Keys and values are URL encoded.
*
* @param key The form field key/name.
* @param value The value for the key.
* @return This builder.
* @throws UnsupportedEncodingException
*/
public FormDataBuilder add(String key, String value) throws UnsupportedEncodingException {
final String pair = URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8");
this.values.add(pair);
return this;
}

/** @return a joined string of all key/value pairs. */
public String build() {
return String.join("&", this.values);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@

import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;

import akka.actor.ActorSystem;
import akka.stream.ActorMaterializer;
import com.mesosphere.utils.mesos.MesosClusterExtension;
import com.mesosphere.utils.zookeeper.ZookeeperServerExtension;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Node.Mode;
import hudson.model.labels.LabelAtom;
import hudson.slaves.NodeProvisioner;
import hudson.tasks.Builder;
import hudson.tasks.Shell;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import jenkins.model.Jenkins;
import okhttp3.Response;
import org.jenkinsci.plugins.mesos.JenkinsConfigClient;
import org.jenkinsci.plugins.mesos.MesosAgentSpecTemplate;
import org.jenkinsci.plugins.mesos.MesosCloud;
import org.jenkinsci.plugins.mesos.MesosJenkinsAgent;
Expand Down Expand Up @@ -96,4 +104,34 @@ public void testStartAgent(TestUtils.JenkinsRule j) throws Exception {
// assert jenkins has the 1 added nodes
assertThat(Jenkins.getInstanceOrNull().getNodes(), hasSize(1));
}

@Test
public void runSimpleBuild(TestUtils.JenkinsRule j) throws Exception {

// Given: a configured Mesos Cloud.
final String label = "mesos";
final JenkinsConfigClient jenkinsClient = new JenkinsConfigClient(j.createWebClient());
final Response response =
jenkinsClient.addMesosCloud(
mesosCluster.getMesosUrl(),
"Jenkins Scheduler",
"*",
System.getProperty("user.name"),
j.getURL().toURI().resolve("jenkins").toString(),
label,
"EXCLUSIVE");
assertThat(response.code(), is(lessThan(400)));

// And: a project with a simple build command.
FreeStyleProject project = j.createFreeStyleProject("mesos-test");
final Builder step = new Shell("echo Hello");
project.getBuildersList().add(step);
project.setAssignedLabel(new LabelAtom(label));

// When: we run a build
FreeStyleBuild build = j.buildAndAssertSuccess(project);

// Then it finishes successfully and the logs contain our command.
assertThat(j.getLog(build), containsString("echo Hello"));
}
}

0 comments on commit 52b54d7

Please sign in to comment.