mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge pull request #9577 from vector-im/dbkr/convert_redeploy_server_to_buildkite
Convert redeploy.py to buildkite
This commit is contained in:
		
						commit
						00d33599f0
					
				@ -2,7 +2,8 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# auto-deploy script for https://riot.im/develop
 | 
					# auto-deploy script for https://riot.im/develop
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Listens for HTTP hits. When it gets one, downloads the artifact from jenkins
 | 
					# Listens for buildkite webhook pokes (https://buildkite.com/docs/apis/webhooks)
 | 
				
			||||||
 | 
					# When it gets one, downloads the artifact from buildkite
 | 
				
			||||||
# and deploys it as the new version.
 | 
					# and deploys it as the new version.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Requires the following python packages:
 | 
					# Requires the following python packages:
 | 
				
			||||||
@ -16,6 +17,8 @@ import time
 | 
				
			|||||||
import traceback
 | 
					import traceback
 | 
				
			||||||
from urlparse import urljoin
 | 
					from urlparse import urljoin
 | 
				
			||||||
import glob
 | 
					import glob
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from flask import Flask, jsonify, request, abort
 | 
					from flask import Flask, jsonify, request, abort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,10 +26,11 @@ from deploy import Deployer, DeployException
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
arg_jenkins_url = None
 | 
					 | 
				
			||||||
deployer = None
 | 
					deployer = None
 | 
				
			||||||
arg_extract_path = None
 | 
					arg_extract_path = None
 | 
				
			||||||
arg_symlink = None
 | 
					arg_symlink = None
 | 
				
			||||||
 | 
					arg_webhook_token = None
 | 
				
			||||||
 | 
					arg_api_token = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_symlink(source, linkname):
 | 
					def create_symlink(source, linkname):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
@ -39,81 +43,98 @@ def create_symlink(source, linkname):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            raise e
 | 
					            raise e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def req_headers():
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        "Authorization": "Bearer %s" % (arg_api_token,),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/", methods=["POST"])
 | 
					@app.route("/", methods=["POST"])
 | 
				
			||||||
