diff --git a/test/cluster/keytar/Dockerfile b/test/cluster/keytar/Dockerfile new file mode 100644 index 00000000000..06456cd6b7e --- /dev/null +++ b/test/cluster/keytar/Dockerfile @@ -0,0 +1,43 @@ +# Dockerfile for generating the keytar image. See README.md for more information. +FROM debian:jessie + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update -y \ + && apt-get install --no-install-recommends -y -q \ + apt-utils \ + apt-transport-https \ + build-essential \ + curl \ + python2.7 \ + python2.7-dev \ + python-pip \ + git \ + wget \ + && pip install -U pip \ + && pip install virtualenv + +RUN echo "deb https://packages.cloud.google.com/apt cloud-sdk-jessie main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list +RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +RUN apt-get update -y && apt-get install -y google-cloud-sdk && apt-get install -y kubectl + +WORKDIR /app +RUN virtualenv /env +ADD requirements.txt /app/requirements.txt +RUN /env/bin/pip install -r /app/requirements.txt +ADD keytar.py test_runner.py /app/ +ADD static /app/static + +ENV USER keytar + +ENV PYTHONPATH /env/lib/python2.7/site-packages +ENV CLOUDSDK_PYTHON_SITEPACKAGES $PYTHONPATH + +RUN /bin/bash -c "source ~/.bashrc" + +EXPOSE 8080 +CMD [] +ENTRYPOINT ["/env/bin/python", "keytar.py"] + +ENV PATH /env/bin:$PATH + diff --git a/test/cluster/keytar/README.md b/test/cluster/keytar/README.md new file mode 100644 index 00000000000..081957d192a --- /dev/null +++ b/test/cluster/keytar/README.md @@ -0,0 +1,43 @@ +# Keytar + +Keytar is an internally used Vitess system for continuous execution of cluster tests on Kubernetes/Google Cloud. It monitors docker images on [Docker Hub](https://hub.docker.com). When a new image is uploaded to Docker Hub, Keytar starts a cluster on Google Compute Engine (GKE) and runs Kubernetes applications for the purpose of executing cluster tests. It will then locally run tests against the cluster. It exposes a simple web status page showing test results. + +## Setup + +How to set up Keytar for Vitess: + +* Create service account keys with GKE credentials on the account to run the tests on. Follow [step 1 from the GKE developers page](https://developers.google.com/identity/protocols/application-default-credentials?hl=en_US#howtheywork). +* Move the generated keyfile to `$VTTOP/test/cluster/keytar/config`. +* Create or modify the test configuration file (`$VTTOP/test/cluster/keytar/config/vitess_config.yaml`). +* Ensure the configuration has the correct values for GKE project name and keyfile: + ``` + cluster_setup: + - type: gke + project_name: + keyfile: /config/ + ``` +* Then run the following commands: + ``` + > cd $VTTOP/test/cluster/keytar + > KEYTAR_PASSWORD= KEYTAR_PORT= KEYTAR_CONFIG= ./keytar-up.sh + ``` +* Add a Docker Hub webhook pointing to the Keytar service. The webhook URL should be in the form: + ``` + http://:80/test_request?password= + ``` + +## Dashboard + +The script to start Keytar should output a web address to view the current status. If not, the following command can also be run: +```shell +> kubectl get service keytar -o template --template '{{if ge (len .status.loadBalancer) 1}}{{index (index .status.loadBalancer.ingress 0) "ip"}}{{end}}' +``` + +## Limitations + +Currently, Keytar has the following limitations: + +* Only one configuration file allowed at a time. +* Configuration cannot be updated dynamically. +* Test results are saved in memory and are not durable. +* Results are only shown on the dashboard, there is no notification mechanism. diff --git a/test/cluster/keytar/config/vitess_config.yaml b/test/cluster/keytar/config/vitess_config.yaml new file mode 100644 index 00000000000..e285661a95b --- /dev/null +++ b/test/cluster/keytar/config/vitess_config.yaml @@ -0,0 +1,45 @@ +install: + dependencies: + - python-mysqldb + extra: + - apt-get update + - wget https://storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz + - tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz + - wget https://storage.googleapis.com/kubernetes-helm/helm-v2.1.3-linux-amd64.tar.gz + - tar -zxvf helm-v2.1.3-linux-amd64.tar.gz + - pip install numpy + - pip install selenium + - pip install --upgrade grpcio==1.0.4 + path: + - /usr/local/go/bin + - /app/linux-amd64/ + cluster_setup: + - type: gke + keyfile: /config/keyfile.json +config: + - docker_image: vitess/root + github: + repo: youtube/vitess + repo_prefix: src/github.com/youtube/vitess + environment: + sandbox: test/cluster/sandbox/vitess_kubernetes_sandbox.py + config: test/cluster/sandbox/example_sandbox.yaml + cluster_type: gke + application_type: k8s + before_test: + - export VTTOP=$(pwd) + - export VTROOT="${VTROOT:-${VTTOP/\/src\/github.com\/youtube\/vitess/}}" + - export GOPATH=$VTROOT + - export PYTHONPATH=$VTTOP/py:$VTTOP/test:$VTTOP/test/cluster/sandbox:/usr/lib/python2.7/dist-packages:/env/lib/python2.7/site-packages + - go get github.com/youtube/vitess/go/cmd/vtctlclient + - export PATH=$GOPATH/bin:$PATH + tests: + - file: test/cluster/drain_test.py + params: + num_drains: 1 + - file: test/cluster/backup_test.py + params: + num_backups: 1 + - file: test/cluster/reparent_test.py + params: + num_reparents: 1 diff --git a/test/cluster/keytar/dummy_test.py b/test/cluster/keytar/dummy_test.py new file mode 100755 index 00000000000..9f779ed4ca4 --- /dev/null +++ b/test/cluster/keytar/dummy_test.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +"""Dummy no-op test to be used in the webdriver test.""" + +import logging +import sys +import unittest + + +class DummyTest(unittest.TestCase): + + def test_dummy(self): + logging.info('Dummy output.') + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + del sys.argv[1:] + unittest.main() diff --git a/test/cluster/keytar/keytar-controller-template.yaml b/test/cluster/keytar/keytar-controller-template.yaml new file mode 100644 index 00000000000..ccc6f12e1d7 --- /dev/null +++ b/test/cluster/keytar/keytar-controller-template.yaml @@ -0,0 +1,30 @@ +kind: ReplicationController +apiVersion: v1 +metadata: + name: keytar +spec: + replicas: 1 + template: + metadata: + labels: + component: frontend + app: keytar + spec: + containers: + - name: keytar + image: vitess/keytar + ports: + - name: http-server + containerPort: {{port}} + resources: + limits: + memory: "4Gi" + cpu: "500m" + args: ["--config_file", "{{config}}", "--port", "{{port}}", "--password", "{{password}}"] + volumeMounts: + - name: config + mountPath: /config + volumes: + - name: config + configMap: + name: config diff --git a/test/cluster/keytar/keytar-down.sh b/test/cluster/keytar/keytar-down.sh new file mode 100755 index 00000000000..58a02b72297 --- /dev/null +++ b/test/cluster/keytar/keytar-down.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +KUBECTL=${KUBECTL:-kubectl} + +$KUBECTL delete replicationcontroller keytar +$KUBECTL delete service keytar +$KUBECTL delete configmap config +gcloud container clusters delete keytar -z us-central1-b -q +gcloud compute firewall-rules delete keytar -q diff --git a/test/cluster/keytar/keytar-service.yaml b/test/cluster/keytar/keytar-service.yaml new file mode 100644 index 00000000000..097fafdb947 --- /dev/null +++ b/test/cluster/keytar/keytar-service.yaml @@ -0,0 +1,15 @@ +kind: Service +apiVersion: v1 +metadata: + name: keytar + labels: + component: frontend + app: keytar +spec: + ports: + - port: 80 + targetPort: http-server + selector: + component: frontend + app: keytar + type: LoadBalancer diff --git a/test/cluster/keytar/keytar-up.sh b/test/cluster/keytar/keytar-up.sh new file mode 100755 index 00000000000..0c9cb2e2d3e --- /dev/null +++ b/test/cluster/keytar/keytar-up.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e + +KUBECTL=${KUBECTL:-kubectl} + +config_path=${KEYTAR_CONFIG_PATH:-"./config"} +port=${KEYTAR_PORT:-8080} +password=${KEYTAR_PASSWORD:-"defaultkey"} +config=${KEYTAR_CONFIG:-"/config/vitess_config.yaml"} + +sed_script="" +for var in config_path port config password; do + sed_script+="s,{{$var}},${!var},g;" +done + +gcloud container clusters create keytar --machine-type n1-standard-4 --num-nodes 1 --scopes cloud-platform --zone us-central1-b + +echo "Creating keytar configmap" +$KUBECTL create configmap --from-file=$config_path config + +echo "Creating keytar service" +$KUBECTL create -f keytar-service.yaml + +echo "Creating keytar controller" +cat keytar-controller-template.yaml | sed -e "$sed_script" | $KUBECTL create -f - + +echo "Creating firewall-rule" +gcloud compute firewall-rules create keytar --allow tcp:80 + +for i in `seq 1 20`; do + ip=`$KUBECTL get service keytar -o template --template '{{if ge (len .status.loadBalancer) 1}}{{index (index .status.loadBalancer.ingress 0) "ip"}}{{end}}'` + if [[ -n "$ip" ]]; then + echo "Keytar address: http://${ip}:80" + break + fi + echo "Waiting for keytar external IP" + sleep 10 +done diff --git a/test/cluster/keytar/keytar.py b/test/cluster/keytar/keytar.py new file mode 100755 index 00000000000..d1b9d1d01e4 --- /dev/null +++ b/test/cluster/keytar/keytar.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +"""Keytar flask app. + +This program is responsible for exposing an interface to trigger cluster level +tests. For instance, docker webhooks can be configured to point to this +application in order to trigger tests upon pushing new docker images. +""" + +import argparse +import collections +import datetime +import json +import logging +import os +import Queue +import shutil +import subprocess +import tempfile +import threading +import yaml + +import flask + + +app = flask.Flask(__name__) +results = collections.OrderedDict() +_TEMPLATE = ( + 'python {directory}/test_runner.py -c "{config}" -t {timestamp} ' + '-d {tempdir} -s {server}') + + +class KeytarError(Exception): + pass + + +def run_test_config(config): + """Runs a single test iteration from a configuration.""" + tempdir = tempfile.mkdtemp() + logging.info('Fetching github repository') + + # Get the github repo and clone it. + github_config = config['github'] + github_clone_args, github_repo_dir = _get_download_github_repo_args( + tempdir, github_config) + os.makedirs(github_repo_dir) + subprocess.call(github_clone_args) + + current_dir = os.getcwd() + + timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M') + results[timestamp] = { + 'timestamp': timestamp, + 'status': 'Start', + 'tests': {}, + 'docker_image': config['docker_image'] + } + + # Generate a test script with the steps described in the configuration, + # as well as the command to execute the test_runner. + with tempfile.NamedTemporaryFile(dir=tempdir, delete=False) as f: + tempscript = f.name + f.write('#!/bin/bash\n') + if 'before_test' in config: + # Change to the github repo directory, any steps to be run before the + # tests should be executed from there. + os.chdir(github_repo_dir) + for before_step in config['before_test']: + f.write('%s\n' % before_step) + server = 'http://localhost:%d' % app.config['port'] + f.write(_TEMPLATE.format( + directory=current_dir, config=yaml.dump(config), timestamp=timestamp, + tempdir=tempdir, server=server)) + os.chmod(tempscript, 0775) + + try: + subprocess.call([tempscript]) + except subprocess.CalledProcessError as e: + logging.warn('Error running test_runner: %s', str(e)) + finally: + os.chdir(current_dir) + shutil.rmtree(tempdir) + + +@app.route('/') +def index(): + return app.send_static_file('index.html') + + +@app.route('/test_results') +def test_results(): + return json.dumps([results[x] for x in sorted(results)]) + + +@app.route('/test_log') +def test_log(): + # Fetch the output from a test. + log = '%s.log' % os.path.basename(flask.request.values['log_name']) + return (flask.send_from_directory('/tmp/testlogs', log), 200, + {'Content-Type': 'text/css'}) + + +@app.route('/update_results', methods=['POST']) +def update_results(): + # Update the results dict, called from the test_runner. + update_args = flask.request.get_json() + timestamp = update_args['timestamp'] + results[timestamp].update(update_args) + return 'OK' + + +def _validate_request(keytar_password, request_values): + """Checks a request against the password provided to the service at startup. + + Raises an exception on errors, otherwise returns None. + + Args: + keytar_password: password provided to the service at startup. + request_values: dict of POST request values provided to Flask. + + Raises: + KeytarError: raised if the password is invalid. + """ + if keytar_password: + if 'password' not in request_values: + raise KeytarError('Expected password not provided in test_request!') + elif request_values['password'] != keytar_password: + raise KeytarError('Incorrect password passed to test_request!') + + +@app.route('/test_request', methods=['POST']) +def test_request(): + """Respond to a post request to execute tests. + + This expects a json payload containing the docker webhook information. + If this app is configured to use a password, the password should be passed in + as part of the POST request. + + Returns: + HTML response. + """ + try: + _validate_request(app.config['password'], flask.request.values) + except KeytarError as e: + flask.abort(400, str(e)) + webhook_data = flask.request.get_json() + repo_name = webhook_data['repository']['repo_name'] + test_configs = [c for c in app.config['keytar_config']['config'] + if c['docker_image'] == repo_name] + if not test_configs: + return 'No config found for repo_name: %s' % repo_name + for test_config in test_configs: + test_worker.add_test(test_config) + return 'OK' + + +def handle_cluster_setup(cluster_setup): + """Setups up a cluster. + + Currently only GKE is supported. This step handles setting up credentials and + ensuring a valid project name is used. + + Args: + cluster_setup: YAML cluster configuration. + + Raises: + KeytarError: raised on invalid setup configurations. + """ + if cluster_setup['type'] != 'gke': + return + + if 'keyfile' not in cluster_setup: + raise KeytarError('No keyfile found in GKE cluster setup!') + # Add authentication steps to allow keytar to start clusters on GKE. + gcloud_args = ['gcloud', 'auth', 'activate-service-account', + '--key-file', cluster_setup['keyfile']] + logging.info('authenticating using keyfile: %s', cluster_setup['keyfile']) + subprocess.call(gcloud_args) + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = cluster_setup['keyfile'] + + # Ensure that a project name is correctly set. Use the name if provided + # in the configuration, otherwise use the current project name, or else + # the first available project name. + if 'project_name' in cluster_setup: + logging.info('Setting gcloud project to %s', cluster_setup['project_name']) + subprocess.call( + ['gcloud', 'config', 'set', 'project', cluster_setup['project_name']]) + else: + config = subprocess.check_output( + ['gcloud', 'config', 'list', '--format', 'json']) + project_name = json.loads(config)['core']['project'] + if not project_name: + projects = subprocess.check_output(['gcloud', 'projects', 'list']) + first_project = projects[0]['projectId'] + logging.info('gcloud project is unset, setting it to %s', first_project) + subprocess.check_output( + ['gcloud', 'config', 'set', 'project', first_project]) + + +def handle_install_steps(keytar_config): + """Runs all config installation/setup steps. + + Args: + keytar_config: YAML keytar configuration. + """ + if 'install' not in keytar_config: + return + install_config = keytar_config['install'] + for cluster_setup in install_config.get('cluster_setup', []): + handle_cluster_setup(cluster_setup) + + # Install any dependencies using apt-get. + if 'dependencies' in install_config: + subprocess.call(['apt-get', 'update']) + os.environ['DEBIAN_FRONTEND'] = 'noninteractive' + for dep in install_config['dependencies']: + subprocess.call( + ['apt-get', 'install', '-y', '--no-install-recommends', dep]) + + # Run any additional commands if provided. + for step in install_config.get('extra', []): + os.system(step) + + # Update path environment variable. + for path in install_config.get('path', []): + os.environ['PATH'] = '%s:%s' % (path, os.environ['PATH']) + + +def _get_download_github_repo_args(tempdir, github_config): + """Get arguments for github actions. + + Args: + tempdir: Base directory to git clone into. + github_config: Configuration describing the repo, branches, etc. + + Returns: + ([string], string) for arguments to pass to git, and the directory to + clone into. + """ + repo_prefix = github_config.get('repo_prefix', 'github') + repo_dir = os.path.join(tempdir, repo_prefix) + git_args = ['git', 'clone', 'https://github.com/%s' % github_config['repo'], + repo_dir] + if 'branch' in github_config: + git_args += ['-b', github_config['branch']] + return git_args, repo_dir + + +class TestWorker(object): + """A simple test queue. HTTP requests append to this work queue.""" + + def __init__(self): + self.test_queue = Queue.Queue() + self.worker_thread = threading.Thread(target=self.worker_loop) + self.worker_thread.daemon = True + + def worker_loop(self): + # Run forever, executing tests as they are added to the queue. + while True: + item = self.test_queue.get() + run_test_config(item) + self.test_queue.task_done() + + def start(self): + self.worker_thread.start() + + def add_test(self, config): + self.test_queue.put(config) + +test_worker = TestWorker() + + +def main(): + logging.getLogger().setLevel(logging.INFO) + parser = argparse.ArgumentParser(description='Run keytar') + parser.add_argument('--config_file', help='Keytar config file', required=True) + parser.add_argument('--password', help='Password', default=None) + parser.add_argument('--port', help='Port', default=8080, type=int) + keytar_args = parser.parse_args() + with open(keytar_args.config_file, 'r') as yaml_file: + yaml_config = yaml_file.read() + if not yaml_config: + raise ValueError('No valid yaml config!') + keytar_config = yaml.load(yaml_config) + handle_install_steps(keytar_config) + + if not os.path.isdir('/tmp/testlogs'): + os.mkdir('/tmp/testlogs') + + test_worker.start() + + app.config['port'] = keytar_args.port + app.config['password'] = keytar_args.password + app.config['keytar_config'] = keytar_config + + app.run(host='0.0.0.0', port=keytar_args.port, debug=True) + + +if __name__ == '__main__': + main() diff --git a/test/cluster/keytar/keytar_test.py b/test/cluster/keytar/keytar_test.py new file mode 100644 index 00000000000..6f933acfe3a --- /dev/null +++ b/test/cluster/keytar/keytar_test.py @@ -0,0 +1,89 @@ +"""Keytar tests.""" + +import json +import os +import unittest + +import keytar + + +class KeytarTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.timestamp = '20160101_0000' + if not os.path.isdir('/tmp/testlogs'): + os.mkdir('/tmp/testlogs') + with open( + '/tmp/testlogs/%s_unittest.py.log' % cls.timestamp, 'w') as testlog: + testlog.write('foo') + + def test_validate_request(self): + keytar._validate_request('foo', {'password': 'foo'}) + keytar._validate_request(None, {'password': 'foo'}) + keytar._validate_request(None, {}) + with self.assertRaises(keytar.KeytarError): + keytar._validate_request('foo', {'password': 'foo2'}) + with self.assertRaises(keytar.KeytarError): + keytar._validate_request('foo', {}) + + def test_get_download_github_repo_args(self): + github_config = {'repo': 'youtube/vitess', 'repo_prefix': 'foo'} + + github_clone_args, repo_dir = ( + keytar._get_download_github_repo_args('/tmp', github_config)) + self.assertEquals( + github_clone_args, + ['git', 'clone', 'https://github.com/youtube/vitess', '/tmp/foo']) + self.assertEquals('/tmp/foo', repo_dir) + + github_config = { + 'repo': 'youtube/vitess', 'repo_prefix': 'foo', 'branch': 'bar'} + github_clone_args, repo_dir = ( + keytar._get_download_github_repo_args('/tmp', github_config)) + self.assertEquals( + github_clone_args, + ['git', 'clone', 'https://github.com/youtube/vitess', '/tmp/foo', '-b', + 'bar']) + self.assertEquals('/tmp/foo', repo_dir) + + def test_logs(self): + # Check GET test_results with no results. + tester = keytar.app.test_client(self) + log = tester.get('/test_log?log_name=%s_unittest.py' % self.timestamp) + self.assertEqual(log.status_code, 200) + self.assertEqual(log.data, 'foo') + + def test_results(self): + # Check GET test_results with no results. + tester = keytar.app.test_client(self) + test_results = tester.get('/test_results') + self.assertEqual(test_results.status_code, 200) + self.assertEqual(json.loads(test_results.data), []) + + # Create a test_result, GET test_results should return an entry now. + keytar.results[self.timestamp] = { + 'timestamp': self.timestamp, + 'status': 'Start', + 'tests': {}, + } + test_results = tester.get('/test_results') + self.assertEqual(test_results.status_code, 200) + self.assertEqual( + json.loads(test_results.data), + [{'timestamp': self.timestamp, 'status': 'Start', 'tests': {}}]) + + # Call POST update_results, GET test_results should return a changed entry. + tester.post( + '/update_results', data=json.dumps(dict( + timestamp='20160101_0000', status='Complete')), + follow_redirects=True, content_type='application/json') + test_results = tester.get('/test_results') + self.assertEqual(test_results.status_code, 200) + self.assertEqual( + json.loads(test_results.data), + [{'timestamp': self.timestamp, 'status': 'Complete', 'tests': {}}]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/cluster/keytar/keytar_web_test.py b/test/cluster/keytar/keytar_web_test.py new file mode 100755 index 00000000000..5c16569e2cb --- /dev/null +++ b/test/cluster/keytar/keytar_web_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +"""A keytar webdriver test.""" + +import json +import logging +import signal +import subprocess +import time +import os +from selenium import webdriver +import unittest +import urllib2 + +import environment + + +class TestKeytarWeb(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.driver = environment.create_webdriver() + port = environment.reserve_ports(1) + keytar_folder = os.path.join(environment.vttop, 'test/cluster/keytar') + cls.flask_process = subprocess.Popen( + [os.path.join(keytar_folder, 'keytar.py'), + '--config_file=%s' % os.path.join(keytar_folder, 'test_config.yaml'), + '--port=%d' % port, '--password=foo'], + preexec_fn=os.setsid) + cls.flask_addr = 'http://localhost:%d' % port + + @classmethod + def tearDownClass(cls): + os.killpg(cls.flask_process.pid, signal.SIGTERM) + cls.driver.quit() + + def _wait_for_complete_status(self, timeout_s=180): + start_time = time.time() + while time.time() - start_time < timeout_s: + if 'Complete' in self.driver.find_element_by_id('results').text: + return + self.driver.refresh() + time.sleep(5) + self.fail('Timed out waiting for test to finish.') + + def test_keytar_web(self): + self.driver.get(self.flask_addr) + req = urllib2.Request('%s/test_request?password=foo' % self.flask_addr) + req.add_header('Content-Type', 'application/json') + urllib2.urlopen( + req, json.dumps({'repository': {'repo_name': 'test/image'}})) + self._wait_for_complete_status() + logging.info('Dummy test complete.') + self.driver.find_element_by_partial_link_text('PASSED').click() + self.assertIn('Dummy output.', + self.driver.find_element_by_tag_name('body').text) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/cluster/keytar/requirements.txt b/test/cluster/keytar/requirements.txt new file mode 100644 index 00000000000..b02181264fe --- /dev/null +++ b/test/cluster/keytar/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.10 +pyyaml==3.10 diff --git a/test/cluster/keytar/static/index.html b/test/cluster/keytar/static/index.html new file mode 100644 index 00000000000..153b752ae55 --- /dev/null +++ b/test/cluster/keytar/static/index.html @@ -0,0 +1,22 @@ + + + + + + + + Keytar + + + + +
+

