#!/usr/bin/env groovy /** * This Jenkinsfile runs a set of parallel builders for the dcos-cli across * multiple platforms (linux/mac/windows). * * One set of builders builds the CLI into a binary on each platform. The other * set of builders runs integration tests on each platform. Under the hood, the * integration test builders use `dcos_launch` to create the DC/OS clusters for * each platform to run their tests against. Unfortunately, `dcos_luanch` only * works reliably on Linux, so we use a single linux instance to create all of * the clusters and separate linux/mac/windows instances to run the actual * tests. */ /** * These are the platforms we are building against. */ def platforms = ["linux", "mac", "windows"] /** * This generates the `dcos_launch` config for a particular build. */ def generateConfig(deploymentName, templateUrl) { return """ --- launch_config_version: 1 deployment_name: ${deploymentName} template_url: ${templateUrl} provider: aws aws_region: us-west-2 template_parameters: KeyName: default AdminLocation: 0.0.0.0/0 PublicSlaveInstanceCount: 1 SlaveInstanceCount: 1 """ } /** * This class abstracts away the functions required to create a test cluster * for each of our platforms. */ class TestCluster implements Serializable { WorkflowScript script String platform int createAttempts TestCluster(WorkflowScript script, String platform) { this.script = script this.platform = platform this.createAttempts = 0 } /** * Creates a new DC/OS cluster for the given platform using `dcos_launch`. */ def launch_create() { script.sh "./dcos-launch create -c ${platform}_config.yaml -i ${platform}_cluster_info.json" } /** * Waits for a cluster previously created with `dcos_launch` to come online. */ def launch_wait() { script.sh "./dcos-launch wait -i ${platform}_cluster_info.json" } /** * Deletes a cluster previously created using `dcos_launch`. */ def launch_delete() { script.sh "./dcos-launch delete -i ${platform}_cluster_info.json" } /** * Creates a new test cluster for the given platform. * * It first creates a custom config file with a new deployment name that * matches the given platform and then uses `launch_create()` to actually * create the cluster. */ def create() { script.sh "rm -rf ${platform}_config.yaml" script.sh "rm -rf ${platform}_cluster_info.json" script.writeFile([ "file": "${platform}_config.yaml", "text" : script.generateConfig( "dcos-cli-${platform}-${script.env.BRANCH_NAME}-${script.env.BUILD_ID}-${createAttempts}", "${script.env.CF_TEMPLATE_URL}")]) launch_create() createAttempts++ } /** * Blocks until a test cluster successfully comes online or a user * interrupts the build. * * Under the hood, `launch_create()` will be re-executed anytime a previous * creation attempt fails. The only way to exit this loop is to either * create a cluster successfully, or interrupt the build manually. */ def block() { while (true) { try { launch_wait() break } catch(InterruptedException e) { destroy() script.echo("Build interrupted. Exiting...") throw e } catch(Exception e) { destroy() if (createAttempts < 3) { create() } else { script.echo("Maximum number of creation attempts exceeded. Exiting...") throw e } } } } /** * Destroys a test cluster previously created using `create()`. */ def destroy() { launch_delete() } /** * Retreives the URL of a cluster previously created using `create()`. */ def getDcosUrl() { /* In the future, consider doing the following with jq instead of inline python (however, jq is not installed on our windows machines at the moment). */ script.sh """ ./dcos-launch describe -i ${platform}_cluster_info.json \ | python -c \ 'import sys, json; \ contents = json.load(sys.stdin); \ print(contents["masters"][0]["public_ip"], end="")' \ > ${platform}_dcos_url""" return script.readFile("${platform}_dcos_url") } /** * Retreives the ACS Token of a cluster previously created using `create()`. */ def getAcsToken() { def dcosUrl = this.getDcosUrl() /* In the future, consider doing the following with curl / jq instead of inline python (however, jq is not installed on our windows machines at the moment). */ script.sh """ python -c \ 'import requests; \ requests.packages.urllib3.disable_warnings(); \ js={"uid":"${script.env.DCOS_ADMIN_USERNAME}", \ "password": "${script.env.DCOS_ADMIN_PASSWORD}"}; \ r=requests.post("http://${dcosUrl}/acs/api/v1/auth/login", \ json=js, \ verify=False); \ print(r.json()["token"], end="")' \ > ${platform}_acs_token""" return script.readFile("${platform}_acs_token") } } /** * This function returns a closure that prepares binary builds for a specific * platform on a specific node in a specific workspace. */ def binaryBuilder(String platform, String nodeId, String workspace = null) { return { Closure _body -> def body = _body return { node(nodeId) { if (!workspace) { workspace = "${env.WORKSPACE}" } ws (workspace) { stage ('Cleanup workspace') { deleteDir() } stage ("Unstash dcos-cli repository") { unstash('dcos-cli') } body() } } } } } /** * This function returns a closure that prepares a test environment for a * specific platform on a specific node in a specific workspace. */ def testBuilder(String platform, String nodeId, String workspace = null) { return { Closure _body -> def body = _body return { def destroyCluster = true def cluster = new TestCluster(this, platform) try { stage ("Create ${platform} cluster") { cluster.create() } stage ("Wait for ${platform} cluster") { cluster.block() } def dcosUrl = cluster.getDcosUrl() def acsToken = cluster.getAcsToken() node(nodeId) { if (!workspace) { workspace = "${env.WORKSPACE}" } ws (workspace) { stage ('Cleanup workspace') { deleteDir() } stage ("Unstash dcos-cli repository") { unstash('dcos-cli') } withCredentials( [[$class: 'FileBinding', credentialsId: '1c206779-acc0-4844-97f6-7b3ed081a456', variable: 'DCOS_SNAKEOIL_CRT_PATH'], [$class: 'FileBinding', credentialsId: '23743034-1ac4-49f7-b2e6-a661aee2d11b', variable: 'CLI_TEST_SSH_KEY_PATH']]) { withEnv(["DCOS_URL=${dcosUrl}", "DCOS_ACS_TOKEN=${acsToken}"]) { try { body() } catch (Exception e) { echo( "Build failed. The DC/OS cluster at" + " ${dcosUrl} will remain temporarily" + " active so you can debug what went" + " wrong.") destroyCluster = false throw e } } } } } } finally { if (destroyCluster) { stage ("Destroy ${platform} cluster") { try { cluster.destroy() } catch (Exception e) {} } } } } } } /** * These are the builds that can be run in parallel. */ def builders = [:] builders['linux-binary'] = binaryBuilder('linux', 'py35', '/workspace')({ stage ("Build dcos-cli binary") { dir('dcos-cli/cli') { sh "make binary" sh "dist/dcos" } } }) builders['mac-binary'] = binaryBuilder('mac', 'mac')({ stage ("Build dcos-cli binary") { dir('dcos-cli/cli') { sh "make binary" sh "dist/dcos" } } }) builders['windows-binary'] = binaryBuilder('windows', 'windows')({ stage ("Build dcos-cli binary") { dir('dcos-cli/cli') { bat 'bash -c "make binary"' bat 'dist\\dcos.exe' } } }) builders['linux-tests'] = testBuilder('linux', 'py35', '/workspace')({ stage ("Run dcos-cli tests") { sh ''' rm -rf ~/.dcos; \ grep -q "^.* dcos.snakeoil.mesosphere.com$" /etc/hosts && \ sed -iold "s/^.* dcos.snakeoil.mesosphere.com$/${DCOS_URL} dcos.snakeoil.mesosphere.com/" /etc/hosts || \ echo ${DCOS_URL} dcos.snakeoil.mesosphere.com >> /etc/hosts''' dir('dcos-cli/cli') { sh ''' export PYTHONIOENCODING=utf-8; \ export DCOS_CONFIG=tests/data/dcos.toml; \ chmod 600 ${DCOS_CONFIG}; \ echo dcos_acs_token = \\\"${DCOS_ACS_TOKEN}\\\" >> ${DCOS_CONFIG}; \ cat ${DCOS_CONFIG}; \ unset DCOS_URL; \ unset DCOS_ACS_TOKEN; \ make test-binary''' } } }) builders['mac-tests'] = testBuilder('mac', 'mac')({ stage ("Run dcos-cli tests") { sh ''' rm -rf ~/.dcos; \ cp /etc/hosts hosts.local; \ grep -q "^.* dcos.snakeoil.mesosphere.com$" hosts.local && \ sed -iold "s/^.* dcos.snakeoil.mesosphere.com$/${DCOS_URL} dcos.snakeoil.mesosphere.com/" hosts.local || \ echo ${DCOS_URL} dcos.snakeoil.mesosphere.com >> hosts.local; \ sudo cp ./hosts.local /etc/hosts''' dir('dcos-cli/cli') { sh ''' export PYTHONIOENCODING=utf-8; \ export DCOS_CONFIG=tests/data/dcos.toml; \ chmod 600 ${DCOS_CONFIG}; \ echo dcos_acs_token = \\\"${DCOS_ACS_TOKEN}\\\" >> ${DCOS_CONFIG}; \ cat ${DCOS_CONFIG}; \ unset DCOS_URL; \ unset DCOS_ACS_TOKEN; \ make test-binary''' } } }) builders['windows-tests'] = testBuilder('windows', 'windows', 'C:\\windows\\workspace')({ stage ("Run dcos-cli tests") { bat ''' bash -c "rm -rf ~/.dcos"''' bat ''' echo %DCOS_URL% dcos.snakeoil.mesosphere.com >> C:\\windows\\system32\\drivers\\etc\\hosts & echo dcos_acs_token = \"%DCOS_ACS_TOKEN%\" >> dcos-cli\\cli\\tests\\data\\dcos.toml''' dir('dcos-cli/cli') { bat ''' bash -c " \ export PYTHONIOENCODING=utf-8; \ export DCOS_CONFIG=tests/data/dcos.toml; \ cat ${DCOS_CONFIG}; \ unset DCOS_URL; \ unset DCOS_ACS_TOKEN; \ make test-binary"''' } } }) /** * This node bootstraps everything including creating all the test clusters, * starting the builders, and finally destroying all the clusters once they * are done. */ node('py35') { stage ('Cleanup workspace') { deleteDir() } stage ('Update node') { sh 'pip install requests' } stage ('Download dcos-launch') { sh 'wget https://downloads.dcos.io/dcos-test-utils/bin/linux/dcos-launch' sh 'chmod a+x dcos-launch' } stage ('Pull dcos-cli repository') { dir('dcos-cli') { checkout scm } } stage ('Stash dcos-cli repository') { stash(['includes': 'dcos-cli/**', name: 'dcos-cli']) } withCredentials( [[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: '7155bd15-767d-4ae3-a375-e0d74c90a2c4', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY'], [$class: 'StringBinding', credentialsId: 'fd1fe0ae-113d-4096-87b2-15aa9606bb4e', variable: 'CF_TEMPLATE_URL'], [$class: 'UsernamePasswordMultiBinding', credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', usernameVariable: 'DCOS_ADMIN_USERNAME', passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { parallel builders } }