def on_receive_jenkins_poke():
 | 
					def on_receive_buildkite_poke():
 | 
				
			||||||
    # {
 | 
					    got_webhook_token = request.headers.get('X-Buildkite-Token')
 | 
				
			||||||
    #    "name": "VectorWebDevelop",
 | 
					    if got_webhook_token != arg_webbook_token:
 | 
				
			||||||
    #    "build": {
 | 
					        print("Denying request with incorrect webhook token: %s" % (got_webhook_token,))
 | 
				
			||||||
    #        "number": 8
 | 
					        abort(400, "Incorrect webhook token")
 | 
				
			||||||
    #    }
 | 
					        return
 | 
				
			||||||
    # }
 | 
					
 | 
				
			||||||
 | 
					    required_api_prefix = None
 | 
				
			||||||
 | 
					    if arg_buildkit_org is not None:
 | 
				
			||||||
 | 
					        required_api_prefix = 'https://api.buildkite.com/v2/organizations/%s' % (arg_buildkit_org,)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    incoming_json = request.get_json()
 | 
					    incoming_json = request.get_json()
 | 
				
			||||||
    if not incoming_json:
 | 
					    if not incoming_json:
 | 
				
			||||||
        abort(400, "No JSON provided!")
 | 
					        abort(400, "No JSON provided!")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    print("Incoming JSON: %s" % (incoming_json,))
 | 
					    print("Incoming JSON: %s" % (incoming_json,))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    job_name = incoming_json.get("name")
 | 
					    event = incoming_json.get("event")
 | 
				
			||||||
    if not isinstance(job_name, basestring):
 | 
					    if event is None:
 | 
				
			||||||
        abort(400, "Bad job name: %s" % (job_name,))
 | 
					        abort(400, "No 'event' specified")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    build_num = incoming_json.get("build", {}).get("number", 0)
 | 
					    if event == 'ping':
 | 
				
			||||||
    if not build_num or build_num <= 0 or not isinstance(build_num, int):
 | 
					        print("Got ping request - responding")
 | 
				
			||||||
        abort(400, "Missing or bad build number")
 | 
					        return jsonify({'response': 'pong!'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if event != 'build.finished':
 | 
				
			||||||
 | 
					        print("Rejecting '%s' event")
 | 
				
			||||||
 | 
					        abort(400, "Unrecognised event")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return fetch_jenkins_build(job_name, build_num)
 | 
					    build_obj = incoming_json.get("build")
 | 
				
			||||||
 | 
					    if build_obj is None:
 | 
				
			||||||
def fetch_jenkins_build(job_name, build_num):
 | 
					        abort(400, "No 'build' object")
 | 
				
			||||||
    artifact_url = urljoin(
 | 
					 | 
				
			||||||
        arg_jenkins_url, "job/%s/%s/api/json" % (job_name, build_num)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    artifact_response = requests.get(artifact_url).json()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # {
 | 
					 | 
				
			||||||
    # "actions": [],
 | 
					 | 
				
			||||||
    # "artifacts": [
 | 
					 | 
				
			||||||
    #   {
 | 
					 | 
				
			||||||
    #   "displayPath": "vector-043f6991a4ed-react-20f77d1224ef-js-0a7efe3e8bd5.tar.gz",
 | 
					 | 
				
			||||||
    #   "fileName": "vector-043f6991a4ed-react-20f77d1224ef-js-0a7efe3e8bd5.tar.gz",
 | 
					 | 
				
			||||||
    #   "relativePath": "vector-043f6991a4ed-react-20f77d1224ef-js-0a7efe3e8bd5.tar.gz"
 | 
					 | 
				
			||||||
    #   }
 | 
					 | 
				
			||||||
    # ],
 | 
					 | 
				
			||||||
    # "building": false,
 | 
					 | 
				
			||||||
    # "description": null,
 | 
					 | 
				
			||||||
    # "displayName": "#11",
 | 
					 | 
				
			||||||
    # "duration": 137976,
 | 
					 | 
				
			||||||
    # "estimatedDuration": 132008,
 | 
					 | 
				
			||||||
    # "executor": null,
 | 
					 | 
				
			||||||
    # "fullDisplayName": "VectorWebDevelop #11",
 | 
					 | 
				
			||||||
    # "id": "11",
 | 
					 | 
				
			||||||
    # "keepLog": false,
 | 
					 | 
				
			||||||
    # "number": 11,
 | 
					 | 
				
			||||||
    # "queueId": 12254,
 | 
					 | 
				
			||||||
    # "result": "SUCCESS",
 | 
					 | 
				
			||||||
    # "timestamp": 1454432640079,
 | 
					 | 
				
			||||||
    # "url": "http://matrix.org/jenkins/job/VectorWebDevelop/11/",
 | 
					 | 
				
			||||||
    # "builtOn": "",
 | 
					 | 
				
			||||||
    # "changeSet": {},
 | 
					 | 
				
			||||||
    # "culprits": []
 | 
					 | 
				
			||||||
    # }
 | 
					 | 
				
			||||||
    if artifact_response.get("result") != "SUCCESS":
 | 
					 | 
				
			||||||
        abort(404, "Not deploying. Build was not marked as SUCCESS.")
 | 
					 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if len(artifact_response.get("artifacts", [])) != 1:
 | 
					    build_url = build_obj.get('url')
 | 
				
			||||||
        abort(404, "Not deploying. Build has an unexpected number of artifacts.")
 | 
					    if build_url is None:
 | 
				
			||||||
 | 
					        abort(400, "build has no url")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tar_gz_path = artifact_response["artifacts"][0]["relativePath"]
 | 
					    if required_api_prefix is not None and not build_url.startswith(required_api_prefix):
 | 
				
			||||||
    if not tar_gz_path.endswith(".tar.gz"):
 | 
					        print("Denying poke for build url with incorrect prefix: %s" % (build_url,))
 | 
				
			||||||
        abort(404, "Not deploying. Artifact is not a .tar.gz file")
 | 
					        abort(400, "Invalid build url")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tar_gz_url = urljoin(
 | 
					    build_num = build_obj.get('number')
 | 
				
			||||||
        arg_jenkins_url, "job/%s/%s/artifact/%s" % (job_name, build_num, tar_gz_path)
 | 
					    if build_num is None:
 | 
				
			||||||
    )
 | 
					        abort(400, "build has no number")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pipeline_obj = incoming_json.get("pipeline")
 | 
				
			||||||
 | 
					    if pipeline_obj is None:
 | 
				
			||||||
 | 
					        abort(400, "No 'pipeline' object")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pipeline_name = pipeline_obj.get('name')
 | 
				
			||||||
 | 
					    if pipeline_name is None:
 | 
				
			||||||
 | 
					        abort(400, "pipeline has no name")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    artifacts_url = build_url + "/artifacts"
 | 
				
			||||||
 | 
					    artifacts_resp = requests.get(artifacts_url, headers=req_headers())
 | 
				
			||||||
 | 
					    artifacts_resp.raise_for_status()
 | 
				
			||||||
 | 
					    artifacts_array = artifacts_resp.json()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for artifact in artifacts_array:
 | 
				
			||||||
 | 
					        artifact_to_deploy = None
 | 
				
			||||||
 | 
					        if re.match(r"dist/.*.tar.gz", artifact['path']):
 | 
				
			||||||
 | 
					            artifact_to_deploy = artifact
 | 
				
			||||||
 | 
					        if artifact_to_deploy is None:
 | 
				
			||||||
 | 
					            print("No suitable artifacts found")
 | 
				
			||||||
 | 
					            return jsonify({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # double paranoia check: make sure the artifact is on the right org too
 | 
				
			||||||
 | 
					    if required_api_prefix is not None and not artifact_to_deploy['url'].startswith(required_api_prefix):
 | 
				
			||||||
 | 
					        print("Denying poke for build url with incorrect prefix: %s" % (artifact_to_deploy['url'],))
 | 
				
			||||||
 | 
					        abort(400, "Refusing to deploy artifact from URL %s", artifact_to_deploy['url'])
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return deploy_buildkite_artifact(artifact_to_deploy, pipeline_name, build_num)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def deploy_buildkite_artifact(artifact, pipeline_name, build_num):
 | 
				
			||||||
 | 
					    artifact_response = requests.get(artifact['url'], headers=req_headers())
 | 
				
			||||||
 | 
					    artifact_response.raise_for_status()
 | 
				
			||||||
 | 
					    artifact_obj = artifact_response.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # we extract into a directory based on the build number. This avoids the
 | 
					    # we extract into a directory based on the build number. This avoids the
 | 
				
			||||||
    # problem of multiple builds building the same git version and thus having
 | 
					    # problem of multiple builds building the same git version and thus having
 | 
				
			||||||
@ -122,9 +143,9 @@ def fetch_jenkins_build(job_name, build_num):
 | 
				
			|||||||
    #       a good deploy with a bad one
 | 
					    #       a good deploy with a bad one
 | 
				
			||||||
    #   (b) we'll be overwriting the live deployment, which means people might
 | 
					    #   (b) we'll be overwriting the live deployment, which means people might
 | 
				
			||||||
    #       see half-written files.
 | 
					    #       see half-written files.
 | 
				
			||||||
    build_dir = os.path.join(arg_extract_path, "%s-#%s" % (job_name, build_num))
 | 
					    build_dir = os.path.join(arg_extract_path, "%s-#%s" % (pipeline_name, build_num))
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        extracted_dir = deploy_tarball(tar_gz_url, build_dir)
 | 
					        extracted_dir = deploy_tarball(artifact_obj, build_dir)
 | 
				
			||||||
    except DeployException as e:
 | 
					    except DeployException as e:
 | 
				
			||||||
        traceback.print_exc()
 | 
					        traceback.print_exc()
 | 
				
			||||||
        abort(400, e.message)
 | 
					        abort(400, e.message)
 | 
				
			||||||
@ -133,7 +154,7 @@ def fetch_jenkins_build(job_name, build_num):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return jsonify({})
 | 
					    return jsonify({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def deploy_tarball(tar_gz_url, build_dir):
 | 
					def deploy_tarball(artifact, build_dir):
 | 
				
			||||||
    """Download a tarball from jenkins and unpack it
 | 
					    """Download a tarball from jenkins and unpack it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns:
 | 
					    Returns:
 | 
				
			||||||
@ -145,20 +166,22 @@ def deploy_tarball(tar_gz_url, build_dir):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    os.mkdir(build_dir)
 | 
					    os.mkdir(build_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Download the tarball here as buildkite needs auth to do this
 | 
				
			||||||
 | 
					    # we don't pgp-sign buildkite artifacts, relying on HTTPS and buildkite
 | 
				
			||||||
 | 
					    # not being evil. If that's not good enough for you, don't use riot.im/develop.
 | 
				
			||||||
 | 
					    resp = requests.get(artifact['download_url'], stream=True, headers=req_headers())
 | 
				
			||||||
 | 
					    resp.raise_for_status()
 | 
				
			||||||
 | 
					    with open(artifact['filename'], 'wb') as ofp:
 | 
				
			||||||
 | 
					        shutil.copyfileobj(resp.raw, ofp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # we rely on the fact that flask only serves one request at a time to
 | 
					    # we rely on the fact that flask only serves one request at a time to
 | 
				
			||||||
    # ensure that we do not overwrite a tarball from a concurrent request.
 | 
					    # ensure that we do not overwrite a tarball from a concurrent request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return deployer.deploy(tar_gz_url, build_dir)
 | 
					    return deployer.deploy(artifact['filename'], build_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    parser = argparse.ArgumentParser("Runs a Vector redeployment server.")
 | 
					    parser = argparse.ArgumentParser("Runs a Vector redeployment server.")
 | 
				
			||||||
    parser.add_argument(
 | 
					 | 
				
			||||||
        "-j", "--jenkins", dest="jenkins", default="https://matrix.org/jenkins/", help=(
 | 
					 | 
				
			||||||
            "The base URL of the Jenkins web server. This will be hit to get the\
 | 
					 | 
				
			||||||
            built artifacts (the .gz file) for redeploying."
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        "-p", "--port", dest="port", default=4000, type=int, help=(
 | 
					        "-p", "--port", dest="port", default=4000, type=int, help=(
 | 
				
			||||||
            "The port to listen on for requests from Jenkins."
 | 
					            "The port to listen on for requests from Jenkins."
 | 
				
			||||||
@ -204,13 +227,33 @@ if __name__ == "__main__":
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--webhook-token", dest="webhook_token", help=(
 | 
				
			||||||
 | 
					            "Only accept pokes with this buildkite token."
 | 
				
			||||||
 | 
					        ), required=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--api-token", dest="api_token", help=(
 | 
				
			||||||
 | 
					            "API access token for buildkite. Require read_artifacts scope."
 | 
				
			||||||
 | 
					        ), required=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # We require a matching webhook token, but because we take everything else
 | 
				
			||||||
 | 
					    # about what to deploy from the poke body, we can be a little more paranoid
 | 
				
			||||||
 | 
					    # and only accept builds / artifacts from a specific buildkite org
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--org", dest="buildkite_org", help=(
 | 
				
			||||||
 | 
					            "Lock down to this buildkite org"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    args = parser.parse_args()
 | 
					    args = parser.parse_args()
 | 
				
			||||||
    if args.jenkins.endswith("/"): # important for urljoin
 | 
					 | 
				
			||||||
        arg_jenkins_url = args.jenkins
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        arg_jenkins_url = args.jenkins + "/"
 | 
					 | 
				
			||||||
    arg_extract_path = args.extract
 | 
					    arg_extract_path = args.extract
 | 
				
			||||||
    arg_symlink = args.symlink
 | 
					    arg_symlink = args.symlink
 | 
				
			||||||
 | 
					    arg_webbook_token = args.webhook_token
 | 
				
			||||||
 | 
					    arg_api_token = args.api_token
 | 
				
			||||||
 | 
					    arg_buildkit_org = args.buildkit_org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not os.path.isdir(arg_extract_path):
 | 
					    if not os.path.isdir(arg_extract_path):
 | 
				
			||||||
        os.mkdir(arg_extract_path)
 | 
					        os.mkdir(arg_extract_path)
 | 
				
			||||||
@ -222,25 +265,17 @@ if __name__ == "__main__":
 | 
				
			|||||||
    for include in args.include:
 | 
					    for include in args.include:
 | 
				
			||||||
        deployer.symlink_paths.update({ os.path.basename(pth): pth for pth in glob.iglob(include) })
 | 
					        deployer.symlink_paths.update({ os.path.basename(pth): pth for pth in glob.iglob(include) })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # we don't pgp-sign jenkins artifacts; instead we rely on HTTPS access to
 | 
					 | 
				
			||||||
    # the jenkins server (and the jenkins server not being compromised and/or
 | 
					 | 
				
			||||||
    # github not serving it compromised source). If that's not good enough for
 | 
					 | 
				
			||||||
    # you, don't use riot.im/develop.
 | 
					 | 
				
			||||||
    deployer.verify_signature = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if args.tarball_uri is not None:
 | 
					    if args.tarball_uri is not None:
 | 
				
			||||||
        build_dir = os.path.join(arg_extract_path, "test-%i" % (time.time()))
 | 
					        build_dir = os.path.join(arg_extract_path, "test-%i" % (time.time()))
 | 
				
			||||||
        deploy_tarball(args.tarball_uri, build_dir)
 | 
					        deploy_tarball(args.tarball_uri, build_dir)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        print(
 | 
					        print(
 | 
				
			||||||
            "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Include files: %s" %
 | 
					            "Listening on port %s. Extracting to %s%s. Symlinking to %s. Include files: %s" %
 | 
				
			||||||
            (args.port,
 | 
					            (args.port,
 | 
				
			||||||
             arg_extract_path,
 | 
					             arg_extract_path,
 | 
				
			||||||
             " (clean after)" if deployer.should_clean else "",
 | 
					             " (clean after)" if deployer.should_clean else "",
 | 
				
			||||||
             arg_symlink,
 | 
					             arg_symlink,
 | 
				
			||||||
             arg_jenkins_url,
 | 
					 | 
				
			||||||
             deployer.symlink_paths,
 | 
					             deployer.symlink_paths,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        app.run(host="0.0.0.0", port=args.port, debug=True)
 | 
					        app.run(port=args.port, debug=False)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user