Waiting for test results...

+
+ + + + + diff --git a/test/cluster/keytar/static/script.js b/test/cluster/keytar/static/script.js new file mode 100644 index 00000000000..29275ffd418 --- /dev/null +++ b/test/cluster/keytar/static/script.js @@ -0,0 +1,42 @@ +$(document).ready(function() { + var resultsElement = $("#test-results"); + + var appendTestResults = function(data) { + resultsElement.empty(); + var html = " \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + "; + $.each(data, function(key, value) { + html += ""; + }); + html += "
TimeDocker ImageSandbox NameStatusTestsResults
" + value.timestamp + "" + value.docker_image + "" + value.name + "" + value.status + ""; + $.each(value.tests, function(key, val) { + html += ""; + }); + html += "
" + key + "
"; + $.each(value.tests, function(key, val) { + html += ""; + }); + html += "
" + val + "
"; + resultsElement.append(html); + }; + + // Poll every second. + var fetchTestResults = function() { + $.getJSON("/test_results").done(appendTestResults).always( + function() { + setTimeout(fetchTestResults, 60000); + }); + }; + fetchTestResults(); +}); diff --git a/test/cluster/keytar/static/style.css b/test/cluster/keytar/static/style.css new file mode 100644 index 00000000000..fd1c393fb08 --- /dev/null +++ b/test/cluster/keytar/static/style.css @@ -0,0 +1,61 @@ +body, input { + color: #123; + font-family: "Gill Sans", sans-serif; +} + +div { + overflow: hidden; + padding: 1em 0; + position: relative; + text-align: center; +} + +h1, h2, p, input, a { + font-weight: 300; + margin: 0; +} + +h1 { + color: #BDB76B; + font-size: 3.5em; +} + +h2 { + color: #999; +} + +form { + margin: 0 auto; + max-width: 50em; + text-align: center; +} + +input { + border: 0; + border-radius: 1000px; + box-shadow: inset 0 0 0 2px #BDB76B; + display: inline; + font-size: 1.5em; + margin-bottom: 1em; + outline: none; + padding: .5em 5%; + width: 55%; +} + +form a { + background: #BDB76B; + border: 0; + border-radius: 1000px; + color: #FFF; + font-size: 1.25em; + font-weight: 400; + padding: .75em 2em; + text-decoration: none; + text-transform: uppercase; + white-space: normal; +} + +p { + font-size: 1.5em; + line-height: 1.5; +} diff --git a/test/cluster/keytar/test_config.yaml b/test/cluster/keytar/test_config.yaml new file mode 100644 index 00000000000..a2f92a572d4 --- /dev/null +++ b/test/cluster/keytar/test_config.yaml @@ -0,0 +1,15 @@ +install: + path: + - /test_path +config: + - docker_image: test/image + github: + repo: youtube/vitess + repo_prefix: src/github.com/youtube/vitess + before_test: + - touch /tmp/test_file + environment: + cluster_type: gke + application_type: k8s + tests: + - file: test/cluster/keytar/dummy_test.py diff --git a/test/cluster/keytar/test_runner.py b/test/cluster/keytar/test_runner.py new file mode 100755 index 00000000000..b23d991d48b --- /dev/null +++ b/test/cluster/keytar/test_runner.py @@ -0,0 +1,115 @@ +"""Script to run a single cluster test. + +This includes the following steps: + 1. Starting a test cluster (GKE supported). + 2. Running tests against the cluster. + 3. Reporting test results. +""" + +import argparse +import json +import logging +import os +import subprocess +import urllib2 +import uuid +import yaml + +keytar_args = None + + +def update_result(k, v): + """Post a key/value pair test result update.""" + url = '%s/update_results' % keytar_args.server + req = urllib2.Request(url) + req.add_header('Content-Type', 'application/json') + urllib2.urlopen(req, json.dumps({k: v, 'timestamp': keytar_args.timestamp})) + + +def run_sandbox_action(environment_config, name, action): + """Run a sandbox action (Start/Stop). + + Args: + environment_config: yaml configuration for the sandbox. + name: unique name for the sandbox. + action: action to pass to the sandbox action parameter. + """ + if 'sandbox' not in environment_config: + return + # Execute sandbox command + sandbox_file = os.path.join(repo_dir, environment_config['sandbox']) + os.chdir(os.path.dirname(sandbox_file)) + sandbox_args = [ + './%s' % os.path.basename(sandbox_file), + '-e', environment_config['cluster_type'], '-n', name, '-k', name, + '-c', os.path.join(repo_dir, environment_config['config']), + '-a', action] + update_result('status', 'Running sandbox action: %s' % action) + try: + subprocess.check_call(sandbox_args) + update_result('status', 'Finished sandbox action: %s' % action) + except subprocess.CalledProcessError as e: + logging.info('Failed to run sandbox action %s: %s', (action, e.output)) + update_result('status', 'Sandbox failure') + + +def run_test_config(): + """Runs a single test iteration from a configuration. + + This includes bringing up an environment, running the tests, and reporting + status. + """ + # Generate a random name. Kubernetes/GKE has name length limits. + name = 'keytar%s' % format(uuid.uuid4().fields[0], 'x') + update_result('name', name) + + environment_config = config['environment'] + run_sandbox_action(environment_config, name, 'Start') + logging.info('Running tests') + update_result('status', 'Running Tests') + + try: + # Run tests and update results. + test_results = {} + for test in config['tests']: + test_file = os.path.join(repo_dir, test['file']) + test_name = os.path.basename(test_file) + logging.info('Running test %s', test_name) + os.chdir(os.path.dirname(test_file)) + test_args = [ + './%s' % test_name, + '-e', environment_config['application_type'], '-n', name] + if 'params' in test: + test_args += ['-t', ':'.join( + '%s=%s' % (k, v) for (k, v) in test['params'].iteritems())] + testlog = '/tmp/testlogs/%s_%s.log' % (keytar_args.timestamp, test_name) + logging.info('Saving log to %s', testlog) + test_results[test_name] = 'RUNNING' + update_result('tests', test_results) + with open(testlog, 'w') as results_file: + if subprocess.call(test_args, stdout=results_file, stderr=results_file): + test_results[test_name] = 'FAILED' + else: + test_results[test_name] = 'PASSED' + update_result('tests', test_results) + update_result('status', 'Tests Complete') + except Exception as e: # pylint: disable=broad-except + logging.info('Exception caught: %s', str(e)) + update_result('status', 'System Error running tests: %s' % str(e)) + finally: + run_sandbox_action(environment_config, name, 'Stop') + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + parser = argparse.ArgumentParser(description='Run keytar') + parser.add_argument('-c', '--config', help='Keytar config yaml') + parser.add_argument('-t', '--timestamp', help='Timestamp string') + parser.add_argument('-d', '--dir', help='temp dir created for the test') + parser.add_argument('-s', '--server', help='keytar server address') + keytar_args = parser.parse_args() + config = yaml.load(keytar_args.config) + repo_prefix = config['github'].get('repo_prefix', 'github') + repo_dir = os.path.join(keytar_args.dir, repo_prefix) + + run_test_config()