From 947ff3580f351d81df89380b4dcaa26bc66a3569 Mon Sep 17 00:00:00 2001 From: Kevin Klues Date: Tue, 22 Aug 2017 17:17:36 -0700 Subject: [PATCH] Refactored top level Jenkinsfile into smaller Jenkinsfiles. (#1041) There is now a subfolder called `ci` under which Jenkinsfiles for each of our various builds can be found ({linux, mac, windows}-binary and {linux, mac, windows}-integration-tests). Right now they are straight copies of the old Jenkinsfile, with the unnecessary stuff from building on other platforms / configurations stripped out. There is *alot* of cleanup / optimization that can happen from here. I just wanted to start with the smallest incremental change from the original as possible. --- ci/Jenkinsfile-linux-binary | 130 ++++++++ ci/Jenkinsfile-linux-integration | 315 ++++++++++++++++++ ci/Jenkinsfile-mac-binary | 130 ++++++++ Jenkinsfile => ci/Jenkinsfile-mac-integration | 112 +------ ci/Jenkinsfile-windows-binary | 130 ++++++++ ci/Jenkinsfile-windows-integration | 313 +++++++++++++++++ 6 files changed, 1021 insertions(+), 109 deletions(-) create mode 100644 ci/Jenkinsfile-linux-binary create mode 100644 ci/Jenkinsfile-linux-integration create mode 100644 ci/Jenkinsfile-mac-binary rename Jenkinsfile => ci/Jenkinsfile-mac-integration (77%) create mode 100644 ci/Jenkinsfile-windows-binary create mode 100644 ci/Jenkinsfile-windows-integration diff --git a/ci/Jenkinsfile-linux-binary b/ci/Jenkinsfile-linux-binary new file mode 100644 index 000000000..c7d062c9b --- /dev/null +++ b/ci/Jenkinsfile-linux-binary @@ -0,0 +1,130 @@ +#!/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"] + + +/** + * 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() + } + } + } + } +} + + +/** + * 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" + } + } +}) + + +/** + * This node bootstraps everything including creating all the test clusters, + * starting the builders, and finally destroying all the clusters once they + * are done. + */ +throttle(['dcos-cli']) { + 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([ + $class: 'GitSCM', + userRemoteConfigs: scm.userRemoteConfigs, + branches: scm.branches, + doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations, + submoduleCfg: scm.submoduleCfg, + extensions: [ + [ + $class: 'CloneOption', + shallow: true, + depth: 0, + noTags: true, + timeout: 10 + ] + ] + ]) + } + } + + 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: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', + variable: 'CF_TEMPLATE_URL'], + [$class: 'UsernamePasswordMultiBinding', + credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', + usernameVariable: 'DCOS_ADMIN_USERNAME', + passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { + + parallel builders + } + } +} diff --git a/ci/Jenkinsfile-linux-integration b/ci/Jenkinsfile-linux-integration new file mode 100644 index 000000000..7166b2f76 --- /dev/null +++ b/ci/Jenkinsfile-linux-integration @@ -0,0 +1,315 @@ +#!/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"] + +/** + * 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 + + TestCluster(WorkflowScript script, String platform) { + this.script = script + this.platform = platform + } + + /** + * Creates a new test cluster for the given platform using dcos-launch. + * Waits (blocks) for the cluster to be ready to use. + * Exits if interrupted by user. + * Retries 3 times. + * Destroys cluster on failure. + */ + def create() { + def createAttempts = 0 + while (true) { + try { + createAttempts++ + 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.ghprbPullId}-${script.env.BUILD_ID}-${createAttempts}", + "${script.env.CF_TEMPLATE_URL}")]) + + script.sh "./dcos-launch create -c ${platform}_config.yaml -i ${platform}_cluster_info.json" + script.sh "./dcos-launch wait -i ${platform}_cluster_info.json" + break + } catch(InterruptedException | hudson.AbortException e) { + script.echo("Build interrupted. Destroying cluster....") + destroy() + script.echo("Cluster destroyed. Exiting...") + throw e + } catch(Exception e) { + script.echo("Exception:" + e.toString()); + script.echo("Cluster creation failed. Destroying cluster...."); + destroy() + if (createAttempts >= 3) { + script.echo("Cluster destroyed. Maximum number of creation attempts exceeded. Exiting...") + throw e + } + script.echo("Cluster destroyed. Retrying...") + } + } + } + + /** + * Destroys a test cluster previously created using `create()`. + */ + def destroy() { + script.sh "./dcos-launch delete -i ${platform}_cluster_info.json" + } + + /** + * Retrieves 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") + } + + /** + * Retrieves 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 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) + + stage ("Create ${platform} cluster") { + cluster.create() + } + + try { + 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(InterruptedException | hudson.AbortException e) { + echo( + "Build interrupted. The DC/OS cluster at" + + " ${dcosUrl} will be destroyed.") + destroyCluster = true + throw e + } 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-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''' + } + } +}) + + +/** + * This node bootstraps everything including creating all the test clusters, + * starting the builders, and finally destroying all the clusters once they + * are done. + */ +throttle(['dcos-cli']) { + 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([ + $class: 'GitSCM', + userRemoteConfigs: scm.userRemoteConfigs, + branches: scm.branches, + doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations, + submoduleCfg: scm.submoduleCfg, + extensions: [ + [ + $class: 'CloneOption', + shallow: true, + depth: 0, + noTags: true, + timeout: 10 + ] + ] + ]) + } + } + + 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: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', + variable: 'CF_TEMPLATE_URL'], + [$class: 'UsernamePasswordMultiBinding', + credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', + usernameVariable: 'DCOS_ADMIN_USERNAME', + passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { + + parallel builders + } + } +} diff --git a/ci/Jenkinsfile-mac-binary b/ci/Jenkinsfile-mac-binary new file mode 100644 index 000000000..a4f995610 --- /dev/null +++ b/ci/Jenkinsfile-mac-binary @@ -0,0 +1,130 @@ +#!/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 = ["mac"] + + +/** + * 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() + } + } + } + } +} + + +/** + * These are the builds that can be run in parallel. + */ +def builders = [:] + + +builders['mac-binary'] = binaryBuilder('mac', 'mac')({ + stage ("Build dcos-cli binary") { + dir('dcos-cli/cli') { + sh "make binary" + sh "dist/dcos" + } + } +}) + + +/** + * This node bootstraps everything including creating all the test clusters, + * starting the builders, and finally destroying all the clusters once they + * are done. + */ +throttle(['dcos-cli']) { + 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([ + $class: 'GitSCM', + userRemoteConfigs: scm.userRemoteConfigs, + branches: scm.branches, + doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations, + submoduleCfg: scm.submoduleCfg, + extensions: [ + [ + $class: 'CloneOption', + shallow: true, + depth: 0, + noTags: true, + timeout: 10 + ] + ] + ]) + } + } + + 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: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', + variable: 'CF_TEMPLATE_URL'], + [$class: 'UsernamePasswordMultiBinding', + credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', + usernameVariable: 'DCOS_ADMIN_USERNAME', + passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { + + parallel builders + } + } +} diff --git a/Jenkinsfile b/ci/Jenkinsfile-mac-integration similarity index 77% rename from Jenkinsfile rename to ci/Jenkinsfile-mac-integration index d58ae9766..182efd7e7 100644 --- a/Jenkinsfile +++ b/ci/Jenkinsfile-mac-integration @@ -16,7 +16,7 @@ /** * These are the platforms we are building against. */ -def platforms = ["linux", "mac", "windows"] +def platforms = ["mac"] /** * This generates the `dcos_launch` config for a particular build. @@ -68,7 +68,7 @@ class TestCluster implements Serializable { script.writeFile([ "file": "${platform}_config.yaml", "text" : script.generateConfig( - "dcos-cli-${platform}-${script.env.BRANCH_NAME}-${script.env.BUILD_ID}-${createAttempts}", + "dcos-cli-${platform}-${script.env.ghprbPullId}-${script.env.BUILD_ID}-${createAttempts}", "${script.env.CF_TEMPLATE_URL}")]) script.sh "./dcos-launch create -c ${platform}_config.yaml -i ${platform}_cluster_info.json" @@ -143,37 +143,6 @@ class TestCluster implements Serializable { } -/** - * 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. @@ -258,59 +227,6 @@ def testBuilder(String platform, String nodeId, String workspace = null) { 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 ''' @@ -336,28 +252,6 @@ builders['mac-tests'] = testBuilder('mac', 'mac')({ }) -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 @@ -409,7 +303,7 @@ throttle(['dcos-cli']) { accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY'], [$class: 'StringBinding', - credentialsId: 'fd1fe0ae-113d-4096-87b2-15aa9606bb4e', + credentialsId: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', variable: 'CF_TEMPLATE_URL'], [$class: 'UsernamePasswordMultiBinding', credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', diff --git a/ci/Jenkinsfile-windows-binary b/ci/Jenkinsfile-windows-binary new file mode 100644 index 000000000..4bd5a5e20 --- /dev/null +++ b/ci/Jenkinsfile-windows-binary @@ -0,0 +1,130 @@ +#!/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 = ["windows"] + + +/** + * 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() + } + } + } + } +} + + +/** + * These are the builds that can be run in parallel. + */ +def builders = [:] + + +builders['windows-binary'] = binaryBuilder('windows', 'windows')({ + stage ("Build dcos-cli binary") { + dir('dcos-cli/cli') { + bat 'bash -c "make binary"' + bat 'dist\\dcos.exe' + } + } +}) + + +/** + * This node bootstraps everything including creating all the test clusters, + * starting the builders, and finally destroying all the clusters once they + * are done. + */ +throttle(['dcos-cli']) { + 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([ + $class: 'GitSCM', + userRemoteConfigs: scm.userRemoteConfigs, + branches: scm.branches, + doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations, + submoduleCfg: scm.submoduleCfg, + extensions: [ + [ + $class: 'CloneOption', + shallow: true, + depth: 0, + noTags: true, + timeout: 10 + ] + ] + ]) + } + } + + 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: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', + variable: 'CF_TEMPLATE_URL'], + [$class: 'UsernamePasswordMultiBinding', + credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', + usernameVariable: 'DCOS_ADMIN_USERNAME', + passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { + + parallel builders + } + } +} diff --git a/ci/Jenkinsfile-windows-integration b/ci/Jenkinsfile-windows-integration new file mode 100644 index 000000000..f966217f5 --- /dev/null +++ b/ci/Jenkinsfile-windows-integration @@ -0,0 +1,313 @@ +#!/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 = ["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 + + TestCluster(WorkflowScript script, String platform) { + this.script = script + this.platform = platform + } + + /** + * Creates a new test cluster for the given platform using dcos-launch. + * Waits (blocks) for the cluster to be ready to use. + * Exits if interrupted by user. + * Retries 3 times. + * Destroys cluster on failure. + */ + def create() { + def createAttempts = 0 + while (true) { + try { + createAttempts++ + 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.ghprbPullId}-${script.env.BUILD_ID}-${createAttempts}", + "${script.env.CF_TEMPLATE_URL}")]) + + script.sh "./dcos-launch create -c ${platform}_config.yaml -i ${platform}_cluster_info.json" + script.sh "./dcos-launch wait -i ${platform}_cluster_info.json" + break + } catch(InterruptedException | hudson.AbortException e) { + script.echo("Build interrupted. Destroying cluster....") + destroy() + script.echo("Cluster destroyed. Exiting...") + throw e + } catch(Exception e) { + script.echo("Exception:" + e.toString()); + script.echo("Cluster creation failed. Destroying cluster...."); + destroy() + if (createAttempts >= 3) { + script.echo("Cluster destroyed. Maximum number of creation attempts exceeded. Exiting...") + throw e + } + script.echo("Cluster destroyed. Retrying...") + } + } + } + + /** + * Destroys a test cluster previously created using `create()`. + */ + def destroy() { + script.sh "./dcos-launch delete -i ${platform}_cluster_info.json" + } + + /** + * Retrieves 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") + } + + /** + * Retrieves 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 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) + + stage ("Create ${platform} cluster") { + cluster.create() + } + + try { + 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(InterruptedException | hudson.AbortException e) { + echo( + "Build interrupted. The DC/OS cluster at" + + " ${dcosUrl} will be destroyed.") + destroyCluster = true + throw e + } 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['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. + */ +throttle(['dcos-cli']) { + 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([ + $class: 'GitSCM', + userRemoteConfigs: scm.userRemoteConfigs, + branches: scm.branches, + doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations, + submoduleCfg: scm.submoduleCfg, + extensions: [ + [ + $class: 'CloneOption', + shallow: true, + depth: 0, + noTags: true, + timeout: 10 + ] + ] + ]) + } + } + + 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: '8d7d3d95-fc1f-42f4-941a-ad43487fcff7', + variable: 'CF_TEMPLATE_URL'], + [$class: 'UsernamePasswordMultiBinding', + credentialsId: '323df884-742b-4099-b8b7-d764e5eb9674', + usernameVariable: 'DCOS_ADMIN_USERNAME', + passwordVariable: 'DCOS_ADMIN_PASSWORD']]) { + + parallel builders + } + } +}