#!/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
    }
}
